diff --git a/Changelog.txt b/Changelog.txt index 99528ae53..698739102 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,122 @@ +Version 2.2.007 + Fixed bug where Green Thumb did not replant if seed was in the off hand + +Version 2.2.006 + Added new config custom_item_support.yml + Added support for hex color codes in the locale file, uses the format &#RRGGBB (see notes) + Added setting to disable repair on items with custom models, this is not on by default + Fixed a bug where sometimes the locale name of a skill would get lowercased + Fixed a bug where JSON text components did not get colored properly some of the time + Fixed en_US locale string 'Commands.Skill.Leaderboard' not being colored properly + Fixed skill commands incorrectly telling you to use their locale name, this isn't currently possible + Updated outdated wiki URLs in commands to point to the new wiki + Removed the msg about skills being migrated to a new system when using /mmoinfo command + Added new locale entry 'Anvil.Repair.Reject.CustomModelData' + Added new locale entry 'Anvil.Salvage.Reject.CustomModelData' + Updated en_US locale entry 'JSON.DescriptionHeader' + (API/Codebase) Added some util methods and basic unit tests for LocaleLoader + + NOTES: + Hex Color support in locale files is here! + The hex color code format for the locale files is &#RRGGBB + An example entry applying yellow as a hex color code would look like this: + Axes.SkillName=&#FFFF00Axes + In general, JSON locale entries will either not work with hex color codes or will have the color code stripped out, in the future I will add support for the JSON components to use hex colors from the locale + + Let me know in detail what kind of support you'd like to see in mcMMO regarding custom items, I'm open to suggestions. + This update adds a new config file to allow server owners to disable repair or salvage on items with custom models, + This prevention mechanism is not enabled by default, change the settings in custom_item_support.yml if you want to enable it. + This feature is off by default for now to keep compatibility with existing servers, but it may be enabled by default in the future if feedback suggests it should be. + As a reminder, anyone can update the wiki by clicking on the "edit on github" link on various pages, this will take you to the wiki's source code on GitHub, submit a PR to make changes + +Version 2.2.005 + Fixed a bug where certain skills such as Dodge/Arrow Deflect had no skill cap and would continue improving forever + Reduced messages on startup for SQL DB + (API) Constructor for ProbabilityImpl now takes a raw value between 0 and 1 instead of an inflated percentage + (API) Added some convenience methods to Probability, and ProbabilityUtil classes + (Codebase) Added more unit tests revolving around Probability/RNG + +Version 2.2.004 + Fixed bug where values from Experience_Formula.Skill_Multiplier were not functioning + + NOTES: + A reminder that these values are multipliers and no longer divisors, if you want 10x lower XP, a value of .1 would do the job. + +Version 2.2.003 + (SQLDB) Fixed a bug where lastlogin was using a value that was too large + (SQLDB) Fixed bug where crossbows was not getting added to SQL schema for some users + +Version 2.2.002 + Fixed bug where thrown tridents did not grant XP or benefit from subskills + Fixed bug where trickshot marked bounced arrows as being shot from a bow instead of being shot from a crossbow + +Version 2.2.001 + Fixed Crossbow's Powered shot showing the text for the wrong skill from the locale when using /crossbows command + +Version 2.2.000 + General + Added Crossbows Skill, this skill is an early preview / WIP and feedback on discord is appreciated + Added Tridents Skill, this skill is an early preview / WIP and feedback on discord is appreciated + Added an "endgame" triple drop subskill 'Mother Lode' to Mining + Added an "endgame" triple drop subskill 'Clean Cuts' to Woodcutting + Added an "endgame" triple drop subskill 'Verdant Bounty' to Herbalism + Added /mmopower command which simply shows your power level (aliases /mmopowerlevel /powerlevel) + Quite a few misc bugs were patched relating to random chance, some involving perk permissions + + Config + Added 'Send_To_Console' settings to chat.yml to toggle sending party or admin chat messages to console + Replaced 'Experience_Formula.Modifier' in experience.yml with 'Experience_Formula.Skill_Multiplier' which is easier to understand and less prone to divide by zero bugs + child.yml config is gone now, feel free to delete it + Added ExploitFix.PreventArmorStandInteraction to experience.yml to prevent players from triggering mcMMO abilities off armor stands + + Tweaks + Tree Feller now drops 90% less non-wood block rewards (leaves/etc) on average from Knock on Wood. + Treasure drop rate from Shake, Fishing, Hylian, and Excavation now benefit from the Luck perk + Updated advanced.yml with entries for the new skills + + Permission nodes + Added 'mcmmo.commands.mmopower' permission node for the new /mmopower command + Added 'mcmmo.commands.crossbows' permission node + Added 'mcmmo.ability.crossbows.crossbowslimitbreak' permission node + Added 'mcmmo.ability.crossbows.trickshot' permission node + Added 'mcmmo.ability.crossbows.poweredshot' permission node + Added 'mcmmo.commands.tridents' permission node + Added 'mcmmo.ability.tridents.tridentslimitbreak' permission node + Added 'mcmmo.ability.tridents.impale' permission node + Added 'mcmmo.ability.herbalism.verdantbounty' permission node + Added 'mcmmo.ability.mining.motherlode' permission node + Added 'mcmmo.ability.woodcutting.cleancuts' permission node + + Locale + Added locale entries for all the new skills/subskills + + Codebase / Misc + PAPI's '/papi reload' command will no longer unload mcMMO (thanks gecko10000) + Major rewrite for how random chance was handled in the code + Many skills with RNG elements now send out a SubSkillEvent (which can be used to modify probability or cancel the results), some skills without RNG still send out this event when activated, this event is cancellable so it can be used to make a skill fail + A lot of new unit tests were added to help keep mcMMO stable as part of this update, of course, more could always be added. + + NOTES: + One feature of this update is to provide a endgame benefits to some skills that you can grind for a long time, ideally for a long while. I will likely expand upon this idea in future updates. + A few skills have these endgame oriented subskills, these new subskills provide a small benefit at first that grows and scales up to level 10,000 (or 1,000 for Standard mode which no one uses) and does not have ranks (other than the initial rank to unlock it). + These endgame sub skills unlock at level 1000 for users with default mcMMO settings, or 100 for those using the optional Standard scaling. + You can tweak the benefits of these skills in advanced.yml, the default settings are meant to be a good starting point. + + Crossbows and Tridents are WIP skills, I would like feedback on discord about them. + Tridents especially is very early, I have some ideas but I want to hear what you think about it. + + More info on the new Triple Drop skills (Mother Lode, Clean Cuts, Verdant Bounty): + Currently these start at about 5% chance and can reach a maximum 50% chance if a player acquired 10,000 skill, you can adjust this in advanced.yml + These skills respect double drop settings from config.yml just like the corresponding Double Drop skills do, if a double drop is disabled for an item, then its disabled for triple drops too. + I added a new Power Level Command, for now this just shows you your current power level. If I ever add features based on power level, this command will likely display output related to those features. + + Regarding Maces, I will likely add that as a WIP skill when the next Minecraft update drops. + +Version 2.1.231 + Fixed a bug preventing parties from being made without passwords (Thanks Momshroom) + Updated korean locale (thanks mangchi57) + Added some unit tests for party creation + Version 2.1.230 Fixed an error that could happen when mcMMO was saving when parties were disabled by party.yml (thanks IAISI & L4BORG) Fixed several exceptions when checking PVP damage when parties were disabled by party.yml (thanks IAISI & L4BORG) @@ -1326,7 +1445,7 @@ Version 2.1.128 Fixed a bug where certain types of ore did not receive bonuses from Blast Mining Fixed a few locale errors with commands (API) Added ExperienceAPI::addCombatXP for adding combat XP to players, signature may change so its deprecated for now - mcMMO now logs whether or not its using FlatFile or SQL database on load + mcMMO now logs whether its using FlatFile or SQL database on load (1.16) Strider added to combat experience with a value of 1.2 NOTES: A more thorough look at Unarmed balance will happen in the future, the intention of this nerf is to make Unarmed less rewarding until it is leveled quite a bit. @@ -1346,7 +1465,7 @@ Version 2.1.127 Version 2.1.126 mcMMO now relies on NMS for some of its features, if NMS cannot properly be wired up when initializing mcMMO behaviours relying on NMS will either be partially supported or disabled mcMMO now has a compatibility mode, any features that require specific versions of Minecraft for full functionality will be disabled if your server is not running a compatible version, mcMMO will still function in compatibility mode, but either the feature will be modified or disabled depending on the version of the server software - New command /mmocompat - Shows information about whether or not mcMMO is fully functional or if some features are disabled due to the server software not being fully supported. Can be used by players or console. + New command /mmocompat - Shows information about whether mcMMO is fully functional or if some features are disabled due to the server software not being fully supported. Can be used by players or console. New command /mmoxpbar (alias /xpbarsettings) - Players can choose to always show XP bars or to never show XP bars on a per skill basis XPBars now last for 3 seconds before hiding instead of 2 seconds Fixed an exploit involving fishing rods @@ -1982,7 +2101,7 @@ Version 2.1.68 Fixed a bug where consuming food in the off hand did not trigger the Diet abilities Version 2.1.67 - The XP bar now reflects whether or not the player is receiving the early game boost + The XP bar now reflects whether the player is receiving the early game boost Players who are receiving an early game boost will be shown "Learning a skill..." as the title of the XP bar while gaining XP New locale string 'XPBar.Template.EarlyGameBoost' @@ -2031,7 +2150,7 @@ Version 2.1.63 Version 2.1.62 Added a new admin notification system, sensitive commands will print chat messages to "admins" (players with either Operator status or admin chat permission) Added a setting to disable the new admin notifications to config.yml 'General.AdminNotifications' (this will be more configurable in 2.2) - OPs and players with the admin chat permission will now see details about XP rate event commands regardless of whether or not the XP rate event messages are enabled + OPs and players with the admin chat permission will now see details about XP rate event commands regardless of whether the XP rate event messages are enabled Updated hu_HU locale (thanks andris155) Added XP for mining Magma_Block (default 30 XP - Update your config, see notes) Diamond tools & armor in the repair config now have a minimum level of 0 (Update your config, temporary hotfix, 2.2 addresses this issue, see notes) @@ -2039,9 +2158,9 @@ Version 2.1.62 New locale string - 'Server.ConsoleName' the name of the server console, this will be used in place of player names when sending admin notifications out if the command was used from console New locale string - 'Notifications.Admin.Format.Others' style formatting + prefix for admin notifications used in the other new strings below New locale string - 'Notifications.Admin.Format.Self' style formatting + prefix for admin command confirmations sent to the user who executed the command - New locale string - 'Notifications.Admin.XPRate.Start.Self' sent to the user who modifies the XP rate regardless of whether or not messages for the event are enabled + New locale string - 'Notifications.Admin.XPRate.Start.Self' sent to the user who modifies the XP rate regardless of whether messages for the event are enabled New locale string - 'Notifications.Admin.XPRate.Start.Others' details of who started an XP rate event are sent to players who have Operator status or admin chat permission when the command to start or modify XP of an event has been issued - New locale string - 'Notifications.Admin.XPRate.End.Self' sent to the user who ended the XP rate event regardless of whether or not messages for the event are enabled + New locale string - 'Notifications.Admin.XPRate.End.Self' sent to the user who ended the XP rate event regardless of whether messages for the event are enabled New locale string - 'Notifications.Admin.XPRate.End.Others' details of who ended an XP rate event are sent to players who have Operator status or admin chat permission when the command to end the event has been issued NOTES: @@ -2312,7 +2431,7 @@ Version 2.1.26 Notes: The new Limit Break subskills are intended to make Prot IV players less tanky and for you to feel more powerful for having high skill level. - Limit Break has 10 ranks, each rank gives 1 extra RAW damage, this is damage before reductions from armor and enchantments. The net result is you deal about 50% more damage with an end game skill compared to before. + Limit Break has 10 ranks, each rank gives 1 extra RAW damage, this is damage before reductions from armor and enchantments. The net result is you deal about 50% more damage with an endgame skill compared to before. With these new changes, most skills can 2 shot normal diamond armor, and it takes about 5 hits to kill someone in Prot IV Diamond Armor. I'm not sure everyone will like these changes, the net result is players are a lot easier to kill now, whereas before you could take quite a beating before getting killed. I collected several sets of data before making these changes, including damage to player with and without prot 4 diamond armor, damage to those players with and without enchanted weapons, damage with and without leveling your skills, and combinations of the previously mentioned things. @@ -4013,7 +4132,7 @@ Removed performance debugging Removed some useless settings from the config file Version 1.0.34 -Fixed the PVP setting determining whether or not you would hurt yourself from AoE Abilities +Fixed the PVP setting determining whether you would hurt yourself from AoE Abilities Added Dutch (nl) language support Super Breaker now gives the correct XP as determined by config.yml Sand Stone XP is now configurable and no longer shares the 'stone' node @@ -4023,7 +4142,7 @@ Version 1.0.33 Fixed the toggle for the Excavation drop 'Cocoa Beans' Fixed bug where Unarmed users could disarm without being bare handed Cocoa Beans now have an XP modifier in config.yml -You can now toggle whether or not Mobspawners will give XP (in config.yml) +You can now toggle whether Mobspawners will give XP (in config.yml) MySQL version now makes requests to the MySQL server less frequently (should help performance) Fixed bug with Skull Splitter hitting the user diff --git a/README.md b/README.md index d69bc1143..e36806569 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,22 @@ Spigot Resource: https://spigot.mcmmo.org I plan to post links to our new wiki (its still under development), downloads, and dev blogs there. +## API +If you are using maven, you can add mcMMO API to your plugin by adding it to pom.xml like so... + +``` + + neetgames + https://nexus.neetgames.com/repository/maven-releases/ + +``` +``` + + com.gmail.nossr50.mcMMO + mcMMO + 2.2.004 + +``` ### Builds Currently, you can obtain our builds via the Spigot or Polymart: diff --git a/pom.xml b/pom.xml index 02f35f1ba..9bdddf369 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.gmail.nossr50.mcMMO mcMMO - 2.1.230 + 2.2.007-SNAPSHOT mcMMO https://github.com/mcMMO-Dev/mcMMO @@ -102,7 +102,7 @@ maven-compiler-plugin 3.10.1 - 16 + 17 -parameters @@ -258,6 +258,19 @@ + + + org.assertj + assertj-core + 3.25.3 + test + + + com.h2database + h2 + 2.2.224 + test + me.clip placeholderapi diff --git a/src/main/java/com/gmail/nossr50/api/ExperienceAPI.java b/src/main/java/com/gmail/nossr50/api/ExperienceAPI.java index f18cd18ca..c626810ec 100644 --- a/src/main/java/com/gmail/nossr50/api/ExperienceAPI.java +++ b/src/main/java/com/gmail/nossr50/api/ExperienceAPI.java @@ -9,7 +9,6 @@ import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.datatypes.player.PlayerProfile; import com.gmail.nossr50.datatypes.skills.PrimarySkillType; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.skills.child.FamilyTree; import com.gmail.nossr50.util.player.UserManager; import com.gmail.nossr50.util.skills.CombatUtils; import com.gmail.nossr50.util.skills.SkillTools; @@ -20,7 +19,6 @@ import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; -import java.util.Set; import java.util.UUID; public final class ExperienceAPI { @@ -706,7 +704,7 @@ public final class ExperienceAPI { PrimarySkillType skill = getSkillType(skillType); if (SkillTools.isChildSkill(skill)) { - Set parentSkills = FamilyTree.getParents(skill); + var parentSkills = mcMMO.p.getSkillTools().getChildSkillParents(skill); for (PrimarySkillType parentSkill : parentSkills) { profile.addLevels(parentSkill, (levels / parentSkills.size())); @@ -737,7 +735,7 @@ public final class ExperienceAPI { PrimarySkillType skill = getSkillType(skillType); if (SkillTools.isChildSkill(skill)) { - Set parentSkills = FamilyTree.getParents(skill); + var parentSkills = mcMMO.p.getSkillTools().getChildSkillParents(skill); for (PrimarySkillType parentSkill : parentSkills) { profile.addLevels(parentSkill, (levels / parentSkills.size())); diff --git a/src/main/java/com/gmail/nossr50/api/PartyAPI.java b/src/main/java/com/gmail/nossr50/api/PartyAPI.java index ce5efaf6e..e86bf4619 100644 --- a/src/main/java/com/gmail/nossr50/api/PartyAPI.java +++ b/src/main/java/com/gmail/nossr50/api/PartyAPI.java @@ -5,10 +5,8 @@ import com.gmail.nossr50.datatypes.party.Party; import com.gmail.nossr50.datatypes.party.PartyLeader; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.player.NotificationManager; import com.gmail.nossr50.util.player.UserManager; -import jdk.jfr.Experimental; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/com/gmail/nossr50/api/exceptions/ValueOutOfBoundsException.java b/src/main/java/com/gmail/nossr50/api/exceptions/ValueOutOfBoundsException.java new file mode 100644 index 000000000..4fc4a7a6f --- /dev/null +++ b/src/main/java/com/gmail/nossr50/api/exceptions/ValueOutOfBoundsException.java @@ -0,0 +1,9 @@ +package com.gmail.nossr50.api.exceptions; + +import org.jetbrains.annotations.NotNull; + +public class ValueOutOfBoundsException extends RuntimeException { + public ValueOutOfBoundsException(@NotNull String message) { + super(message); + } +} diff --git a/src/main/java/com/gmail/nossr50/chat/mailer/AdminChatMailer.java b/src/main/java/com/gmail/nossr50/chat/mailer/AdminChatMailer.java index f798f5d47..5ec0f5c37 100644 --- a/src/main/java/com/gmail/nossr50/chat/mailer/AdminChatMailer.java +++ b/src/main/java/com/gmail/nossr50/chat/mailer/AdminChatMailer.java @@ -3,6 +3,7 @@ package com.gmail.nossr50.chat.mailer; import com.gmail.nossr50.chat.author.Author; import com.gmail.nossr50.chat.message.AdminChatMessage; import com.gmail.nossr50.chat.message.ChatMessage; +import com.gmail.nossr50.config.ChatConfig; import com.gmail.nossr50.datatypes.chat.ChatChannel; import com.gmail.nossr50.events.chat.McMMOAdminChatEvent; import com.gmail.nossr50.events.chat.McMMOChatEvent; @@ -44,7 +45,7 @@ public class AdminChatMailer extends AbstractChatMailer { public @NotNull Predicate predicate() { return (commandSender) -> commandSender.isOp() || commandSender.hasPermission(MCMMO_CHAT_ADMINCHAT_PERMISSION) - || commandSender instanceof ConsoleCommandSender; + || (ChatConfig.getInstance().isConsoleIncludedInAudience(ChatChannel.ADMIN) && commandSender instanceof ConsoleCommandSender); } /** diff --git a/src/main/java/com/gmail/nossr50/chat/message/PartyChatMessage.java b/src/main/java/com/gmail/nossr50/chat/message/PartyChatMessage.java index 4c143ec69..756c3d457 100644 --- a/src/main/java/com/gmail/nossr50/chat/message/PartyChatMessage.java +++ b/src/main/java/com/gmail/nossr50/chat/message/PartyChatMessage.java @@ -1,6 +1,7 @@ package com.gmail.nossr50.chat.message; import com.gmail.nossr50.chat.author.Author; +import com.gmail.nossr50.config.ChatConfig; import com.gmail.nossr50.datatypes.chat.ChatChannel; import com.gmail.nossr50.datatypes.party.Party; import com.gmail.nossr50.datatypes.player.McMMOPlayer; @@ -51,7 +52,8 @@ public class PartyChatMessage extends AbstractChatMessage { messagePartyChatSpies(spyMessage); //Console message - mcMMO.p.getChatManager().sendConsoleMessage(author, spyMessage); + if(ChatConfig.getInstance().isConsoleIncludedInAudience(ChatChannel.PARTY)) + mcMMO.p.getChatManager().sendConsoleMessage(author, spyMessage); } /** diff --git a/src/main/java/com/gmail/nossr50/commands/CommandManager.java b/src/main/java/com/gmail/nossr50/commands/CommandManager.java index c17c7fb1d..3b9d8516e 100644 --- a/src/main/java/com/gmail/nossr50/commands/CommandManager.java +++ b/src/main/java/com/gmail/nossr50/commands/CommandManager.java @@ -5,6 +5,7 @@ import co.aikar.commands.BukkitCommandManager; import co.aikar.commands.ConditionFailedException; import com.gmail.nossr50.commands.chat.AdminChatCommand; import com.gmail.nossr50.commands.chat.PartyChatCommand; +import com.gmail.nossr50.commands.skills.PowerLevelCommand; import com.gmail.nossr50.config.ChatConfig; import com.gmail.nossr50.datatypes.chat.ChatChannel; import com.gmail.nossr50.datatypes.player.McMMOPlayer; @@ -20,9 +21,14 @@ import org.jetbrains.annotations.NotNull; * For now this class will only handle ACF converted commands, all other commands will be handled elsewhere */ public class CommandManager { + public static final @NotNull String MMO_DATA_LOADED = "mmoDataLoaded"; + + //CHAT public static final @NotNull String ADMIN_CONDITION = "adminCondition"; public static final @NotNull String PARTY_CONDITION = "partyCondition"; - public static final @NotNull String MMO_DATA_LOADED = "mmoDataLoaded"; + + //SKILLS + public static final @NotNull String POWER_LEVEL_CONDITION = "powerLevelCondition"; private final @NotNull mcMMO pluginRef; private final @NotNull BukkitCommandManager bukkitCommandManager; @@ -36,9 +42,16 @@ public class CommandManager { } private void registerCommands() { + registerSkillCommands(); //TODO: Implement other skills not just power level registerChatCommands(); } + private void registerSkillCommands() { + if(mcMMO.p.getGeneralConfig().isMasterySystemEnabled()) { + bukkitCommandManager.registerCommand(new PowerLevelCommand(pluginRef)); + } + } + /** * Registers chat commands if the chat system is enabled */ @@ -54,6 +67,23 @@ public class CommandManager { } public void registerConditions() { + registerChatCommandConditions(); //Chat Commands + registerSkillConditions(); + } + + private void registerSkillConditions() { + bukkitCommandManager.getCommandConditions().addCondition(POWER_LEVEL_CONDITION, (context) -> { + BukkitCommandIssuer issuer = context.getIssuer(); + + if(issuer.getIssuer() instanceof Player) { + validateLoadedData(issuer.getPlayer()); + } else { + throw new ConditionFailedException(LocaleLoader.getString("Commands.NoConsole")); + } + }); + } + + private void registerChatCommandConditions() { // Method or Class based - Can only be used on methods bukkitCommandManager.getCommandConditions().addCondition(ADMIN_CONDITION, (context) -> { BukkitCommandIssuer issuer = context.getIssuer(); @@ -78,6 +108,7 @@ public class CommandManager { if(bukkitCommandIssuer.getIssuer() instanceof Player) { validateLoadedData(bukkitCommandIssuer.getPlayer()); validatePlayerParty(bukkitCommandIssuer.getPlayer()); + //TODO: Is there even a point in validating permission? look into this later validatePermission("mcmmo.chat.partychat", bukkitCommandIssuer.getPlayer()); } }); diff --git a/src/main/java/com/gmail/nossr50/commands/chat/PartyChatCommand.java b/src/main/java/com/gmail/nossr50/commands/chat/PartyChatCommand.java index acd7a6168..cfdbc61d8 100644 --- a/src/main/java/com/gmail/nossr50/commands/chat/PartyChatCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/chat/PartyChatCommand.java @@ -11,7 +11,6 @@ import com.gmail.nossr50.datatypes.chat.ChatChannel; import com.gmail.nossr50.datatypes.party.Party; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.player.UserManager; import com.gmail.nossr50.util.text.StringUtils; import org.bukkit.entity.Player; diff --git a/src/main/java/com/gmail/nossr50/commands/party/PartyAcceptCommand.java b/src/main/java/com/gmail/nossr50/commands/party/PartyAcceptCommand.java index 7febacd11..3b9d07f62 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/PartyAcceptCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/PartyAcceptCommand.java @@ -3,7 +3,6 @@ package com.gmail.nossr50.commands.party; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; diff --git a/src/main/java/com/gmail/nossr50/commands/party/PartyChangeOwnerCommand.java b/src/main/java/com/gmail/nossr50/commands/party/PartyChangeOwnerCommand.java index 28d5079f7..29b3d64d1 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/PartyChangeOwnerCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/PartyChangeOwnerCommand.java @@ -3,7 +3,6 @@ package com.gmail.nossr50.commands.party; import com.gmail.nossr50.datatypes.party.Party; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.commands.CommandUtils; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.OfflinePlayer; diff --git a/src/main/java/com/gmail/nossr50/commands/party/PartyCreateCommand.java b/src/main/java/com/gmail/nossr50/commands/party/PartyCreateCommand.java index 80d4d23ee..75a7c785c 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/PartyCreateCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/PartyCreateCommand.java @@ -3,7 +3,6 @@ package com.gmail.nossr50.commands.party; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; diff --git a/src/main/java/com/gmail/nossr50/commands/party/PartyDisbandCommand.java b/src/main/java/com/gmail/nossr50/commands/party/PartyDisbandCommand.java index 3b0ed16bc..ac16d357b 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/PartyDisbandCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/PartyDisbandCommand.java @@ -5,7 +5,6 @@ import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; diff --git a/src/main/java/com/gmail/nossr50/commands/party/PartyInfoCommand.java b/src/main/java/com/gmail/nossr50/commands/party/PartyInfoCommand.java index c17707926..3e34078e8 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/PartyInfoCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/PartyInfoCommand.java @@ -6,7 +6,6 @@ import com.gmail.nossr50.datatypes.party.ShareMode; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.ChatColor; import org.bukkit.command.Command; diff --git a/src/main/java/com/gmail/nossr50/commands/party/PartyInviteCommand.java b/src/main/java/com/gmail/nossr50/commands/party/PartyInviteCommand.java index 65abe4e82..91d6b39be 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/PartyInviteCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/PartyInviteCommand.java @@ -4,7 +4,6 @@ import com.gmail.nossr50.datatypes.party.Party; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.commands.CommandUtils; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.command.Command; diff --git a/src/main/java/com/gmail/nossr50/commands/party/PartyJoinCommand.java b/src/main/java/com/gmail/nossr50/commands/party/PartyJoinCommand.java index be170cd4e..a5474d355 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/PartyJoinCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/PartyJoinCommand.java @@ -4,7 +4,6 @@ import com.gmail.nossr50.datatypes.party.Party; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.commands.CommandUtils; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.command.Command; diff --git a/src/main/java/com/gmail/nossr50/commands/party/PartyKickCommand.java b/src/main/java/com/gmail/nossr50/commands/party/PartyKickCommand.java index b9218b980..9ee2de089 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/PartyKickCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/PartyKickCommand.java @@ -4,7 +4,6 @@ import com.gmail.nossr50.datatypes.party.Party; import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.commands.CommandUtils; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.OfflinePlayer; diff --git a/src/main/java/com/gmail/nossr50/commands/party/PartyQuitCommand.java b/src/main/java/com/gmail/nossr50/commands/party/PartyQuitCommand.java index 920f69ac9..880541add 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/PartyQuitCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/PartyQuitCommand.java @@ -5,7 +5,6 @@ import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; diff --git a/src/main/java/com/gmail/nossr50/commands/party/PartyRenameCommand.java b/src/main/java/com/gmail/nossr50/commands/party/PartyRenameCommand.java index c6e55fc4a..7d2954ad0 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/PartyRenameCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/PartyRenameCommand.java @@ -5,7 +5,6 @@ import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.events.party.McMMOPartyChangeEvent.EventReason; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; diff --git a/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceAcceptCommand.java b/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceAcceptCommand.java index 2e8eed2d5..b4bb002ab 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceAcceptCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceAcceptCommand.java @@ -3,7 +3,6 @@ package com.gmail.nossr50.commands.party.alliance; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; diff --git a/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceCommand.java b/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceCommand.java index 5e47a451c..e39e7c3e2 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceCommand.java @@ -5,7 +5,6 @@ import com.gmail.nossr50.datatypes.party.PartyFeature; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.commands.CommandUtils; import com.gmail.nossr50.util.player.UserManager; import com.google.common.collect.ImmutableList; diff --git a/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceDisbandCommand.java b/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceDisbandCommand.java index c8f2609e0..6b5e2dcea 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceDisbandCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceDisbandCommand.java @@ -4,7 +4,6 @@ import com.gmail.nossr50.datatypes.party.Party; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; diff --git a/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceInviteCommand.java b/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceInviteCommand.java index 855a525e8..04d12b649 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceInviteCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/alliance/PartyAllianceInviteCommand.java @@ -4,7 +4,6 @@ import com.gmail.nossr50.datatypes.party.Party; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.commands.CommandUtils; import com.gmail.nossr50.util.player.UserManager; import org.bukkit.command.Command; diff --git a/src/main/java/com/gmail/nossr50/commands/party/teleport/PtpCommand.java b/src/main/java/com/gmail/nossr50/commands/party/teleport/PtpCommand.java index e757688f8..10f0f1d98 100644 --- a/src/main/java/com/gmail/nossr50/commands/party/teleport/PtpCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/party/teleport/PtpCommand.java @@ -7,7 +7,6 @@ import com.gmail.nossr50.datatypes.party.PartyTeleportRecord; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.runnables.items.TeleportationWarmup; import com.gmail.nossr50.util.EventUtils; import com.gmail.nossr50.util.Misc; diff --git a/src/main/java/com/gmail/nossr50/commands/skills/AcrobaticsCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/AcrobaticsCommand.java index bb963168f..64950211c 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/AcrobaticsCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/AcrobaticsCommand.java @@ -5,9 +5,8 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill; import com.gmail.nossr50.listeners.InteractionManager; import com.gmail.nossr50.locale.LocaleLoader; -import com.gmail.nossr50.util.random.RandomChanceSkill; -import com.gmail.nossr50.util.random.RandomChanceUtil; -import com.gmail.nossr50.util.skills.SkillActivationType; +import com.gmail.nossr50.util.Permissions; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.text.TextComponentFactory; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; @@ -30,7 +29,7 @@ public class AcrobaticsCommand extends SkillCommand { protected void dataCalculations(Player player, float skillValue) { // ACROBATICS_DODGE if (canDodge) { - String[] dodgeStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ACROBATICS_DODGE); + String[] dodgeStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.ACROBATICS_DODGE); dodgeChance = dodgeStrings[0]; dodgeChanceLucky = dodgeStrings[1]; } @@ -38,8 +37,8 @@ public class AcrobaticsCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { - canDodge = canUseSubskill(player, SubSkillType.ACROBATICS_DODGE); - canRoll = canUseSubskill(player, SubSkillType.ACROBATICS_ROLL); + canDodge = Permissions.canUseSubSkill(player, SubSkillType.ACROBATICS_DODGE); + canRoll = Permissions.canUseSubSkill(player, SubSkillType.ACROBATICS_ROLL); } @Override @@ -57,25 +56,7 @@ public class AcrobaticsCommand extends SkillCommand { if(abstractSubSkill != null) { - double rollChance, graceChance; - - //Chance to roll at half - RandomChanceSkill roll_rcs = new RandomChanceSkill(player, SubSkillType.ACROBATICS_ROLL); - - //Chance to graceful roll - RandomChanceSkill grace_rcs = new RandomChanceSkill(player, SubSkillType.ACROBATICS_ROLL); - grace_rcs.setSkillLevel(grace_rcs.getSkillLevel() * 2); //Double Odds - - //Chance Stat Calculations - rollChance = RandomChanceUtil.getRandomChanceExecutionChance(roll_rcs); - graceChance = RandomChanceUtil.getRandomChanceExecutionChance(grace_rcs); - //damageThreshold = mcMMO.p.getAdvancedConfig().getRollDamageThreshold(); - - String[] rollStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ACROBATICS_ROLL); - - //Format - double rollChanceLucky = rollChance * 1.333D; - double graceChanceLucky = graceChance * 1.333D; + String[] rollStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.ACROBATICS_ROLL); messages.add(getStatMessage(SubSkillType.ACROBATICS_ROLL, rollStrings[0]) + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", rollStrings[1]) : "")); diff --git a/src/main/java/com/gmail/nossr50/commands/skills/AlchemyCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/AlchemyCommand.java index a8d5fcc8b..be34ebe2f 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/AlchemyCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/AlchemyCommand.java @@ -68,8 +68,8 @@ public class AlchemyCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { - canCatalysis = canUseSubskill(player, SubSkillType.ALCHEMY_CATALYSIS); - canConcoctions = canUseSubskill(player, SubSkillType.ALCHEMY_CONCOCTIONS); + canCatalysis = Permissions.canUseSubSkill(player, SubSkillType.ALCHEMY_CATALYSIS); + canConcoctions = Permissions.canUseSubSkill(player, SubSkillType.ALCHEMY_CONCOCTIONS); } @Override diff --git a/src/main/java/com/gmail/nossr50/commands/skills/ArcheryCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/ArcheryCommand.java index 59decaf9f..dd7bf8d4b 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/ArcheryCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/ArcheryCommand.java @@ -4,8 +4,9 @@ import com.gmail.nossr50.datatypes.skills.PrimarySkillType; import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.skills.archery.Archery; +import com.gmail.nossr50.util.Permissions; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.CombatUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.text.TextComponentFactory; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; @@ -32,14 +33,14 @@ public class ArcheryCommand extends SkillCommand { protected void dataCalculations(Player player, float skillValue) { // ARCHERY_ARROW_RETRIEVAL if (canRetrieve) { - String[] retrieveStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ARCHERY_ARROW_RETRIEVAL); + String[] retrieveStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.ARCHERY_ARROW_RETRIEVAL); retrieveChance = retrieveStrings[0]; retrieveChanceLucky = retrieveStrings[1]; } // ARCHERY_DAZE if (canDaze) { - String[] dazeStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ARCHERY_DAZE); + String[] dazeStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.ARCHERY_DAZE); dazeChance = dazeStrings[0]; dazeChanceLucky = dazeStrings[1]; } @@ -52,9 +53,9 @@ public class ArcheryCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { - canSkillShot = canUseSubskill(player, SubSkillType.ARCHERY_SKILL_SHOT); - canDaze = canUseSubskill(player, SubSkillType.ARCHERY_DAZE); - canRetrieve = canUseSubskill(player, SubSkillType.ARCHERY_ARROW_RETRIEVAL); + canSkillShot = Permissions.canUseSubSkill(player, SubSkillType.ARCHERY_SKILL_SHOT); + canDaze = Permissions.canUseSubSkill(player, SubSkillType.ARCHERY_DAZE); + canRetrieve = Permissions.canUseSubSkill(player, SubSkillType.ARCHERY_ARROW_RETRIEVAL); } @Override @@ -75,7 +76,7 @@ public class ArcheryCommand extends SkillCommand { messages.add(getStatMessage(SubSkillType.ARCHERY_SKILL_SHOT, skillShotBonus)); } - if(canUseSubskill(player, SubSkillType.ARCHERY_ARCHERY_LIMIT_BREAK)) { + if(Permissions.canUseSubSkill(player, SubSkillType.ARCHERY_ARCHERY_LIMIT_BREAK)) { messages.add(getStatMessage(SubSkillType.ARCHERY_ARCHERY_LIMIT_BREAK, String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.ARCHERY_ARCHERY_LIMIT_BREAK, 1000)))); } diff --git a/src/main/java/com/gmail/nossr50/commands/skills/AxesCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/AxesCommand.java index d6b5c029a..bcfb38ed0 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/AxesCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/AxesCommand.java @@ -6,9 +6,9 @@ import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.skills.axes.Axes; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.UserManager; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.CombatUtils; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.text.TextComponentFactory; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; @@ -48,7 +48,7 @@ public class AxesCommand extends SkillCommand { // CRITICAL HIT if (canCritical) { - String[] criticalHitStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.AXES_CRITICAL_STRIKES); + String[] criticalHitStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.AXES_CRITICAL_STRIKES); critChance = criticalHitStrings[0]; critChanceLucky = criticalHitStrings[1]; } @@ -64,10 +64,10 @@ public class AxesCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { canSkullSplitter = Permissions.skullSplitter(player) && RankUtils.hasUnlockedSubskill(player, SubSkillType.AXES_SKULL_SPLITTER); - canCritical = canUseSubskill(player, SubSkillType.AXES_CRITICAL_STRIKES); - canAxeMastery = canUseSubskill(player, SubSkillType.AXES_AXE_MASTERY); - canImpact = canUseSubskill(player, SubSkillType.AXES_ARMOR_IMPACT); - canGreaterImpact = canUseSubskill(player, SubSkillType.AXES_GREATER_IMPACT); + canCritical = Permissions.canUseSubSkill(player, SubSkillType.AXES_CRITICAL_STRIKES); + canAxeMastery = Permissions.canUseSubSkill(player, SubSkillType.AXES_AXE_MASTERY); + canImpact = Permissions.canUseSubSkill(player, SubSkillType.AXES_ARMOR_IMPACT); + canGreaterImpact = Permissions.canUseSubSkill(player, SubSkillType.AXES_GREATER_IMPACT); } @Override @@ -96,7 +96,7 @@ public class AxesCommand extends SkillCommand { + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", skullSplitterLengthEndurance) : "")); } - if(canUseSubskill(player, SubSkillType.AXES_AXES_LIMIT_BREAK)) { + if(Permissions.canUseSubSkill(player, SubSkillType.AXES_AXES_LIMIT_BREAK)) { messages.add(getStatMessage(SubSkillType.AXES_AXES_LIMIT_BREAK, String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.AXES_AXES_LIMIT_BREAK, 1000)))); } @@ -106,7 +106,7 @@ public class AxesCommand extends SkillCommand { @Override protected List getTextComponents(Player player) { - List textComponents = new ArrayList<>(); + final List textComponents = new ArrayList<>(); TextComponentFactory.getSubSkillTextComponents(player, textComponents, PrimarySkillType.AXES); diff --git a/src/main/java/com/gmail/nossr50/commands/skills/CrossbowsCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/CrossbowsCommand.java new file mode 100644 index 000000000..bbf29caaf --- /dev/null +++ b/src/main/java/com/gmail/nossr50/commands/skills/CrossbowsCommand.java @@ -0,0 +1,78 @@ +package com.gmail.nossr50.commands.skills; + +import com.gmail.nossr50.datatypes.player.McMMOPlayer; +import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +import com.gmail.nossr50.util.Permissions; +import com.gmail.nossr50.util.player.UserManager; +import com.gmail.nossr50.util.skills.CombatUtils; +import com.gmail.nossr50.util.skills.RankUtils; +import com.gmail.nossr50.util.text.TextComponentFactory; +import net.kyori.adventure.text.Component; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +import static com.gmail.nossr50.datatypes.skills.SubSkillType.*; + +public class CrossbowsCommand extends SkillCommand { + private boolean canTrickShot; + private boolean canPoweredShot; + + public CrossbowsCommand() { + super(PrimarySkillType.CROSSBOWS); + } + + @Override + protected void dataCalculations(Player player, float skillValue) { + // TODO: Implement data calculations + } + + @Override + protected void permissionsCheck(Player player) { + canTrickShot = RankUtils.hasUnlockedSubskill(player, CROSSBOWS_TRICK_SHOT) + && Permissions.trickShot(player); + + canPoweredShot = RankUtils.hasUnlockedSubskill(player, CROSSBOWS_POWERED_SHOT) + && Permissions.poweredShot(player); + } + + @Override + protected List statsDisplay(Player player, float skillValue, boolean hasEndurance, boolean isLucky) { + List messages = new ArrayList<>(); + + McMMOPlayer mmoPlayer = UserManager.getPlayer(player); + if (mmoPlayer == null) { + return messages; + } + + if (canPoweredShot) { + messages.add(getStatMessage(CROSSBOWS_POWERED_SHOT, + percent.format(mmoPlayer.getCrossbowsManager().getDamageBonusPercent(player)))); + } + + if (canTrickShot) { + messages.add(getStatMessage(CROSSBOWS_TRICK_SHOT, + String.valueOf(mmoPlayer.getCrossbowsManager().getTrickShotMaxBounceCount()))); + } + + if(Permissions.canUseSubSkill(player, CROSSBOWS_CROSSBOWS_LIMIT_BREAK)) { + messages.add(getStatMessage(CROSSBOWS_CROSSBOWS_LIMIT_BREAK, + String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, CROSSBOWS_CROSSBOWS_LIMIT_BREAK, 1000)))); + } + + messages.add(ChatColor.GRAY + "The Crossbows skill is a work in progress and is still being developed, feedback would be appreciated in the mcMMO discord server."); + + return messages; + } + + @Override + protected List getTextComponents(Player player) { + List textComponents = new ArrayList<>(); + + TextComponentFactory.getSubSkillTextComponents(player, textComponents, PrimarySkillType.CROSSBOWS); + + return textComponents; + } +} diff --git a/src/main/java/com/gmail/nossr50/commands/skills/ExcavationCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/ExcavationCommand.java index de7341e93..952766b15 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/ExcavationCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/ExcavationCommand.java @@ -38,7 +38,7 @@ public class ExcavationCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { canGigaDrill = Permissions.gigaDrillBreaker(player) && RankUtils.hasUnlockedSubskill(player, SubSkillType.EXCAVATION_GIGA_DRILL_BREAKER); - canTreasureHunt = canUseSubskill(player, SubSkillType.EXCAVATION_ARCHAEOLOGY); + canTreasureHunt = Permissions.canUseSubSkill(player, SubSkillType.EXCAVATION_ARCHAEOLOGY); } @Override @@ -54,7 +54,7 @@ public class ExcavationCommand extends SkillCommand { //messages.add(LocaleLoader.getString("Excavation.Effect.Length", gigaDrillBreakerLength) + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", gigaDrillBreakerLengthEndurance) : "")); } - if(canUseSubskill(player, SubSkillType.EXCAVATION_ARCHAEOLOGY)) { + if(Permissions.canUseSubSkill(player, SubSkillType.EXCAVATION_ARCHAEOLOGY)) { messages.add(getStatMessage(false, false, SubSkillType.EXCAVATION_ARCHAEOLOGY, percent.format(excavationManager.getArchaelogyExperienceOrbChance() / 100.0D))); messages.add(getStatMessage(true, false, SubSkillType.EXCAVATION_ARCHAEOLOGY, diff --git a/src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java index f22f8399e..6b188d2d7 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/FishingCommand.java @@ -7,8 +7,10 @@ import com.gmail.nossr50.datatypes.treasure.Rarity; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.skills.fishing.FishingManager; +import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.UserManager; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.Probability; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; import com.gmail.nossr50.util.text.StringUtils; import com.gmail.nossr50.util.text.TextComponentFactory; @@ -79,7 +81,8 @@ public class FishingCommand extends SkillCommand { // FISHING_SHAKE if (canShake) { - String[] shakeStrings = RandomChanceUtil.calculateAbilityDisplayValuesStatic(player, PrimarySkillType.FISHING, fishingManager.getShakeChance()); + Probability shakeProbability = Probability.ofPercent(fishingManager.getShakeChance()); + String[] shakeStrings = ProbabilityUtil.getRNGDisplayValues(shakeProbability); shakeChance = shakeStrings[0]; shakeChanceLucky = shakeStrings[1]; } @@ -98,12 +101,12 @@ public class FishingCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { - canTreasureHunt = canUseSubskill(player, SubSkillType.FISHING_TREASURE_HUNTER); - canMagicHunt = canUseSubskill(player, SubSkillType.FISHING_MAGIC_HUNTER) && canUseSubskill(player, SubSkillType.FISHING_TREASURE_HUNTER); - canShake = canUseSubskill(player, SubSkillType.FISHING_SHAKE); - canFishermansDiet = canUseSubskill(player, SubSkillType.FISHING_FISHERMANS_DIET); - canMasterAngler = mcMMO.getCompatibilityManager().getMasterAnglerCompatibilityLayer() != null && canUseSubskill(player, SubSkillType.FISHING_MASTER_ANGLER); - canIceFish = canUseSubskill(player, SubSkillType.FISHING_ICE_FISHING); + canTreasureHunt = Permissions.canUseSubSkill(player, SubSkillType.FISHING_TREASURE_HUNTER); + canMagicHunt = Permissions.canUseSubSkill(player, SubSkillType.FISHING_MAGIC_HUNTER) && Permissions.canUseSubSkill(player, SubSkillType.FISHING_TREASURE_HUNTER); + canShake = Permissions.canUseSubSkill(player, SubSkillType.FISHING_SHAKE); + canFishermansDiet = Permissions.canUseSubSkill(player, SubSkillType.FISHING_FISHERMANS_DIET); + canMasterAngler = mcMMO.getCompatibilityManager().getMasterAnglerCompatibilityLayer() != null && Permissions.canUseSubSkill(player, SubSkillType.FISHING_MASTER_ANGLER); + canIceFish = Permissions.canUseSubSkill(player, SubSkillType.FISHING_ICE_FISHING); } @Override diff --git a/src/main/java/com/gmail/nossr50/commands/skills/HerbalismCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/HerbalismCommand.java index eba4520a7..8a5ce8d33 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/HerbalismCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/HerbalismCommand.java @@ -5,8 +5,8 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.util.Permissions; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.text.TextComponentFactory; import net.kyori.adventure.text.Component; import org.bukkit.Material; @@ -24,6 +24,8 @@ public class HerbalismCommand extends SkillCommand { private int farmersDietRank; private String doubleDropChance; private String doubleDropChanceLucky; + private String tripleDropChance; + private String tripleDropChanceLucky; private String hylianLuckChance; private String hylianLuckChanceLucky; private String shroomThumbChance; @@ -35,6 +37,7 @@ public class HerbalismCommand extends SkillCommand { private boolean canGreenThumbBlocks; private boolean canFarmersDiet; private boolean canDoubleDrop; + private boolean canTripleDrop; private boolean canShroomThumb; public HerbalismCommand() { @@ -46,10 +49,16 @@ public class HerbalismCommand extends SkillCommand { // DOUBLE DROPS if (canDoubleDrop) { - String[] doubleDropStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.HERBALISM_DOUBLE_DROPS); + String[] doubleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.HERBALISM_DOUBLE_DROPS); doubleDropChance = doubleDropStrings[0]; doubleDropChanceLucky = doubleDropStrings[1]; } + + if (canTripleDrop) { + String[] tripleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.HERBALISM_VERDANT_BOUNTY); + tripleDropChance = tripleDropStrings[0]; + tripleDropChanceLucky = tripleDropStrings[1]; + } // FARMERS DIET if (canFarmersDiet) { @@ -67,21 +76,21 @@ public class HerbalismCommand extends SkillCommand { if (canGreenThumbBlocks || canGreenThumbPlants) { greenThumbStage = RankUtils.getRank(player, SubSkillType.HERBALISM_GREEN_THUMB); - String[] greenThumbStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.HERBALISM_GREEN_THUMB); + String[] greenThumbStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.HERBALISM_GREEN_THUMB); greenThumbChance = greenThumbStrings[0]; greenThumbChanceLucky = greenThumbStrings[1]; } // HYLIAN LUCK if (hasHylianLuck) { - String[] hylianLuckStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.HERBALISM_HYLIAN_LUCK); + String[] hylianLuckStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.HERBALISM_HYLIAN_LUCK); hylianLuckChance = hylianLuckStrings[0]; hylianLuckChanceLucky = hylianLuckStrings[1]; } // SHROOM THUMB if (canShroomThumb) { - String[] shroomThumbStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.HERBALISM_SHROOM_THUMB); + String[] shroomThumbStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.HERBALISM_SHROOM_THUMB); shroomThumbChance = shroomThumbStrings[0]; shroomThumbChanceLucky = shroomThumbStrings[1]; } @@ -89,13 +98,14 @@ public class HerbalismCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { - hasHylianLuck = canUseSubskill(player, SubSkillType.HERBALISM_HYLIAN_LUCK); + hasHylianLuck = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_HYLIAN_LUCK); canGreenTerra = Permissions.greenTerra(player); canGreenThumbPlants = RankUtils.hasUnlockedSubskill(player, SubSkillType.HERBALISM_GREEN_THUMB) && (Permissions.greenThumbPlant(player, Material.WHEAT) || Permissions.greenThumbPlant(player, Material.CARROT) || Permissions.greenThumbPlant(player, Material.POTATO) || Permissions.greenThumbPlant(player, Material.BEETROOTS) || Permissions.greenThumbPlant(player, Material.NETHER_WART) || Permissions.greenThumbPlant(player, Material.COCOA)); canGreenThumbBlocks = RankUtils.hasUnlockedSubskill(player, SubSkillType.HERBALISM_GREEN_THUMB) && (Permissions.greenThumbBlock(player, Material.DIRT) || Permissions.greenThumbBlock(player, Material.COBBLESTONE) || Permissions.greenThumbBlock(player, Material.COBBLESTONE_WALL) || Permissions.greenThumbBlock(player, Material.STONE_BRICKS)); - canFarmersDiet = canUseSubskill(player, SubSkillType.HERBALISM_FARMERS_DIET); - canDoubleDrop = canUseSubskill(player, SubSkillType.HERBALISM_DOUBLE_DROPS) && !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill); - canShroomThumb = canUseSubskill(player, SubSkillType.HERBALISM_SHROOM_THUMB); + canFarmersDiet = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_FARMERS_DIET); + canDoubleDrop = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_DOUBLE_DROPS) && !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill); + canTripleDrop = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_VERDANT_BOUNTY) && !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill); + canShroomThumb = Permissions.canUseSubSkill(player, SubSkillType.HERBALISM_SHROOM_THUMB); } @Override @@ -106,11 +116,16 @@ public class HerbalismCommand extends SkillCommand { messages.add(getStatMessage(SubSkillType.HERBALISM_DOUBLE_DROPS, doubleDropChance) + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : "")); } - + + if (canTripleDrop) { + messages.add(getStatMessage(SubSkillType.HERBALISM_VERDANT_BOUNTY, tripleDropChance) + + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", tripleDropChanceLucky) : "")); + } + if (canFarmersDiet) { messages.add(getStatMessage(false, true, SubSkillType.HERBALISM_FARMERS_DIET, String.valueOf(farmersDietRank))); } - + if (canGreenTerra) { messages.add(getStatMessage(SubSkillType.HERBALISM_GREEN_TERRA, greenTerraLength) + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", greenTerraLengthEndurance) : "")); diff --git a/src/main/java/com/gmail/nossr50/commands/skills/MiningCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/MiningCommand.java index 5e7a14a7a..b756dd282 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/MiningCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/MiningCommand.java @@ -6,8 +6,8 @@ import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.skills.mining.MiningManager; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.UserManager; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.text.TextComponentFactory; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; @@ -18,6 +18,8 @@ import java.util.List; public class MiningCommand extends SkillCommand { private String doubleDropChance; private String doubleDropChanceLucky; + private String tripleDropChance; + private String tripleDropChanceLucky; private String superBreakerLength; private String superBreakerLengthEndurance; @@ -30,6 +32,7 @@ public class MiningCommand extends SkillCommand { private boolean canSuperBreaker; private boolean canDoubleDrop; + private boolean canTripleDrop; private boolean canBlast; private boolean canBiggerBombs; private boolean canDemoExpert; @@ -51,10 +54,17 @@ public class MiningCommand extends SkillCommand { blastDamageDecrease = percent.format(miningManager.getBlastDamageModifier() / 100.0D); blastRadiusIncrease = miningManager.getBlastRadiusModifier(); } + + // Mastery TRIPLE DROPS + if (canTripleDrop) { + String[] masteryTripleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.MINING_MOTHER_LODE); + tripleDropChance = masteryTripleDropStrings[0]; + tripleDropChanceLucky = masteryTripleDropStrings[1]; + } // DOUBLE DROPS if (canDoubleDrop) { - String[] doubleDropStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.MINING_DOUBLE_DROPS); + String[] doubleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.MINING_DOUBLE_DROPS); doubleDropChance = doubleDropStrings[0]; doubleDropChanceLucky = doubleDropStrings[1]; } @@ -72,7 +82,8 @@ public class MiningCommand extends SkillCommand { canBiggerBombs = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_BIGGER_BOMBS) && Permissions.biggerBombs(player); canBlast = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_BLAST_MINING) && Permissions.remoteDetonation(player); canDemoExpert = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_DEMOLITIONS_EXPERTISE) && Permissions.demolitionsExpertise(player); - canDoubleDrop = canUseSubskill(player, SubSkillType.MINING_DOUBLE_DROPS); + canDoubleDrop = Permissions.canUseSubSkill(player, SubSkillType.MINING_DOUBLE_DROPS); + canTripleDrop = Permissions.canUseSubSkill(player, SubSkillType.MINING_MOTHER_LODE); canSuperBreaker = RankUtils.hasUnlockedSubskill(player, SubSkillType.MINING_SUPER_BREAKER) && Permissions.superBreaker(player); } @@ -94,13 +105,18 @@ public class MiningCommand extends SkillCommand { messages.add(getStatMessage(SubSkillType.MINING_DEMOLITIONS_EXPERTISE, blastDamageDecrease)); //messages.add(LocaleLoader.getString("Mining.Effect.Decrease", blastDamageDecrease)); } - + if (canDoubleDrop) { messages.add(getStatMessage(SubSkillType.MINING_DOUBLE_DROPS, doubleDropChance) + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : "")); //messages.add(LocaleLoader.getString("Mining.Effect.DropChance", doubleDropChance) + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : "")); } + if(canTripleDrop) { + messages.add(getStatMessage(SubSkillType.MINING_MOTHER_LODE, tripleDropChance) + + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", tripleDropChanceLucky) : "")); + } + if (canSuperBreaker) { messages.add(getStatMessage(SubSkillType.MINING_SUPER_BREAKER, superBreakerLength) + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", superBreakerLengthEndurance) : "")); diff --git a/src/main/java/com/gmail/nossr50/commands/skills/MmoInfoCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/MmoInfoCommand.java index 0f0a4a710..3a085a5cf 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/MmoInfoCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/MmoInfoCommand.java @@ -1,7 +1,6 @@ package com.gmail.nossr50.commands.skills; import com.gmail.nossr50.datatypes.skills.SubSkillType; -import com.gmail.nossr50.listeners.InteractionManager; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.util.Permissions; @@ -29,14 +28,11 @@ public class MmoInfoCommand implements TabExecutor { */ if(commandSender instanceof Player player) { - if(args.length < 1) + if(args == null || args.length < 1 || args[0] == null || args[0].isEmpty()) return false; if(Permissions.mmoinfo(player)) { - if(args == null || args[0] == null) - return false; - if(args[0].equalsIgnoreCase( "???")) { player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.Header")); @@ -44,14 +40,15 @@ public class MmoInfoCommand implements TabExecutor { player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.DetailsHeader")); player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.Mystery")); return true; - } else if(InteractionManager.getAbstractByName(args[0]) != null || mcMMO.p.getSkillTools().EXACT_SUBSKILL_NAMES.contains(args[0])) - { - displayInfo(player, args[0]); - return true; } - //Not a real skill - player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.NoMatch")); + final SubSkillType subSkillType = matchSubSkill(args[0]); + if (subSkillType != null) { + displayInfo(player, subSkillType); + } else { + //Not a real skill + player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.NoMatch")); + } return true; } } @@ -59,6 +56,16 @@ public class MmoInfoCommand implements TabExecutor { return false; } + public SubSkillType matchSubSkill(String name) { + for(SubSkillType subSkillType : SubSkillType.values()) + { + if(subSkillType.getNiceNameNoSpaces(subSkillType).equalsIgnoreCase(name) + || subSkillType.name().equalsIgnoreCase(name)) + return subSkillType; + } + return null; + } + @Override public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] args) { if (args.length == 1) { @@ -67,20 +74,13 @@ public class MmoInfoCommand implements TabExecutor { return ImmutableList.of(); } - private void displayInfo(Player player, String subSkillName) + private void displayInfo(Player player, SubSkillType subSkillType) { player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.Header")); - player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.SubSkillHeader", subSkillName)); + player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.SubSkillHeader", subSkillType.getLocaleName())); player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.DetailsHeader")); - player.sendMessage(LocaleLoader.getString("Commands.MmoInfo.OldSkill")); - - for(SubSkillType subSkillType : SubSkillType.values()) - { - if(subSkillType.getNiceNameNoSpaces(subSkillType).equalsIgnoreCase(subSkillName)) - subSkillName = subSkillType.getWikiName(subSkillType.toString()); - } //Send Player Wiki Link - TextComponentFactory.sendPlayerSubSkillWikiLink(player, subSkillName); + TextComponentFactory.sendPlayerSubSkillWikiLink(player, subSkillType.getLocaleName(), subSkillType); } } diff --git a/src/main/java/com/gmail/nossr50/commands/skills/PowerLevelCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/PowerLevelCommand.java new file mode 100644 index 000000000..639275b55 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/commands/skills/PowerLevelCommand.java @@ -0,0 +1,40 @@ +package com.gmail.nossr50.commands.skills; + +import co.aikar.commands.BaseCommand; +import co.aikar.commands.BukkitCommandIssuer; +import co.aikar.commands.annotation.CommandAlias; +import co.aikar.commands.annotation.CommandPermission; +import co.aikar.commands.annotation.Conditions; +import co.aikar.commands.annotation.Default; +import com.gmail.nossr50.commands.CommandManager; +import com.gmail.nossr50.datatypes.player.McMMOPlayer; +import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.util.player.UserManager; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +@CommandPermission("mcmmo.commands.mmopower") +@CommandAlias("mmopower|mmopowerlevel|powerlevel") +public class PowerLevelCommand extends BaseCommand { + private final @NotNull mcMMO pluginRef; + + public PowerLevelCommand(@NotNull mcMMO pluginRef) { + this.pluginRef = pluginRef; + } + + @Default + @Conditions(CommandManager.POWER_LEVEL_CONDITION) + public void processCommand(String[] args) { + BukkitCommandIssuer bukkitCommandIssuer = (BukkitCommandIssuer) getCurrentCommandIssuer(); + Player player = bukkitCommandIssuer.getPlayer(); + McMMOPlayer mmoPlayer = UserManager.getPlayer(player); //Should never be null at this point because its caught in an ACF validation + if (mmoPlayer == null) { + return; + } + + int powerLevel = mmoPlayer.getPowerLevel(); + + mmoPlayer.getPlayer().sendMessage(ChatColor.DARK_AQUA + "Your " + ChatColor.GOLD + "[mcMMO]" + ChatColor.DARK_AQUA + " power level is: " + ChatColor.GREEN + powerLevel); + } +} diff --git a/src/main/java/com/gmail/nossr50/commands/skills/RepairCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/RepairCommand.java index 1941357d0..32927ddcf 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/RepairCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/RepairCommand.java @@ -11,8 +11,8 @@ import com.gmail.nossr50.skills.repair.RepairManager; import com.gmail.nossr50.skills.repair.repairables.Repairable; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.UserManager; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.text.TextComponentFactory; import net.kyori.adventure.text.Component; import org.bukkit.Material; @@ -68,7 +68,7 @@ public class RepairCommand extends SkillCommand { // SUPER REPAIR if (canSuperRepair) { - String[] superRepairStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.REPAIR_SUPER_REPAIR); + String[] superRepairStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.REPAIR_SUPER_REPAIR); superRepairChance = superRepairStrings[0]; superRepairChanceLucky = superRepairStrings[1]; } @@ -76,9 +76,9 @@ public class RepairCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { - canSuperRepair = canUseSubskill(player, SubSkillType.REPAIR_SUPER_REPAIR); - canMasterRepair = canUseSubskill(player, SubSkillType.REPAIR_REPAIR_MASTERY); - canArcaneForge = canUseSubskill(player, SubSkillType.REPAIR_ARCANE_FORGING); + canSuperRepair = Permissions.canUseSubSkill(player, SubSkillType.REPAIR_SUPER_REPAIR); + canMasterRepair = Permissions.canUseSubSkill(player, SubSkillType.REPAIR_REPAIR_MASTERY); + canArcaneForge = Permissions.canUseSubSkill(player, SubSkillType.REPAIR_ARCANE_FORGING); canRepairDiamond = Permissions.repairMaterialType(player, MaterialType.DIAMOND); canRepairGold = Permissions.repairMaterialType(player, MaterialType.GOLD); canRepairIron = Permissions.repairMaterialType(player, MaterialType.IRON); diff --git a/src/main/java/com/gmail/nossr50/commands/skills/SalvageCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/SalvageCommand.java index 770299951..93a748023 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/SalvageCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/SalvageCommand.java @@ -5,6 +5,7 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.skills.salvage.Salvage; import com.gmail.nossr50.skills.salvage.SalvageManager; +import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.UserManager; import com.gmail.nossr50.util.skills.RankUtils; import com.gmail.nossr50.util.text.TextComponentFactory; @@ -30,8 +31,8 @@ public class SalvageCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { - canScrapCollector = canUseSubskill(player, SubSkillType.SALVAGE_SCRAP_COLLECTOR); - canArcaneSalvage = canUseSubskill(player, SubSkillType.SALVAGE_ARCANE_SALVAGE); + canScrapCollector = Permissions.canUseSubSkill(player, SubSkillType.SALVAGE_SCRAP_COLLECTOR); + canArcaneSalvage = Permissions.canUseSubSkill(player, SubSkillType.SALVAGE_ARCANE_SALVAGE); } @Override diff --git a/src/main/java/com/gmail/nossr50/commands/skills/SkillCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/SkillCommand.java index df3f8a8a4..e8ab95e02 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/SkillCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/SkillCommand.java @@ -5,16 +5,12 @@ import com.gmail.nossr50.datatypes.skills.PrimarySkillType; import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.skills.child.FamilyTree; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.commands.CommandUtils; import com.gmail.nossr50.util.player.NotificationManager; import com.gmail.nossr50.util.player.UserManager; -import com.gmail.nossr50.util.random.RandomChanceUtil; import com.gmail.nossr50.util.scoreboards.ScoreboardManager; import com.gmail.nossr50.util.skills.PerksUtils; -import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.skills.SkillTools; import com.gmail.nossr50.util.text.StringUtils; import com.gmail.nossr50.util.text.TextComponentFactory; @@ -32,11 +28,9 @@ import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.Set; public abstract class SkillCommand implements TabExecutor { protected PrimarySkillType skill; - private final String skillName; protected DecimalFormat percent = new DecimalFormat("##0.00%"); protected DecimalFormat decimal = new DecimalFormat("##0.00"); @@ -45,7 +39,6 @@ public abstract class SkillCommand implements TabExecutor { public SkillCommand(PrimarySkillType skill) { this.skill = skill; - skillName = mcMMO.p.getSkillTools().getLocalizedSkillName(skill); skillGuideCommand = new SkillGuideCommand(skill); } @@ -81,7 +74,8 @@ public abstract class SkillCommand implements TabExecutor { permissionsCheck(player); dataCalculations(player, skillValue); - sendSkillCommandHeader(player, mcMMOPlayer, (int) skillValue); + sendSkillCommandHeader(mcMMO.p.getSkillTools().getLocalizedSkillName(skill), + player, mcMMOPlayer, (int) skillValue); //Make JSON text components List subskillTextComponents = getTextComponents(player); @@ -144,15 +138,14 @@ public abstract class SkillCommand implements TabExecutor { } } - player.sendMessage(LocaleLoader.getString("Guides.Available", skillName, skillName.toLowerCase(Locale.ENGLISH))); + final String skillName = mcMMO.p.getSkillTools().getLocalizedSkillName(skill); + player.sendMessage(LocaleLoader.getString("Guides.Available", + skillName, + skillName.toLowerCase(Locale.ENGLISH))); } - private void sendSkillCommandHeader(Player player, McMMOPlayer mcMMOPlayer, int skillValue) { - ChatColor hd1 = ChatColor.DARK_AQUA; - ChatColor c1 = ChatColor.GOLD; - ChatColor c2 = ChatColor.RED; - - + private void sendSkillCommandHeader(String skillName, Player player, McMMOPlayer mcMMOPlayer, int skillValue) { + // send header player.sendMessage(LocaleLoader.getString("Skills.Overhaul.Header", skillName)); if(!SkillTools.isChildSkill(skill)) @@ -173,7 +166,7 @@ public abstract class SkillCommand implements TabExecutor { */ - Set parents = FamilyTree.getParents(skill); + var parents = mcMMO.p.getSkillTools().getChildSkillParents(skill); //TODO: Add JSON here /*player.sendMessage(parent.getName() + " - " + LocaleLoader.getString("Effects.Level.Overhaul", mcMMOPlayer.getSkillLevel(parent), mcMMOPlayer.getSkillXpLevel(parent), mcMMOPlayer.getXpToLevel(parent)))*/ @@ -232,9 +225,9 @@ public abstract class SkillCommand implements TabExecutor { return Math.min((int) skillValue, maxLevel) / rankChangeLevel; } - protected String[] getAbilityDisplayValues(SkillActivationType skillActivationType, Player player, SubSkillType subSkill) { - return RandomChanceUtil.calculateAbilityDisplayValues(skillActivationType, player, subSkill); - } +// protected String[] getAbilityDisplayValues(SkillActivationType skillActivationType, Player player, SubSkillType subSkill) { +// return RandomChanceUtil.calculateAbilityDisplayValues(skillActivationType, player, subSkill); +// } protected String[] calculateLengthDisplayValues(Player player, float skillValue) { int maxLength = mcMMO.p.getSkillTools().getSuperAbilityMaxLength(mcMMO.p.getSkillTools().getSuperAbility(skill)); @@ -297,14 +290,4 @@ public abstract class SkillCommand implements TabExecutor { protected abstract List getTextComponents(Player player); - /** - * Checks if a player can use a skill - * @param player target player - * @param subSkillType target subskill - * @return true if the player has permission and has the skill unlocked - */ - protected boolean canUseSubskill(Player player, SubSkillType subSkillType) - { - return Permissions.isSubSkillEnabled(player, subSkillType) && RankUtils.hasUnlockedSubskill(player, subSkillType); - } } diff --git a/src/main/java/com/gmail/nossr50/commands/skills/SmeltingCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/SmeltingCommand.java index 55544acc2..297662e10 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/SmeltingCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/SmeltingCommand.java @@ -5,8 +5,8 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.UserManager; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.text.TextComponentFactory; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; @@ -39,14 +39,14 @@ public class SmeltingCommand extends SkillCommand { // FLUX MINING /*if (canFluxMine) { - String[] fluxMiningStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.SMELTING_FLUX_MINING); + String[] fluxMiningStrings = getRNGDisplayValues(player, SubSkillType.SMELTING_FLUX_MINING); str_fluxMiningChance = fluxMiningStrings[0]; str_fluxMiningChanceLucky = fluxMiningStrings[1]; }*/ // SECOND SMELT if (canSecondSmelt) { - String[] secondSmeltStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.SMELTING_SECOND_SMELT); + String[] secondSmeltStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.SMELTING_SECOND_SMELT); str_secondSmeltChance = secondSmeltStrings[0]; str_secondSmeltChanceLucky = secondSmeltStrings[1]; } @@ -54,8 +54,8 @@ public class SmeltingCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { - canFuelEfficiency = canUseSubskill(player, SubSkillType.SMELTING_FUEL_EFFICIENCY); - canSecondSmelt = canUseSubskill(player, SubSkillType.SMELTING_SECOND_SMELT); + canFuelEfficiency = Permissions.canUseSubSkill(player, SubSkillType.SMELTING_FUEL_EFFICIENCY); + canSecondSmelt = Permissions.canUseSubSkill(player, SubSkillType.SMELTING_SECOND_SMELT); //canFluxMine = canUseSubskill(player, SubSkillType.SMELTING_FLUX_MINING); canUnderstandTheArt = Permissions.vanillaXpBoost(player, skill) && RankUtils.hasUnlockedSubskill(player, SubSkillType.SMELTING_UNDERSTANDING_THE_ART); } diff --git a/src/main/java/com/gmail/nossr50/commands/skills/SwordsCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/SwordsCommand.java index 954c39024..03d15d4ce 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/SwordsCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/SwordsCommand.java @@ -6,9 +6,10 @@ import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.UserManager; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.CombatUtils; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; +import com.gmail.nossr50.util.skills.SkillUtils; import com.gmail.nossr50.util.text.TextComponentFactory; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; @@ -38,7 +39,7 @@ public class SwordsCommand extends SkillCommand { protected void dataCalculations(Player player, float skillValue) { // SWORDS_COUNTER_ATTACK if (canCounter) { - String[] counterStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.SWORDS_COUNTER_ATTACK); + String[] counterStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.SWORDS_COUNTER_ATTACK); counterChance = counterStrings[0]; counterChanceLucky = counterStrings[1]; } @@ -69,8 +70,8 @@ public class SwordsCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { - canRupture = canUseSubskill(player, SubSkillType.SWORDS_RUPTURE); - canCounter = canUseSubskill(player, SubSkillType.SWORDS_COUNTER_ATTACK); + canRupture = SkillUtils.canUseSubskill(player, SubSkillType.SWORDS_RUPTURE); + canCounter = SkillUtils.canUseSubskill(player, SubSkillType.SWORDS_COUNTER_ATTACK); canSerratedStrike = RankUtils.hasUnlockedSubskill(player, SubSkillType.SWORDS_SERRATED_STRIKES) && Permissions.serratedStrikes(player); } @@ -101,13 +102,13 @@ public class SwordsCommand extends SkillCommand { + (hasEndurance ? LocaleLoader.getString("Perks.ActivationTime.Bonus", serratedStrikesLengthEndurance) : "")); } - if(canUseSubskill(player, SubSkillType.SWORDS_STAB)) + if(SkillUtils.canUseSubskill(player, SubSkillType.SWORDS_STAB)) { messages.add(getStatMessage(SubSkillType.SWORDS_STAB, String.valueOf(UserManager.getPlayer(player).getSwordsManager().getStabDamage()))); } - if(canUseSubskill(player, SubSkillType.SWORDS_SWORDS_LIMIT_BREAK)) { + if(SkillUtils.canUseSubskill(player, SubSkillType.SWORDS_SWORDS_LIMIT_BREAK)) { messages.add(getStatMessage(SubSkillType.SWORDS_SWORDS_LIMIT_BREAK, String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.SWORDS_SWORDS_LIMIT_BREAK, 1000)))); } diff --git a/src/main/java/com/gmail/nossr50/commands/skills/TamingCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/TamingCommand.java index 35f04f5bc..fc6fd2e46 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/TamingCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/TamingCommand.java @@ -5,7 +5,7 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.skills.taming.Taming; import com.gmail.nossr50.util.Permissions; -import com.gmail.nossr50.util.skills.SkillActivationType; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.text.TextComponentFactory; import net.kyori.adventure.text.Component; import org.bukkit.entity.EntityType; @@ -35,7 +35,7 @@ public class TamingCommand extends SkillCommand { @Override protected void dataCalculations(Player player, float skillValue) { if (canGore) { - String[] goreStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.TAMING_GORE); + String[] goreStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.TAMING_GORE); goreChance = goreStrings[0]; goreChanceLucky = goreStrings[1]; } @@ -43,15 +43,15 @@ public class TamingCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { - canBeastLore = canUseSubskill(player, SubSkillType.TAMING_BEAST_LORE); + canBeastLore = Permissions.canUseSubSkill(player, SubSkillType.TAMING_BEAST_LORE); canCallWild = Permissions.callOfTheWild(player, EntityType.HORSE) || Permissions.callOfTheWild(player, EntityType.WOLF) || Permissions.callOfTheWild(player, EntityType.OCELOT); - canEnvironmentallyAware = canUseSubskill(player, SubSkillType.TAMING_ENVIRONMENTALLY_AWARE); - canFastFood = canUseSubskill(player, SubSkillType.TAMING_FAST_FOOD_SERVICE); - canGore = canUseSubskill(player, SubSkillType.TAMING_GORE); - canSharpenedClaws = canUseSubskill(player, SubSkillType.TAMING_SHARPENED_CLAWS); - canShockProof = canUseSubskill(player, SubSkillType.TAMING_SHOCK_PROOF); - canThickFur = canUseSubskill(player, SubSkillType.TAMING_THICK_FUR); - canHolyHound = canUseSubskill(player, SubSkillType.TAMING_HOLY_HOUND); + canEnvironmentallyAware = Permissions.canUseSubSkill(player, SubSkillType.TAMING_ENVIRONMENTALLY_AWARE); + canFastFood = Permissions.canUseSubSkill(player, SubSkillType.TAMING_FAST_FOOD_SERVICE); + canGore = Permissions.canUseSubSkill(player, SubSkillType.TAMING_GORE); + canSharpenedClaws = Permissions.canUseSubSkill(player, SubSkillType.TAMING_SHARPENED_CLAWS); + canShockProof = Permissions.canUseSubSkill(player, SubSkillType.TAMING_SHOCK_PROOF); + canThickFur = Permissions.canUseSubSkill(player, SubSkillType.TAMING_THICK_FUR); + canHolyHound = Permissions.canUseSubSkill(player, SubSkillType.TAMING_HOLY_HOUND); } @Override diff --git a/src/main/java/com/gmail/nossr50/commands/skills/TridentsCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/TridentsCommand.java new file mode 100644 index 000000000..ff4165659 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/commands/skills/TridentsCommand.java @@ -0,0 +1,62 @@ +package com.gmail.nossr50.commands.skills; + +import com.gmail.nossr50.datatypes.player.McMMOPlayer; +import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +import com.gmail.nossr50.util.player.UserManager; +import com.gmail.nossr50.util.skills.CombatUtils; +import com.gmail.nossr50.util.skills.SkillUtils; +import com.gmail.nossr50.util.text.TextComponentFactory; +import net.kyori.adventure.text.Component; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +import static com.gmail.nossr50.datatypes.skills.SubSkillType.TRIDENTS_IMPALE; +import static com.gmail.nossr50.datatypes.skills.SubSkillType.TRIDENTS_TRIDENTS_LIMIT_BREAK; + +public class TridentsCommand extends SkillCommand { + + + public TridentsCommand() { + super(PrimarySkillType.TRIDENTS); + } + + @Override + protected void dataCalculations(Player player, float skillValue) {} + + @Override + protected void permissionsCheck(Player player) {} + + @Override + protected List statsDisplay(Player player, float skillValue, boolean hasEndurance, boolean isLucky) { + List messages = new ArrayList<>(); + McMMOPlayer mmoPlayer = UserManager.getPlayer(player); + if (mmoPlayer == null) { + return messages; + } + + if(SkillUtils.canUseSubskill(player, TRIDENTS_TRIDENTS_LIMIT_BREAK)) { + messages.add(getStatMessage(TRIDENTS_TRIDENTS_LIMIT_BREAK, + String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, TRIDENTS_TRIDENTS_LIMIT_BREAK, 1000)))); + } + + if(SkillUtils.canUseSubskill(player, TRIDENTS_IMPALE)) { + messages.add(getStatMessage(TRIDENTS_IMPALE, + String.valueOf(mmoPlayer.getTridentsManager().impaleDamageBonus()))); + } + + messages.add(ChatColor.GRAY + "The Tridents skill is a work in progress and is still being developed, feedback would be appreciated in the mcMMO discord server."); + return messages; + } + + @Override + protected List getTextComponents(Player player) { + List textComponents = new ArrayList<>(); + + TextComponentFactory.getSubSkillTextComponents(player, textComponents, PrimarySkillType.TRIDENTS); + + return textComponents; + } +} diff --git a/src/main/java/com/gmail/nossr50/commands/skills/UnarmedCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/UnarmedCommand.java index 5f9784be7..554eca6b3 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/UnarmedCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/UnarmedCommand.java @@ -5,9 +5,9 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.UserManager; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.CombatUtils; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.text.TextComponentFactory; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; @@ -40,7 +40,7 @@ public class UnarmedCommand extends SkillCommand { protected void dataCalculations(Player player, float skillValue) { // UNARMED_ARROW_DEFLECT if (canDeflect) { - String[] deflectStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.UNARMED_ARROW_DEFLECT); + String[] deflectStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.UNARMED_ARROW_DEFLECT); deflectChance = deflectStrings[0]; deflectChanceLucky = deflectStrings[1]; } @@ -54,7 +54,7 @@ public class UnarmedCommand extends SkillCommand { // UNARMED_DISARM if (canDisarm) { - String[] disarmStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.UNARMED_DISARM); + String[] disarmStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.UNARMED_DISARM); disarmChance = disarmStrings[0]; disarmChanceLucky = disarmStrings[1]; } @@ -66,7 +66,7 @@ public class UnarmedCommand extends SkillCommand { // IRON GRIP if (canIronGrip) { - String[] ironGripStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.UNARMED_IRON_GRIP); + String[] ironGripStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.UNARMED_IRON_GRIP); ironGripChance = ironGripStrings[0]; ironGripChanceLucky = ironGripStrings[1]; } @@ -75,10 +75,10 @@ public class UnarmedCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { canBerserk = RankUtils.hasUnlockedSubskill(player, SubSkillType.UNARMED_BERSERK) && Permissions.berserk(player); - canIronArm = canUseSubskill(player, SubSkillType.UNARMED_STEEL_ARM_STYLE); - canDeflect = canUseSubskill(player, SubSkillType.UNARMED_ARROW_DEFLECT); - canDisarm = canUseSubskill(player, SubSkillType.UNARMED_DISARM); - canIronGrip = canUseSubskill(player, SubSkillType.UNARMED_IRON_GRIP); + canIronArm = Permissions.canUseSubSkill(player, SubSkillType.UNARMED_STEEL_ARM_STYLE); + canDeflect = Permissions.canUseSubSkill(player, SubSkillType.UNARMED_ARROW_DEFLECT); + canDisarm = Permissions.canUseSubSkill(player, SubSkillType.UNARMED_DISARM); + canIronGrip = Permissions.canUseSubSkill(player, SubSkillType.UNARMED_IRON_GRIP); // TODO: Apparently we forgot about block cracker? } @@ -114,7 +114,7 @@ public class UnarmedCommand extends SkillCommand { //messages.add(LocaleLoader.getString("Unarmed.Ability.Chance.IronGrip", ironGripChance) + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", ironGripChanceLucky) : "")); } - if(canUseSubskill(player, SubSkillType.UNARMED_UNARMED_LIMIT_BREAK)) { + if(Permissions.canUseSubSkill(player, SubSkillType.UNARMED_UNARMED_LIMIT_BREAK)) { messages.add(getStatMessage(SubSkillType.UNARMED_UNARMED_LIMIT_BREAK, String.valueOf(CombatUtils.getLimitBreakDamageAgainstQuality(player, SubSkillType.UNARMED_UNARMED_LIMIT_BREAK, 1000)))); } diff --git a/src/main/java/com/gmail/nossr50/commands/skills/WoodcuttingCommand.java b/src/main/java/com/gmail/nossr50/commands/skills/WoodcuttingCommand.java index 08639c27b..1dd4beeb0 100644 --- a/src/main/java/com/gmail/nossr50/commands/skills/WoodcuttingCommand.java +++ b/src/main/java/com/gmail/nossr50/commands/skills/WoodcuttingCommand.java @@ -5,8 +5,8 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.util.Permissions; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.text.TextComponentFactory; import net.kyori.adventure.text.Component; import org.bukkit.entity.Player; @@ -18,15 +18,15 @@ public class WoodcuttingCommand extends SkillCommand { private String treeFellerLength; private String treeFellerLengthEndurance; private String doubleDropChance; + private String tripleDropChance; private String doubleDropChanceLucky; + private String tripleDropChanceLucky; private boolean canTreeFell; private boolean canLeafBlow; private boolean canDoubleDrop; + private boolean canTripleDrop; private boolean canKnockOnWood; - private boolean canSplinter; - private boolean canBarkSurgeon; - private boolean canNaturesBounty; public WoodcuttingCommand() { super(PrimarySkillType.WOODCUTTING); @@ -38,7 +38,14 @@ public class WoodcuttingCommand extends SkillCommand { if (canDoubleDrop) { setDoubleDropClassicChanceStrings(player); } - + + //Clean Cuts + if(canTripleDrop) { + String[] tripleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.WOODCUTTING_CLEAN_CUTS); + tripleDropChance = tripleDropStrings[0]; + tripleDropChanceLucky = tripleDropStrings[1]; + } + // TREE FELLER if (canTreeFell) { String[] treeFellerStrings = calculateLengthDisplayValues(player, skillValue); @@ -48,7 +55,7 @@ public class WoodcuttingCommand extends SkillCommand { } private void setDoubleDropClassicChanceStrings(Player player) { - String[] doubleDropStrings = getAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.WOODCUTTING_HARVEST_LUMBER); + String[] doubleDropStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER); doubleDropChance = doubleDropStrings[0]; doubleDropChanceLucky = doubleDropStrings[1]; } @@ -56,14 +63,12 @@ public class WoodcuttingCommand extends SkillCommand { @Override protected void permissionsCheck(Player player) { canTreeFell = RankUtils.hasUnlockedSubskill(player, SubSkillType.WOODCUTTING_TREE_FELLER) && Permissions.treeFeller(player); - canDoubleDrop = canUseSubskill(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER) - && !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill) + canDoubleDrop = !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill) + && Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER) && RankUtils.getRank(player, SubSkillType.WOODCUTTING_HARVEST_LUMBER) >= 1; - canLeafBlow = canUseSubskill(player, SubSkillType.WOODCUTTING_LEAF_BLOWER); - canKnockOnWood = canTreeFell && canUseSubskill(player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD); - /*canSplinter = canUseSubskill(player, SubSkillType.WOODCUTTING_SPLINTER); - canBarkSurgeon = canUseSubskill(player, SubSkillType.WOODCUTTING_BARK_SURGEON); - canNaturesBounty = canUseSubskill(player, SubSkillType.WOODCUTTING_NATURES_BOUNTY);*/ + canTripleDrop = !mcMMO.p.getGeneralConfig().getDoubleDropsDisabled(skill) && Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_CLEAN_CUTS); + canLeafBlow = Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_LEAF_BLOWER); + canKnockOnWood = canTreeFell && Permissions.canUseSubSkill(player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD); } @Override @@ -75,6 +80,12 @@ public class WoodcuttingCommand extends SkillCommand { + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", doubleDropChanceLucky) : "")); } + if(canTripleDrop) { + messages.add(getStatMessage(SubSkillType.WOODCUTTING_CLEAN_CUTS, tripleDropChance) + + (isLucky ? LocaleLoader.getString("Perks.Lucky.Bonus", tripleDropChanceLucky) : "")); + } + + if (canKnockOnWood) { String lootNote; diff --git a/src/main/java/com/gmail/nossr50/config/AdvancedConfig.java b/src/main/java/com/gmail/nossr50/config/AdvancedConfig.java index 04cca138a..da882f098 100644 --- a/src/main/java/com/gmail/nossr50/config/AdvancedConfig.java +++ b/src/main/java/com/gmail/nossr50/config/AdvancedConfig.java @@ -627,7 +627,11 @@ public class AdvancedConfig extends BukkitConfig { /* ALCHEMY */ public int getCatalysisMaxBonusLevel() { - return config.getInt("Skills.Alchemy.Catalysis.MaxBonusLevel", 1000); + if (mcMMO.isRetroModeEnabled()) { + return config.getInt("Skills.Alchemy.Catalysis.MaxBonusLevel.RetroMode", 1000); + } else { + return config.getInt("Skills.Alchemy.Catalysis.MaxBonusLevel.Standard", 100); + } } public double getCatalysisMinSpeed() { @@ -693,6 +697,15 @@ public class AdvancedConfig extends BukkitConfig { return config.getDouble("Skills.Axes.SkullSplitter.DamageModifier", 2.0D); } + /* CROSSBOWS */ + public double getPoweredShotRankDamageMultiplier() { + return config.getDouble("Skills.Crossbows.PoweredShot.RankDamageMultiplier", 10.0D); + } + + public double getPoweredShotDamageMax() { + return config.getDouble("Skills.Archery.SkillShot.MaxDamage", 9.0D); + } + /* EXCAVATION */ //Nothing to configure, everything is already configurable in config.yml diff --git a/src/main/java/com/gmail/nossr50/config/ChatConfig.java b/src/main/java/com/gmail/nossr50/config/ChatConfig.java index 0e33d74d1..627686efb 100644 --- a/src/main/java/com/gmail/nossr50/config/ChatConfig.java +++ b/src/main/java/com/gmail/nossr50/config/ChatConfig.java @@ -51,6 +51,12 @@ public class ChatConfig extends BukkitConfig { return config.getBoolean(key, true); } + public boolean isConsoleIncludedInAudience(@NotNull ChatChannel chatChannel) { + String key = "Chat.Channels." + StringUtils.getCapitalized(chatChannel.toString()) + ".Send_To_Console"; + return config.getBoolean(key, true); + } + + public boolean isSpyingAutomatic() { return config.getBoolean("Chat.Channels.Party.Spies.Automatically_Enable_Spying", false); } diff --git a/src/main/java/com/gmail/nossr50/config/CustomItemSupportConfig.java b/src/main/java/com/gmail/nossr50/config/CustomItemSupportConfig.java new file mode 100644 index 000000000..51731d367 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/config/CustomItemSupportConfig.java @@ -0,0 +1,23 @@ +package com.gmail.nossr50.config; + +import java.io.File; + +public class CustomItemSupportConfig extends BukkitConfig { + public CustomItemSupportConfig(File dataFolder) { + super("custom_item_support.yml", dataFolder); + validate(); + } + + @Override + protected void loadKeys() { + + } + + public boolean isCustomRepairAllowed() { + return config.getBoolean("Custom_Item_Support.Repair.Allow_Repair_On_Items_With_Custom_Model_Data", true); + } + + public boolean isCustomSalvageAllowed() { + return config.getBoolean("Custom_Item_Support.Salvage.Allow_Salvage_On_Items_With_Custom_Model_Data", true); + } +} diff --git a/src/main/java/com/gmail/nossr50/config/GeneralConfig.java b/src/main/java/com/gmail/nossr50/config/GeneralConfig.java index 7a6f766c0..801cae25c 100644 --- a/src/main/java/com/gmail/nossr50/config/GeneralConfig.java +++ b/src/main/java/com/gmail/nossr50/config/GeneralConfig.java @@ -1009,4 +1009,6 @@ public class GeneralConfig extends BukkitConfig { public boolean useVerboseLogging() { return config.getBoolean("General.Verbose_Logging", false); } + + public boolean isMasterySystemEnabled() { return config.getBoolean( "General.PowerLevel.Skill_Mastery.Enabled"); } } diff --git a/src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java b/src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java index 929fdcefe..b64e3b62b 100644 --- a/src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java +++ b/src/main/java/com/gmail/nossr50/config/experience/ExperienceConfig.java @@ -177,6 +177,10 @@ public class ExperienceConfig extends BukkitConfig { return config.getBoolean("ExploitFix.PreventPluginNPCInteraction", true); } + public boolean isArmorStandInteractionPrevented() { + return config.getBoolean("ExploitFix.PreventArmorStandInteraction", true); + } + public boolean isFishingExploitingPrevented() { return config.getBoolean("ExploitFix.Fishing", true); } @@ -256,7 +260,7 @@ public class ExperienceConfig extends BukkitConfig { /* Skill modifiers */ public double getFormulaSkillModifier(PrimarySkillType skill) { - return config.getDouble("Experience_Formula.Modifier." + StringUtils.getCapitalized(skill.toString())); + return config.getDouble("Experience_Formula.Skill_Multiplier." + StringUtils.getCapitalized(skill.toString()), 1); } /* Custom XP perk */ @@ -296,6 +300,10 @@ public class ExperienceConfig extends BukkitConfig { } /* Combat XP Multipliers */ + public double getCombatXP(String entity) { + return config.getDouble("Experience_Values.Combat.Multiplier." + entity); + } + public double getCombatXP(EntityType entity) { return config.getDouble("Experience_Values.Combat.Multiplier." + StringUtils.getPrettyEntityTypeString(entity).replace(" ", "_")); } @@ -314,96 +322,73 @@ public class ExperienceConfig extends BukkitConfig { /* Materials */ public int getXp(PrimarySkillType skill, Material material) { - //TODO: Temporary measure to fix an exploit caused by a yet to be fixed Spigot bug (as of 7/3/2020) - if (material.toString().equalsIgnoreCase("LILY_PAD")) - return 0; - - String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + "."; - String explicitString = baseString + StringUtils.getExplicitConfigMaterialString(material); - if (config.contains(explicitString)) - return config.getInt(explicitString); - String friendlyString = baseString + StringUtils.getFriendlyConfigMaterialString(material); - if (config.contains(friendlyString)) - return config.getInt(friendlyString); - String wildcardString = baseString + StringUtils.getWildcardConfigMaterialString(material); - if (config.contains(wildcardString)) - return config.getInt(wildcardString); - return 0; + return getXpHelper(skill, StringUtils.getExplicitConfigMaterialString(material), + StringUtils.getFriendlyConfigMaterialString(material), + StringUtils.getWildcardConfigMaterialString(material)); } - /* Materials */ public int getXp(PrimarySkillType skill, BlockState blockState) { - Material data = blockState.getType(); - - String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + "."; - String explicitString = baseString + StringUtils.getExplicitConfigMaterialString(data); - if (config.contains(explicitString)) - return config.getInt(explicitString); - String friendlyString = baseString + StringUtils.getFriendlyConfigMaterialString(data); - if (config.contains(friendlyString)) - return config.getInt(friendlyString); - String wildcardString = baseString + StringUtils.getWildcardConfigMaterialString(data); - if (config.contains(wildcardString)) - return config.getInt(wildcardString); - return 0; + Material material = blockState.getType(); + return getXp(skill, material); } - /* Materials */ public int getXp(PrimarySkillType skill, Block block) { - Material data = block.getType(); - - String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + "."; - String explicitString = baseString + StringUtils.getExplicitConfigMaterialString(data); - if (config.contains(explicitString)) - return config.getInt(explicitString); - String friendlyString = baseString + StringUtils.getFriendlyConfigMaterialString(data); - if (config.contains(friendlyString)) - return config.getInt(friendlyString); - String wildcardString = baseString + StringUtils.getWildcardConfigMaterialString(data); - if (config.contains(wildcardString)) - return config.getInt(wildcardString); - return 0; + Material material = block.getType(); + return getXp(skill, material); } - /* Materials */ public int getXp(PrimarySkillType skill, BlockData data) { + return getXpHelper(skill, StringUtils.getExplicitConfigBlockDataString(data), + StringUtils.getFriendlyConfigBlockDataString(data), + StringUtils.getWildcardConfigBlockDataString(data)); + } + + private int getXpHelper(PrimarySkillType skill, String explicitString, String friendlyString, String wildcardString) { + if (explicitString.equalsIgnoreCase("LILY_PAD")) { + return 0; + } + String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + "."; - String explicitString = baseString + StringUtils.getExplicitConfigBlockDataString(data); - if (config.contains(explicitString)) - return config.getInt(explicitString); - String friendlyString = baseString + StringUtils.getFriendlyConfigBlockDataString(data); - if (config.contains(friendlyString)) - return config.getInt(friendlyString); - String wildcardString = baseString + StringUtils.getWildcardConfigBlockDataString(data); - if (config.contains(wildcardString)) - return config.getInt(wildcardString); + String[] configStrings = {explicitString, friendlyString, wildcardString}; + + for (String configString : configStrings) { + String fullPath = baseString + configString; + if (config.contains(fullPath)) { + return config.getInt(fullPath); + } + } + return 0; } - public boolean doesBlockGiveSkillXP(PrimarySkillType skill, Material data) { - String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + "."; - String explicitString = baseString + StringUtils.getExplicitConfigMaterialString(data); - if (config.contains(explicitString)) - return true; - String friendlyString = baseString + StringUtils.getFriendlyConfigMaterialString(data); - if (config.contains(friendlyString)) - return true; - String wildcardString = baseString + StringUtils.getWildcardConfigMaterialString(data); - return config.contains(wildcardString); + + public boolean doesBlockGiveSkillXP(PrimarySkillType skill, Material material) { + return doesBlockGiveSkillXPHelper(skill, StringUtils.getExplicitConfigMaterialString(material), + StringUtils.getFriendlyConfigMaterialString(material), + StringUtils.getWildcardConfigMaterialString(material)); } public boolean doesBlockGiveSkillXP(PrimarySkillType skill, BlockData data) { - String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + "."; - String explicitString = baseString + StringUtils.getExplicitConfigBlockDataString(data); - if (config.contains(explicitString)) - return true; - String friendlyString = baseString + StringUtils.getFriendlyConfigBlockDataString(data); - if (config.contains(friendlyString)) - return true; - String wildcardString = baseString + StringUtils.getWildcardConfigBlockDataString(data); - return config.contains(wildcardString); + return doesBlockGiveSkillXPHelper(skill, StringUtils.getExplicitConfigBlockDataString(data), + StringUtils.getFriendlyConfigBlockDataString(data), + StringUtils.getWildcardConfigBlockDataString(data)); } + private boolean doesBlockGiveSkillXPHelper(PrimarySkillType skill, String explicitString, String friendlyString, String wildcardString) { + String baseString = "Experience_Values." + StringUtils.getCapitalized(skill.toString()) + "."; + String[] configStrings = {explicitString, friendlyString, wildcardString}; + + for (String configString : configStrings) { + String fullPath = baseString + configString; + if (config.contains(fullPath)) { + return true; + } + } + + return false; + } + + /* * Experience Bar Stuff */ diff --git a/src/main/java/com/gmail/nossr50/database/DatabaseManagerFactory.java b/src/main/java/com/gmail/nossr50/database/DatabaseManagerFactory.java index cb5025f6f..df6c5a158 100644 --- a/src/main/java/com/gmail/nossr50/database/DatabaseManagerFactory.java +++ b/src/main/java/com/gmail/nossr50/database/DatabaseManagerFactory.java @@ -10,6 +10,7 @@ import java.util.logging.Logger; public class DatabaseManagerFactory { private static Class customManager = null; + public static final String MYSQL_DRIVER = "com.mysql.cj.jdbc.Driver"; public static DatabaseManager getDatabaseManager(@NotNull String userFilePath, @NotNull Logger logger, long purgeTime, int startingLevel) { if (customManager != null) { @@ -27,7 +28,7 @@ public class DatabaseManagerFactory { LogUtils.debug(mcMMO.p.getLogger(), "Falling back on " + (mcMMO.p.getGeneralConfig().getUseMySQL() ? "SQL" : "Flatfile") + " database"); } - return mcMMO.p.getGeneralConfig().getUseMySQL() ? new SQLDatabaseManager() : new FlatFileDatabaseManager(userFilePath, logger, purgeTime, startingLevel); + return mcMMO.p.getGeneralConfig().getUseMySQL() ? new SQLDatabaseManager(logger, MYSQL_DRIVER) : new FlatFileDatabaseManager(userFilePath, logger, purgeTime, startingLevel); } /** @@ -68,7 +69,7 @@ public class DatabaseManagerFactory { case SQL: LogUtils.debug(mcMMO.p.getLogger(), "Using SQL Database"); - return new SQLDatabaseManager(); + return new SQLDatabaseManager(logger, "com.mysql.cj.jdbc.Driver"); case CUSTOM: try { diff --git a/src/main/java/com/gmail/nossr50/database/FlatFileDataProcessor.java b/src/main/java/com/gmail/nossr50/database/FlatFileDataProcessor.java index a232d86cd..6f8c397e3 100644 --- a/src/main/java/com/gmail/nossr50/database/FlatFileDataProcessor.java +++ b/src/main/java/com/gmail/nossr50/database/FlatFileDataProcessor.java @@ -276,6 +276,8 @@ public class FlatFileDataProcessor { case SKILLS_TAMING: case SKILLS_FISHING: case SKILLS_ALCHEMY: + case SKILLS_CROSSBOWS: + case SKILLS_TRIDENTS: case COOLDOWN_BERSERK: case COOLDOWN_GIGA_DRILL_BREAKER: case COOLDOWN_TREE_FELLER: @@ -286,6 +288,9 @@ public class FlatFileDataProcessor { case COOLDOWN_BLAST_MINING: case SCOREBOARD_TIPS: case COOLDOWN_CHIMAERA_WING: + case COOLDOWN_SUPER_SHOTGUN: + case COOLDOWN_TRIDENTS: + case COOLDOWN_ARCHERY: return ExpectedType.INTEGER; case EXP_MINING: case EXP_WOODCUTTING: @@ -300,6 +305,8 @@ public class FlatFileDataProcessor { case EXP_TAMING: case EXP_FISHING: case EXP_ALCHEMY: + case EXP_CROSSBOWS: + case EXP_TRIDENTS: return ExpectedType.FLOAT; case UUID_INDEX: return ExpectedType.UUID; diff --git a/src/main/java/com/gmail/nossr50/database/FlatFileDatabaseManager.java b/src/main/java/com/gmail/nossr50/database/FlatFileDatabaseManager.java index 52fa9a330..d4d2e51b6 100644 --- a/src/main/java/com/gmail/nossr50/database/FlatFileDatabaseManager.java +++ b/src/main/java/com/gmail/nossr50/database/FlatFileDatabaseManager.java @@ -80,10 +80,17 @@ public final class FlatFileDatabaseManager implements DatabaseManager { public static final int SCOREBOARD_TIPS = 42; public static final int COOLDOWN_CHIMAERA_WING = 43; public static final int OVERHAUL_LAST_LOGIN = 44; + public static final int EXP_CROSSBOWS = 45; + public static final int SKILLS_CROSSBOWS = 46; + public static final int EXP_TRIDENTS = 47; + public static final int SKILLS_TRIDENTS = 48; + public static final int COOLDOWN_SUPER_SHOTGUN = 49; + public static final int COOLDOWN_TRIDENTS = 50; + public static final int COOLDOWN_ARCHERY = 51; + //Update this everytime new data is added + public static final int DATA_ENTRY_COUNT = COOLDOWN_ARCHERY + 1; - public static final int DATA_ENTRY_COUNT = OVERHAUL_LAST_LOGIN + 1; //Update this everytime new data is added - - protected FlatFileDatabaseManager(@NotNull File usersFile, @NotNull Logger logger, long purgeTime, int startingLevel, boolean testing) { + FlatFileDatabaseManager(@NotNull File usersFile, @NotNull Logger logger, long purgeTime, int startingLevel, boolean testing) { this.usersFile = usersFile; this.usersFilePath = usersFile.getPath(); this.logger = logger; @@ -99,7 +106,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager { List flatFileDataFlags = checkFileHealthAndStructure(); if(flatFileDataFlags != null) { - if(flatFileDataFlags.size() > 0) { + if(!flatFileDataFlags.isEmpty()) { logger.info("Detected "+flatFileDataFlags.size() + " data entries which need correction."); } } @@ -108,7 +115,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager { } } - protected FlatFileDatabaseManager(@NotNull String usersFilePath, @NotNull Logger logger, long purgeTime, int startingLevel) { + FlatFileDatabaseManager(@NotNull String usersFilePath, @NotNull Logger logger, long purgeTime, int startingLevel) { this(new File(usersFilePath), logger, purgeTime, startingLevel, false); } @@ -237,7 +244,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager { out.write(writer.toString()); if(testing) { - System.out.println(writer.toString()); + System.out.println(writer); } } catch (IOException e) { @@ -467,6 +474,10 @@ public final class FlatFileDatabaseManager implements DatabaseManager { appendable.append(String.valueOf(profile.getScoreboardTipsShown())).append(":"); appendable.append(String.valueOf(profile.getUniqueData(UniqueDataType.CHIMAERA_WING_DATS))).append(":"); appendable.append(String.valueOf(profile.getLastLogin())).append(":"); //overhaul last login + appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.CROSSBOWS))).append(":"); + appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.CROSSBOWS))).append(":"); + appendable.append(String.valueOf(profile.getSkillXpLevel(PrimarySkillType.TRIDENTS))).append(":"); + appendable.append(String.valueOf(profile.getSkillLevel(PrimarySkillType.TRIDENTS))).append(":"); appendable.append("\r\n"); } @@ -565,16 +576,11 @@ public final class FlatFileDatabaseManager implements DatabaseManager { * @return a profile with the targets data or an unloaded profile if no data was found */ private @NotNull PlayerProfile processUserQuery(@NotNull UserQuery userQuery) throws RuntimeException { - switch(userQuery.getType()) { - case UUID_AND_NAME: - return queryByUUIDAndName((UserQueryFull) userQuery); - case UUID: - return queryByUUID((UserQueryUUID) userQuery); - case NAME: - return queryByName((UserQueryNameImpl) userQuery); - default: - throw new RuntimeException("No case for this UserQueryType!"); - } + return switch (userQuery.getType()) { + case UUID_AND_NAME -> queryByUUIDAndName((UserQueryFull) userQuery); + case UUID -> queryByUUID((UserQueryUUID) userQuery); + case NAME -> queryByName((UserQueryNameImpl) userQuery); + }; } private @NotNull PlayerProfile queryByName(@NotNull UserQueryName userQuery) { @@ -603,8 +609,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager { continue; } - - //If we couldn't find anyone + // we found the player if(playerName.equalsIgnoreCase(rawSplitData[USERNAME_INDEX])) { return loadFromLine(rawSplitData); } @@ -681,7 +686,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager { * No match was found in the file */ - return grabUnloadedProfile(uuid, "Player-Not-Found="+uuid.toString()); + return grabUnloadedProfile(uuid, "Player-Not-Found="+ uuid); } private @NotNull PlayerProfile queryByUUIDAndName(@NotNull UserQueryFull userQuery) { @@ -716,7 +721,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager { boolean matchingName = dbPlayerName.equalsIgnoreCase(playerName); if (!matchingName) { - logger.warning("When loading user: "+playerName +" with UUID of (" + uuid.toString() + logger.warning("When loading user: "+playerName +" with UUID of (" + uuid +") we found a mismatched name, the name in the DB will be replaced (DB name: "+dbPlayerName+")"); //logger.info("Name updated for player: " + rawSplitData[USERNAME_INDEX] + " => " + playerName); rawSplitData[USERNAME_INDEX] = playerName; @@ -980,6 +985,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager { List taming = new ArrayList<>(); List fishing = new ArrayList<>(); List alchemy = new ArrayList<>(); + List crossbows = new ArrayList<>(); + List tridents = new ArrayList<>(); BufferedReader in = null; String playerName = null; @@ -1013,6 +1020,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager { powerLevel += putStat(taming, playerName, skills.get(PrimarySkillType.TAMING)); powerLevel += putStat(unarmed, playerName, skills.get(PrimarySkillType.UNARMED)); powerLevel += putStat(woodcutting, playerName, skills.get(PrimarySkillType.WOODCUTTING)); + powerLevel += putStat(crossbows, playerName, skills.get(PrimarySkillType.CROSSBOWS)); + powerLevel += putStat(tridents, playerName, skills.get(PrimarySkillType.TRIDENTS)); putStat(powerLevels, playerName, powerLevel); } @@ -1048,6 +1057,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager { taming.sort(c); fishing.sort(c); alchemy.sort(c); + crossbows.sort(c); + tridents.sort(c); powerLevels.sort(c); playerStatHash.put(PrimarySkillType.MINING, mining); @@ -1063,6 +1074,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager { playerStatHash.put(PrimarySkillType.TAMING, taming); playerStatHash.put(PrimarySkillType.FISHING, fishing); playerStatHash.put(PrimarySkillType.ALCHEMY, alchemy); + playerStatHash.put(PrimarySkillType.CROSSBOWS, crossbows); + playerStatHash.put(PrimarySkillType.TRIDENTS, tridents); return LeaderboardStatus.UPDATED; } @@ -1094,7 +1107,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager { public @Nullable List checkFileHealthAndStructure() { ArrayList flagsFound = null; LogUtils.debug(logger, "(" + usersFile.getPath() + ") Validating database file.."); - FlatFileDataProcessor dataProcessor = null; + FlatFileDataProcessor dataProcessor; if (usersFile.exists()) { BufferedReader bufferedReader = null; @@ -1126,7 +1139,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager { } //Only update the file if needed - if(dataProcessor.getFlatFileDataFlags().size() > 0) { + if(!dataProcessor.getFlatFileDataFlags().isEmpty()) { flagsFound = new ArrayList<>(dataProcessor.getFlatFileDataFlags()); logger.info("Updating FlatFile Database..."); fileWriter = new FileWriter(usersFilePath); @@ -1144,7 +1157,7 @@ public final class FlatFileDatabaseManager implements DatabaseManager { } } - if(flagsFound == null || flagsFound.size() == 0) { + if(flagsFound == null || flagsFound.isEmpty()) { return null; } else { return flagsFound; @@ -1224,6 +1237,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager { tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.ACROBATICS, EXP_ACROBATICS, username); tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.FISHING, EXP_FISHING, username); tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.ALCHEMY, EXP_ALCHEMY, username); + tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.CROSSBOWS, EXP_CROSSBOWS, username); + tryLoadSkillFloatValuesFromRawData(skillsXp, character, PrimarySkillType.TRIDENTS, EXP_TRIDENTS, username); // Taming - Unused tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SUPER_BREAKER, COOLDOWN_SUPER_BREAKER, username); @@ -1232,11 +1247,13 @@ public final class FlatFileDatabaseManager implements DatabaseManager { tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.BERSERK, COOLDOWN_BERSERK, username); tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.GREEN_TERRA, COOLDOWN_GREEN_TERRA, username); tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.GIGA_DRILL_BREAKER, COOLDOWN_GIGA_DRILL_BREAKER, username); - // Archery - Unused + tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.EXPLOSIVE_SHOT, COOLDOWN_ARCHERY, username); tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SERRATED_STRIKES, COOLDOWN_SERRATED_STRIKES, username); tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SKULL_SPLITTER, COOLDOWN_SKULL_SPLITTER, username); // Acrobatics - Unused tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.BLAST_MINING, COOLDOWN_BLAST_MINING, username); + tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.SUPER_SHOTGUN, COOLDOWN_SUPER_SHOTGUN, username); + tryLoadSkillCooldownFromRawData(skillsDATS, character, SuperAbilityType.TRIDENTS_SUPER_ABILITY, COOLDOWN_TRIDENTS, username); UUID uuid; try { @@ -1269,12 +1286,15 @@ public final class FlatFileDatabaseManager implements DatabaseManager { return new PlayerProfile(username, uuid, skills, skillsXp, skillsDATS, scoreboardTipsShown, uniquePlayerDataMap, lastLogin); } - private void tryLoadSkillCooldownFromRawData(@NotNull Map cooldownMap, @NotNull String[] character, @NotNull SuperAbilityType superAbilityType, int cooldownSuperBreaker, @NotNull String userName) { + private void tryLoadSkillCooldownFromRawData(@NotNull Map cooldownMap, @NotNull String[] splitData, @NotNull SuperAbilityType superAbilityType, int index, @NotNull String userName) { try { - cooldownMap.put(superAbilityType, Integer.valueOf(character[cooldownSuperBreaker])); + cooldownMap.put(superAbilityType, Integer.valueOf(splitData[index])); + } catch (IndexOutOfBoundsException e) { + // TODO: Add debug message + // set to 0 when data not found + cooldownMap.put(superAbilityType, 0); } catch (NumberFormatException e) { - logger.severe("Data corruption when trying to load the value for skill "+superAbilityType+" for player named " + userName+ " setting value to zero"); - e.printStackTrace(); + throw new NumberFormatException("Data corruption when trying to load the cooldown for skill "+superAbilityType+" for player named " + userName); } } @@ -1293,6 +1313,10 @@ public final class FlatFileDatabaseManager implements DatabaseManager { try { int valueFromString = Integer.parseInt(character[index]); skillMap.put(primarySkillType, valueFromString); + } catch (ArrayIndexOutOfBoundsException e ) { + // TODO: Add debug message + // set to 0 when data not found + skillMap.put(primarySkillType, 0); } catch (NumberFormatException e) { skillMap.put(primarySkillType, 0); logger.severe("Data corruption when trying to load the value for skill "+primarySkillType+" for player named " + userName+ " setting value to zero"); @@ -1317,6 +1341,8 @@ public final class FlatFileDatabaseManager implements DatabaseManager { tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.AXES, SKILLS_AXES, username); tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.FISHING, SKILLS_FISHING, username); tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.ALCHEMY, SKILLS_ALCHEMY, username); + tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.CROSSBOWS, SKILLS_CROSSBOWS, username); + tryLoadSkillIntValuesFromRawData(skills, character, PrimarySkillType.TRIDENTS, SKILLS_TRIDENTS, username); return skills; } diff --git a/src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java b/src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java index 0be013e7e..892a43721 100644 --- a/src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java +++ b/src/main/java/com/gmail/nossr50/database/SQLDatabaseManager.java @@ -24,6 +24,7 @@ import org.jetbrains.annotations.Nullable; import java.sql.*; import java.util.*; import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Logger; public final class SQLDatabaseManager implements DatabaseManager { private static final String ALL_QUERY_VERSION = "total"; @@ -32,6 +33,7 @@ public final class SQLDatabaseManager implements DatabaseManager { public static final String USER_VARCHAR = "VARCHAR(40)"; public static final int CHILD_SKILLS_SIZE = 2; public static final String LEGACY_DRIVER_PATH = "com.mysql.jdbc.Driver"; + public static final int MAGIC_NUMBER = 44; private final String tablePrefix = mcMMO.p.getGeneralConfig().getMySQLTablePrefix(); private final Map cachedUserIDs = new HashMap<>(); @@ -45,23 +47,19 @@ public final class SQLDatabaseManager implements DatabaseManager { private final ReentrantLock massUpdateLock = new ReentrantLock(); private final String CHARSET_SQL = "utf8mb4"; //This is compliant with UTF-8 while "utf8" is not, confusing but this is how it is. - private String driverPath = "com.mysql.cj.jdbc.Driver"; //modern driver + private final Logger logger; + private final boolean h2; - protected SQLDatabaseManager() { - String connectionString = "jdbc:mysql://" + mcMMO.p.getGeneralConfig().getMySQLServerName() - + ":" + mcMMO.p.getGeneralConfig().getMySQLServerPort() + "/" + mcMMO.p.getGeneralConfig().getMySQLDatabaseName(); + SQLDatabaseManager(Logger logger, String driverPath) { + this(logger, driverPath, false); + } - if(!mcMMO.getCompatibilityManager().getMinecraftGameVersion().isAtLeast(1, 17, 0) //Temporary hack for SQL and 1.17 support - && mcMMO.p.getGeneralConfig().getMySQLSSL()) - connectionString += - "?verifyServerCertificate=false"+ - "&useSSL=true"+ - "&requireSSL=true"; - else - connectionString+= - "?useSSL=false"; + SQLDatabaseManager(Logger logger, String driverPath, boolean h2) { + this.logger = logger; + this.h2 = h2; + String connectionString = getConnectionString(h2); - if(mcMMO.p.getGeneralConfig().getMySQLPublicKeyRetrieval()) { + if(!h2 && mcMMO.p.getGeneralConfig().getMySQLPublicKeyRetrieval()) { connectionString+= "&allowPublicKeyRetrieval=true"; } @@ -76,7 +74,7 @@ public final class SQLDatabaseManager implements DatabaseManager { } catch (ClassNotFoundException ex) { e.printStackTrace(); ex.printStackTrace(); - mcMMO.p.getLogger().severe("Neither driver found"); + logger.severe("Neither driver found"); return; } //throw e; // aborts onEnable() Riking if you want to do this, fully implement it. @@ -133,9 +131,31 @@ public final class SQLDatabaseManager implements DatabaseManager { checkStructure(); } + @NotNull + private static String getConnectionString(boolean h2) { + if (h2) { + return "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL"; + } + + String connectionString = "jdbc:mysql://" + mcMMO.p.getGeneralConfig().getMySQLServerName() + + ":" + mcMMO.p.getGeneralConfig().getMySQLServerPort() + "/" + mcMMO.p.getGeneralConfig().getMySQLDatabaseName(); + + if(!mcMMO.getCompatibilityManager().getMinecraftGameVersion().isAtLeast(1, 17, 0) //Temporary hack for SQL and 1.17 support + && mcMMO.p.getGeneralConfig().getMySQLSSL()) + connectionString += + "?verifyServerCertificate=false"+ + "&useSSL=true"+ + "&requireSSL=true"; + else + connectionString+= + "?useSSL=false"; + return connectionString; + } + + // TODO: unit tests public int purgePowerlessUsers() { massUpdateLock.lock(); - mcMMO.p.getLogger().info("Purging powerless users..."); + logger.info("Purging powerless users..."); Connection connection = null; Statement statement = null; @@ -149,7 +169,7 @@ public final class SQLDatabaseManager implements DatabaseManager { + "taming = 0 AND mining = 0 AND woodcutting = 0 AND repair = 0 " + "AND unarmed = 0 AND herbalism = 0 AND excavation = 0 AND " + "archery = 0 AND swords = 0 AND axes = 0 AND acrobatics = 0 " - + "AND fishing = 0 AND alchemy = 0;"); + + "AND fishing = 0 AND alchemy = 0 AND crossbows = 0 AND tridents = 0;"); statement.executeUpdate("DELETE FROM `" + tablePrefix + "experience` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "experience`.`user_id` = `s`.`user_id`)"); statement.executeUpdate("DELETE FROM `" + tablePrefix + "huds` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "huds`.`user_id` = `s`.`user_id`)"); @@ -165,13 +185,13 @@ public final class SQLDatabaseManager implements DatabaseManager { massUpdateLock.unlock(); } - mcMMO.p.getLogger().info("Purged " + purged + " users from the database."); + logger.info("Purged " + purged + " users from the database."); return purged; } public void purgeOldUsers() { massUpdateLock.lock(); - mcMMO.p.getLogger().info("Purging inactive users older than " + (mcMMO.p.getPurgeTime() / 2630000000L) + " months..."); + logger.info("Purging inactive users older than " + (mcMMO.p.getPurgeTime() / 2630000000L) + " months..."); Connection connection = null; Statement statement = null; @@ -196,7 +216,7 @@ public final class SQLDatabaseManager implements DatabaseManager { massUpdateLock.unlock(); } - mcMMO.p.getLogger().info("Purged " + purged + " users from the database."); + logger.info("Purged " + purged + " users from the database."); } public boolean removeUser(String playerName, UUID uuid) { @@ -212,7 +232,7 @@ public final class SQLDatabaseManager implements DatabaseManager { "JOIN " + tablePrefix + "huds h ON (u.id = h.user_id) " + "JOIN " + tablePrefix + "skills s ON (u.id = s.user_id) " + "JOIN " + tablePrefix + "cooldowns c ON (u.id = c.user_id) " + - "WHERE u.user = ?"); + "WHERE u.`user` = ?"); statement.setString(1, playerName); @@ -253,7 +273,7 @@ public final class SQLDatabaseManager implements DatabaseManager { if (id == -1) { id = newUser(connection, profile.getPlayerName(), profile.getUniqueId()); if (id == -1) { - mcMMO.p.getLogger().severe("Failed to create new account for " + profile.getPlayerName()); + logger.severe("Failed to create new account for " + profile.getPlayerName()); return false; } } @@ -263,7 +283,7 @@ public final class SQLDatabaseManager implements DatabaseManager { success &= (statement.executeUpdate() != 0); statement.close(); if (!success) { - mcMMO.p.getLogger().severe("Failed to update last login for " + profile.getPlayerName()); + logger.severe("Failed to update last login for " + profile.getPlayerName()); return false; } @@ -271,7 +291,7 @@ public final class SQLDatabaseManager implements DatabaseManager { + " taming = ?, mining = ?, repair = ?, woodcutting = ?" + ", unarmed = ?, herbalism = ?, excavation = ?" + ", archery = ?, swords = ?, axes = ?, acrobatics = ?" - + ", fishing = ?, alchemy = ?, total = ? WHERE user_id = ?"); + + ", fishing = ?, alchemy = ?, crossbows = ?, tridents = ?, total = ? WHERE user_id = ?"); statement.setInt(1, profile.getSkillLevel(PrimarySkillType.TAMING)); statement.setInt(2, profile.getSkillLevel(PrimarySkillType.MINING)); statement.setInt(3, profile.getSkillLevel(PrimarySkillType.REPAIR)); @@ -285,15 +305,17 @@ public final class SQLDatabaseManager implements DatabaseManager { statement.setInt(11, profile.getSkillLevel(PrimarySkillType.ACROBATICS)); statement.setInt(12, profile.getSkillLevel(PrimarySkillType.FISHING)); statement.setInt(13, profile.getSkillLevel(PrimarySkillType.ALCHEMY)); + statement.setInt(14, profile.getSkillLevel(PrimarySkillType.CROSSBOWS)); + statement.setInt(15, profile.getSkillLevel(PrimarySkillType.TRIDENTS)); int total = 0; for (PrimarySkillType primarySkillType : SkillTools.NON_CHILD_SKILLS) total += profile.getSkillLevel(primarySkillType); - statement.setInt(14, total); - statement.setInt(15, id); + statement.setInt(16, total); + statement.setInt(17, id); success &= (statement.executeUpdate() != 0); statement.close(); if (!success) { - mcMMO.p.getLogger().severe("Failed to update skills for " + profile.getPlayerName()); + logger.severe("Failed to update skills for " + profile.getPlayerName()); return false; } @@ -301,7 +323,7 @@ public final class SQLDatabaseManager implements DatabaseManager { + " taming = ?, mining = ?, repair = ?, woodcutting = ?" + ", unarmed = ?, herbalism = ?, excavation = ?" + ", archery = ?, swords = ?, axes = ?, acrobatics = ?" - + ", fishing = ?, alchemy = ? WHERE user_id = ?"); + + ", fishing = ?, alchemy = ?, crossbows = ?, tridents = ? WHERE user_id = ?"); statement.setInt(1, profile.getSkillXpLevel(PrimarySkillType.TAMING)); statement.setInt(2, profile.getSkillXpLevel(PrimarySkillType.MINING)); statement.setInt(3, profile.getSkillXpLevel(PrimarySkillType.REPAIR)); @@ -315,18 +337,20 @@ public final class SQLDatabaseManager implements DatabaseManager { statement.setInt(11, profile.getSkillXpLevel(PrimarySkillType.ACROBATICS)); statement.setInt(12, profile.getSkillXpLevel(PrimarySkillType.FISHING)); statement.setInt(13, profile.getSkillXpLevel(PrimarySkillType.ALCHEMY)); - statement.setInt(14, id); + statement.setInt(14, profile.getSkillXpLevel(PrimarySkillType.CROSSBOWS)); + statement.setInt(15, profile.getSkillXpLevel(PrimarySkillType.TRIDENTS)); + statement.setInt(16, id); success &= (statement.executeUpdate() != 0); statement.close(); if (!success) { - mcMMO.p.getLogger().severe("Failed to update experience for " + profile.getPlayerName()); + logger.severe("Failed to update experience for " + profile.getPlayerName()); return false; } statement = connection.prepareStatement("UPDATE " + tablePrefix + "cooldowns SET " + " mining = ?, woodcutting = ?, unarmed = ?" + ", herbalism = ?, excavation = ?, swords = ?" - + ", axes = ?, blast_mining = ?, chimaera_wing = ? WHERE user_id = ?"); + + ", axes = ?, blast_mining = ?, chimaera_wing = ?, crossbows = ?, tridents = ? WHERE user_id = ?"); statement.setLong(1, profile.getAbilityDATS(SuperAbilityType.SUPER_BREAKER)); statement.setLong(2, profile.getAbilityDATS(SuperAbilityType.TREE_FELLER)); statement.setLong(3, profile.getAbilityDATS(SuperAbilityType.BERSERK)); @@ -336,11 +360,13 @@ public final class SQLDatabaseManager implements DatabaseManager { statement.setLong(7, profile.getAbilityDATS(SuperAbilityType.SKULL_SPLITTER)); statement.setLong(8, profile.getAbilityDATS(SuperAbilityType.BLAST_MINING)); statement.setLong(9, profile.getUniqueData(UniqueDataType.CHIMAERA_WING_DATS)); - statement.setInt(10, id); + statement.setLong(10, profile.getAbilityDATS(SuperAbilityType.SUPER_SHOTGUN)); + statement.setLong(11, profile.getAbilityDATS(SuperAbilityType.TRIDENTS_SUPER_ABILITY)); + statement.setInt(12, id); success = (statement.executeUpdate() != 0); statement.close(); if (!success) { - mcMMO.p.getLogger().severe("Failed to update cooldowns for " + profile.getPlayerName()); + logger.severe("Failed to update cooldowns for " + profile.getPlayerName()); return false; } @@ -351,7 +377,7 @@ public final class SQLDatabaseManager implements DatabaseManager { success = (statement.executeUpdate() != 0); statement.close(); if (!success) { - mcMMO.p.getLogger().severe("Failed to update hud settings for " + profile.getPlayerName()); + logger.severe("Failed to update hud settings for " + profile.getPlayerName()); return false; } } @@ -371,11 +397,10 @@ public final class SQLDatabaseManager implements DatabaseManager { //Fix for a plugin that people are using that is throwing SQL errors if(skill != null && SkillTools.isChildSkill(skill)) { - mcMMO.p.getLogger().severe("A plugin hooking into mcMMO is being naughty with our database commands, update all plugins that hook into mcMMO and contact their devs!"); + logger.severe("A plugin hooking into mcMMO is being naughty with our database commands, update all plugins that hook into mcMMO and contact their devs!"); throw new InvalidSkillException("A plugin hooking into mcMMO that you are using is attempting to read leaderboard skills for child skills, child skills do not have leaderboards! This is NOT an mcMMO error!"); } - String query = skill == null ? ALL_QUERY_VERSION : skill.name().toLowerCase(Locale.ENGLISH); ResultSet resultSet = null; PreparedStatement statement = null; @@ -383,7 +408,7 @@ public final class SQLDatabaseManager implements DatabaseManager { try { connection = getConnection(PoolIdentifier.MISC); - statement = connection.prepareStatement("SELECT " + query + ", user FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON (user_id = id) WHERE " + query + " > 0 AND NOT user = '\\_INVALID\\_OLD\\_USERNAME\\_' ORDER BY " + query + " DESC, user LIMIT ?, ?"); + statement = connection.prepareStatement("SELECT " + query + ", `user` FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON (user_id = id) WHERE " + query + " > 0 AND NOT `user` = '\\_INVALID\\_OLD\\_USERNAME\\_' ORDER BY " + query + " DESC, `user` LIMIT ?, ?"); statement.setInt(1, (pageNumber * statsPerPage) - statsPerPage); statement.setInt(2, statsPerPage); resultSet = statement.executeQuery(); @@ -424,7 +449,7 @@ public final class SQLDatabaseManager implements DatabaseManager { // Get count of all users with higher skill level than player String sql = "SELECT COUNT(*) AS 'rank' FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE " + skillName + " > 0 " + "AND " + skillName + " > (SELECT " + skillName + " FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id " + - "WHERE user = ?)"; + "WHERE `user` = ?)"; statement = connection.prepareStatement(sql); statement.setString(1, playerName); @@ -437,7 +462,7 @@ public final class SQLDatabaseManager implements DatabaseManager { // Ties are settled by alphabetical order sql = "SELECT user, " + skillName + " FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE " + skillName + " > 0 " + "AND " + skillName + " = (SELECT " + skillName + " FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id " + - "WHERE user = '" + playerName + "') ORDER BY user"; + "WHERE `user` = '" + playerName + "') ORDER BY user"; resultSet.close(); statement.close(); @@ -460,7 +485,7 @@ public final class SQLDatabaseManager implements DatabaseManager { "WHERE " + ALL_QUERY_VERSION + " > 0 " + "AND " + ALL_QUERY_VERSION + " > " + "(SELECT " + ALL_QUERY_VERSION + " " + - "FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE user = ?)"; + "FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE `user` = ?)"; statement = connection.prepareStatement(sql); statement.setString(1, playerName); @@ -478,7 +503,7 @@ public final class SQLDatabaseManager implements DatabaseManager { "WHERE " + ALL_QUERY_VERSION + " > 0 " + "AND " + ALL_QUERY_VERSION + " = " + "(SELECT " + ALL_QUERY_VERSION + " " + - "FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE user = ?) ORDER BY user"; + "FROM " + tablePrefix + "users JOIN " + tablePrefix + "skills ON user_id = id WHERE `user` = ?) ORDER BY user"; statement = connection.prepareStatement(sql); statement.setString(1, playerName); @@ -546,12 +571,13 @@ public final class SQLDatabaseManager implements DatabaseManager { try { statement = connection.prepareStatement( "UPDATE `" + tablePrefix + "users` " - + "SET user = ? " - + "WHERE user = ?"); + + "SET `user` = ? " + + "WHERE `user` = ?"); statement.setString(1, "_INVALID_OLD_USERNAME_"); statement.setString(2, playerName); statement.executeUpdate(); statement.close(); + statement = connection.prepareStatement("INSERT INTO " + tablePrefix + "users (user, uuid, lastlogin) VALUES (?, ?, UNIX_TIMESTAMP())", Statement.RETURN_GENERATED_KEYS); statement.setString(1, playerName); statement.setString(2, uuid != null ? uuid.toString() : null); @@ -560,7 +586,7 @@ public final class SQLDatabaseManager implements DatabaseManager { resultSet = statement.getGeneratedKeys(); if (!resultSet.next()) { - mcMMO.p.getLogger().severe("Unable to create new user account in DB"); + logger.severe("Unable to create new user account in DB"); return -1; } @@ -600,7 +626,6 @@ public final class SQLDatabaseManager implements DatabaseManager { return loadPlayerFromDB(uuid, null); } - private PlayerProfile loadPlayerFromDB(@Nullable UUID uuid, @Nullable String playerName) throws RuntimeException { if(uuid == null && playerName == null) { throw new RuntimeException("Error looking up player, both UUID and playerName are null and one must not be."); @@ -622,17 +647,18 @@ public final class SQLDatabaseManager implements DatabaseManager { writeMissingRows(connection, id); statement = connection.prepareStatement( - "SELECT " - + "s.taming, s.mining, s.repair, s.woodcutting, s.unarmed, s.herbalism, s.excavation, s.archery, s.swords, s.axes, s.acrobatics, s.fishing, s.alchemy, " - + "e.taming, e.mining, e.repair, e.woodcutting, e.unarmed, e.herbalism, e.excavation, e.archery, e.swords, e.axes, e.acrobatics, e.fishing, e.alchemy, " - + "c.taming, c.mining, c.repair, c.woodcutting, c.unarmed, c.herbalism, c.excavation, c.archery, c.swords, c.axes, c.acrobatics, c.blast_mining, c.chimaera_wing, " - + "h.mobhealthbar, h.scoreboardtips, u.uuid, u.user " + "SELECT " + + "s.taming, s.mining, s.repair, s.woodcutting, s.unarmed, s.herbalism, s.excavation, s.archery, s.swords, s.axes, s.acrobatics, s.fishing, s.alchemy, s.crossbows, s.tridents, " + + "e.taming, e.mining, e.repair, e.woodcutting, e.unarmed, e.herbalism, e.excavation, e.archery, e.swords, e.axes, e.acrobatics, e.fishing, e.alchemy, e.crossbows, e.tridents, " + + "c.taming, c.mining, c.repair, c.woodcutting, c.unarmed, c.herbalism, c.excavation, c.archery, c.swords, c.axes, c.acrobatics, c.blast_mining, c.chimaera_wing, c.crossbows, c.tridents, " + + "h.mobhealthbar, h.scoreboardtips, u.uuid, u.`user` " + "FROM " + tablePrefix + "users u " + "JOIN " + tablePrefix + "skills s ON (u.id = s.user_id) " + "JOIN " + tablePrefix + "experience e ON (u.id = e.user_id) " + "JOIN " + tablePrefix + "cooldowns c ON (u.id = c.user_id) " + "JOIN " + tablePrefix + "huds h ON (u.id = h.user_id) " - + "WHERE u.id = ?"); + + "WHERE u.id = ?" + ); statement.setInt(1, id); resultSet = statement.executeQuery(); @@ -640,7 +666,7 @@ public final class SQLDatabaseManager implements DatabaseManager { if (resultSet.next()) { try { PlayerProfile profile = loadFromResult(playerName, resultSet); - String name = resultSet.getString(42); // TODO: Magic Number, make sure it stays updated + String name = resultSet.getString(MAGIC_NUMBER); // TODO: Magic Number, make sure it stays updated resultSet.close(); statement.close(); @@ -650,15 +676,15 @@ public final class SQLDatabaseManager implements DatabaseManager { && uuid != null) { statement = connection.prepareStatement( "UPDATE `" + tablePrefix + "users` " - + "SET user = ? " - + "WHERE user = ?"); + + "SET `user` = ? " + + "WHERE `user` = ?"); statement.setString(1, "_INVALID_OLD_USERNAME_"); statement.setString(2, name); statement.executeUpdate(); statement.close(); statement = connection.prepareStatement( "UPDATE `" + tablePrefix + "users` " - + "SET user = ?, uuid = ? " + + "SET `user` = ?, uuid = ? " + "WHERE id = ?"); statement.setString(1, playerName); statement.setString(2, uuid.toString()); @@ -706,7 +732,7 @@ public final class SQLDatabaseManager implements DatabaseManager { + "JOIN " + tablePrefix + "experience e ON (u.id = e.user_id) " + "JOIN " + tablePrefix + "cooldowns c ON (u.id = c.user_id) " + "JOIN " + tablePrefix + "huds h ON (u.id = h.user_id) " - + "WHERE u.user = ?"); + + "WHERE u.`user` = ?"); List usernames = getStoredUsers(); int convertedUsers = 0; long startMillis = System.currentTimeMillis(); @@ -745,7 +771,7 @@ public final class SQLDatabaseManager implements DatabaseManager { connection = getConnection(PoolIdentifier.MISC); statement = connection.prepareStatement( "UPDATE `" + tablePrefix + "users` SET " - + " uuid = ? WHERE user = ?"); + + " uuid = ? WHERE `user` = ?"); statement.setString(1, uuid.toString()); statement.setString(2, userName); statement.execute(); @@ -769,7 +795,7 @@ public final class SQLDatabaseManager implements DatabaseManager { try { connection = getConnection(PoolIdentifier.MISC); - statement = connection.prepareStatement("UPDATE " + tablePrefix + "users SET uuid = ? WHERE user = ?"); + statement = connection.prepareStatement("UPDATE " + tablePrefix + "users SET uuid = ? WHERE `user` = ?"); for (Map.Entry entry : fetchedUUIDs.entrySet()) { statement.setString(1, entry.getValue().toString()); @@ -811,7 +837,7 @@ public final class SQLDatabaseManager implements DatabaseManager { try { connection = getConnection(PoolIdentifier.MISC); statement = connection.createStatement(); - resultSet = statement.executeQuery("SELECT user FROM " + tablePrefix + "users"); + resultSet = statement.executeQuery("SELECT `user` FROM " + tablePrefix + "users"); while (resultSet.next()) { users.add(resultSet.getString("user")); } @@ -832,7 +858,6 @@ public final class SQLDatabaseManager implements DatabaseManager { * Checks that the database structure is present and correct */ private void checkStructure() { - PreparedStatement statement = null; Statement createStatement = null; ResultSet resultSet = null; @@ -840,27 +865,30 @@ public final class SQLDatabaseManager implements DatabaseManager { try { connection = getConnection(PoolIdentifier.MISC); - statement = connection.prepareStatement("SELECT table_name FROM INFORMATION_SCHEMA.TABLES" - + " WHERE table_schema = ?" - + " AND table_name = ?"); - statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName()); - statement.setString(2, tablePrefix + "users"); + String schemaQuery = this.h2 ? "SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_name = ?" + : "SELECT table_name FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = ? AND table_name = ?"; + + statement = connection.prepareStatement(schemaQuery); + + setStatementQuery(statement, "users"); + resultSet = statement.executeQuery(); + if (!resultSet.next()) { createStatement = connection.createStatement(); - createStatement.executeUpdate("CREATE TABLE IF NOT EXISTS `" + tablePrefix + "users` (" - + "`id` int(10) unsigned NOT NULL AUTO_INCREMENT," - + "`user` varchar(40) NOT NULL," - + "`uuid` varchar(36) NULL DEFAULT NULL," - + "`lastlogin` int(32) unsigned NOT NULL," - + "PRIMARY KEY (`id`)," - + "INDEX(`user`(20) ASC)," - + "UNIQUE KEY `uuid` (`uuid`)) DEFAULT CHARSET=" + CHARSET_SQL + " AUTO_INCREMENT=1;"); + String sql = "CREATE TABLE IF NOT EXISTS `" + tablePrefix + "users` (" + + "`id` int AUTO_INCREMENT," + + "`user` varchar(40) NOT NULL," + + "`uuid` varchar(36)," + + "`lastlogin` bigint NOT NULL," + + "PRIMARY KEY (`id`)," + + "INDEX `user_index`(`user`)," + + "UNIQUE(`uuid`))"; + createStatement.executeUpdate(sql); tryClose(createStatement); } tryClose(resultSet); - statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName()); - statement.setString(2, tablePrefix + "huds"); + setStatementQuery(statement, "huds"); resultSet = statement.executeQuery(); if (!resultSet.next()) { createStatement = connection.createStatement(); @@ -873,8 +901,7 @@ public final class SQLDatabaseManager implements DatabaseManager { tryClose(createStatement); } tryClose(resultSet); - statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName()); - statement.setString(2, tablePrefix + "cooldowns"); + setStatementQuery(statement, "cooldowns"); resultSet = statement.executeQuery(); if (!resultSet.next()) { createStatement = connection.createStatement(); @@ -893,13 +920,14 @@ public final class SQLDatabaseManager implements DatabaseManager { + "`acrobatics` int(32) unsigned NOT NULL DEFAULT '0'," + "`blast_mining` int(32) unsigned NOT NULL DEFAULT '0'," + "`chimaera_wing` int(32) unsigned NOT NULL DEFAULT '0'," + + "`crossbows` int(32) unsigned NOT NULL DEFAULT '0'," + + "`tridents` int(32) unsigned NOT NULL DEFAULT '0'," + "PRIMARY KEY (`user_id`)) " + "DEFAULT CHARSET=" + CHARSET_SQL + ";"); tryClose(createStatement); } tryClose(resultSet); - statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName()); - statement.setString(2, tablePrefix + "skills"); + setStatementQuery(statement, "skills"); resultSet = statement.executeQuery(); if (!resultSet.next()) { String startingLevel = "'" + mcMMO.p.getAdvancedConfig().getStartingLevel() + "'"; @@ -920,14 +948,15 @@ public final class SQLDatabaseManager implements DatabaseManager { + "`acrobatics` int(10) unsigned NOT NULL DEFAULT "+startingLevel+"," + "`fishing` int(10) unsigned NOT NULL DEFAULT "+startingLevel+"," + "`alchemy` int(10) unsigned NOT NULL DEFAULT "+startingLevel+"," + + "`crossbows` int(10) unsigned NOT NULL DEFAULT "+startingLevel+"," + + "`tridents` int(10) unsigned NOT NULL DEFAULT "+startingLevel+"," + "`total` int(10) unsigned NOT NULL DEFAULT "+totalLevel+"," + "PRIMARY KEY (`user_id`)) " + "DEFAULT CHARSET=" + CHARSET_SQL + ";"); tryClose(createStatement); } tryClose(resultSet); - statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName()); - statement.setString(2, tablePrefix + "experience"); + setStatementQuery(statement, "experience"); resultSet = statement.executeQuery(); if (!resultSet.next()) { createStatement = connection.createStatement(); @@ -946,6 +975,8 @@ public final class SQLDatabaseManager implements DatabaseManager { + "`acrobatics` int(10) unsigned NOT NULL DEFAULT '0'," + "`fishing` int(10) unsigned NOT NULL DEFAULT '0'," + "`alchemy` int(10) unsigned NOT NULL DEFAULT '0'," + + "`crossbows` int(10) unsigned NOT NULL DEFAULT '0'," + + "`tridents` int(10) unsigned NOT NULL DEFAULT '0'," + "PRIMARY KEY (`user_id`)) " + "DEFAULT CHARSET=" + CHARSET_SQL + ";"); tryClose(createStatement); @@ -968,7 +999,8 @@ public final class SQLDatabaseManager implements DatabaseManager { } } - mcMMO.p.getLogger().info("Killing orphans"); + // TODO: refactor + LogUtils.debug(logger, "Killing orphans"); createStatement = connection.createStatement(); createStatement.executeUpdate("DELETE FROM `" + tablePrefix + "experience` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "users` `u` WHERE `" + tablePrefix + "experience`.`user_id` = `u`.`id`)"); createStatement.executeUpdate("DELETE FROM `" + tablePrefix + "huds` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "users` `u` WHERE `" + tablePrefix + "huds`.`user_id` = `u`.`id`)"); @@ -985,21 +1017,75 @@ public final class SQLDatabaseManager implements DatabaseManager { tryClose(connection); } + String skills = "skills"; + String crossbows = "crossbows"; + String tridents = "tridents"; + String experience = "experience"; + String cooldowns = "cooldowns"; + + updateStructure(skills, crossbows, String.valueOf(32)); + updateStructure(skills, tridents, String.valueOf(32)); + + updateStructure(experience, crossbows, String.valueOf(10)); + updateStructure(experience, tridents, String.valueOf(10)); + + updateStructure(cooldowns, crossbows, String.valueOf(10)); + updateStructure(cooldowns, tridents, String.valueOf(10)); } - private Connection getConnection(PoolIdentifier identifier) throws SQLException { - Connection connection = null; - switch (identifier) { - case LOAD: - connection = loadPool.getConnection(); - break; - case MISC: - connection = miscPool.getConnection(); - break; - case SAVE: - connection = savePool.getConnection(); - break; + private void updateStructure(String tableName, String columnName, String columnSize) { + try (Connection connection = getConnection(PoolIdentifier.MISC)) { + if (!columnExists(connection, mcMMO.p.getGeneralConfig().getMySQLDatabaseName(), tablePrefix+tableName, columnName)) { + try (Statement createStatement = connection.createStatement()) { + // logger.info("[SQLDB Check] Adding column '" + columnName + "' to table '" + tablePrefix + tableName + "'..."); + String startingLevel = "'" + mcMMO.p.getAdvancedConfig().getStartingLevel() + "'"; + createStatement.executeUpdate("ALTER TABLE `" + tablePrefix + tableName + "` " + + "ADD COLUMN `" + columnName + "` int(" + columnSize + ") unsigned NOT NULL DEFAULT " + startingLevel); + } + } else { + // logger.info("[SQLDB Check] Column '" + columnName + "' already exists in table '" + tablePrefix + tableName + "', looks good!"); + } + } catch (SQLException e) { + e.printStackTrace(); // Consider more robust logging + throw new RuntimeException(e); } + } + + private boolean columnExists(Connection connection, String database, String tableName, String columnName) throws SQLException { + // logger.info("[SQLDB Check] Checking if column '" + columnName + "' exists in table '" + tableName + "'"); + try (Statement createStatement = connection.createStatement()) { + String sql = "SELECT `COLUMN_NAME`\n" + + "FROM `INFORMATION_SCHEMA`.`COLUMNS`\n" + + "WHERE `TABLE_SCHEMA`='" + database + "'\n" + + " AND `TABLE_NAME`='" + tableName + "'\n" + + " AND `COLUMN_NAME`='" + columnName + "'"; + var resultSet = createStatement.executeQuery(sql); + return resultSet.next(); + } catch (SQLException e) { + logger.info("Failed to check if column exists in table " + tableName + " for column " + columnName); + e.printStackTrace(); + throw e; + } + } + + + private void setStatementQuery(PreparedStatement statement, String tableName) throws SQLException { + if (!this.h2) { + // Set schema name for MySQL + statement.setString(1, mcMMO.p.getGeneralConfig().getMySQLDatabaseName()); + statement.setString(2, tablePrefix + tableName); + } else { + // For H2, the schema parameter is not needed + statement.setString(1, tablePrefix + tableName); + } + } + + Connection getConnection(PoolIdentifier identifier) throws SQLException { + Connection connection = switch (identifier) { + case LOAD -> loadPool.getConnection(); + case MISC -> miscPool.getConnection(); + case SAVE -> savePool.getConnection(); + }; if (connection == null) { throw new RuntimeException("getConnection() for " + identifier.name().toLowerCase(Locale.ENGLISH) + " pool timed out. Increase max connections settings."); } @@ -1012,8 +1098,9 @@ public final class SQLDatabaseManager implements DatabaseManager { * @param upgrade Upgrade to attempt to apply */ private void checkDatabaseStructure(Connection connection, UpgradeType upgrade) { + // TODO: Rewrite / Refactor if (!mcMMO.getUpgradeManager().shouldUpgrade(upgrade)) { - LogUtils.debug(mcMMO.p.getLogger(), "Skipping " + upgrade.name() + " upgrade (unneeded)"); + LogUtils.debug(logger, "Skipping " + upgrade.name() + " upgrade (unneeded)"); return; } @@ -1132,9 +1219,9 @@ public final class SQLDatabaseManager implements DatabaseManager { final int OFFSET_SKILLS = 0; // TODO update these numbers when the query // changes (a new skill is added) - final int OFFSET_XP = 13; - final int OFFSET_DATS = 26; - final int OFFSET_OTHER = 39; + final int OFFSET_XP = 15; + final int OFFSET_DATS = 28; + final int OFFSET_OTHER = 41; skills.put(PrimarySkillType.TAMING, result.getInt(OFFSET_SKILLS + 1)); skills.put(PrimarySkillType.MINING, result.getInt(OFFSET_SKILLS + 2)); @@ -1149,6 +1236,8 @@ public final class SQLDatabaseManager implements DatabaseManager { skills.put(PrimarySkillType.ACROBATICS, result.getInt(OFFSET_SKILLS + 11)); skills.put(PrimarySkillType.FISHING, result.getInt(OFFSET_SKILLS + 12)); skills.put(PrimarySkillType.ALCHEMY, result.getInt(OFFSET_SKILLS + 13)); + skills.put(PrimarySkillType.CROSSBOWS, result.getInt(OFFSET_SKILLS + 14)); + skills.put(PrimarySkillType.TRIDENTS, result.getInt(OFFSET_SKILLS + 15)); skillsXp.put(PrimarySkillType.TAMING, result.getFloat(OFFSET_XP + 1)); skillsXp.put(PrimarySkillType.MINING, result.getFloat(OFFSET_XP + 2)); @@ -1163,6 +1252,8 @@ public final class SQLDatabaseManager implements DatabaseManager { skillsXp.put(PrimarySkillType.ACROBATICS, result.getFloat(OFFSET_XP + 11)); skillsXp.put(PrimarySkillType.FISHING, result.getFloat(OFFSET_XP + 12)); skillsXp.put(PrimarySkillType.ALCHEMY, result.getFloat(OFFSET_XP + 13)); + skillsXp.put(PrimarySkillType.CROSSBOWS, result.getFloat(OFFSET_XP + 14)); + skillsXp.put(PrimarySkillType.TRIDENTS, result.getFloat(OFFSET_XP + 15)); // Taming - Unused - result.getInt(OFFSET_DATS + 1) skillsDATS.put(SuperAbilityType.SUPER_BREAKER, result.getInt(OFFSET_DATS + 2)); @@ -1177,6 +1268,8 @@ public final class SQLDatabaseManager implements DatabaseManager { // Acrobatics - Unused - result.getInt(OFFSET_DATS + 11) skillsDATS.put(SuperAbilityType.BLAST_MINING, result.getInt(OFFSET_DATS + 12)); uniqueData.put(UniqueDataType.CHIMAERA_WING_DATS, result.getInt(OFFSET_DATS + 13)); + skillsDATS.put(SuperAbilityType.SUPER_SHOTGUN, result.getInt(OFFSET_DATS + 14)); + skillsDATS.put(SuperAbilityType.TRIDENTS_SUPER_ABILITY, result.getInt(OFFSET_DATS + 15)); try { scoreboardTipsShown = result.getInt(OFFSET_OTHER + 2); @@ -1196,17 +1289,21 @@ public final class SQLDatabaseManager implements DatabaseManager { } private void printErrors(SQLException ex) { - if (debug) { - ex.printStackTrace(); - } + ex.printStackTrace(); - StackTraceElement element = ex.getStackTrace()[0]; - mcMMO.p.getLogger().severe("Location: " + element.getClassName() + " " + element.getMethodName() + " " + element.getLineNumber()); - mcMMO.p.getLogger().severe("SQLException: " + ex.getMessage()); - mcMMO.p.getLogger().severe("SQLState: " + ex.getSQLState()); - mcMMO.p.getLogger().severe("VendorError: " + ex.getErrorCode()); + // logger.severe("SQLException: " + ex.getMessage()); + logger.severe("SQLState: " + ex.getSQLState()); + logger.severe("VendorError: " + ex.getErrorCode()); + + // Handling SQLException chain + SQLException nextException = ex.getNextException(); + while (nextException != null) { + logger.severe("Caused by: " + nextException.getMessage()); + nextException = nextException.getNextException(); + } } + public DatabaseType getDatabaseType() { return DatabaseType.SQL; } @@ -1222,7 +1319,7 @@ public final class SQLDatabaseManager implements DatabaseManager { return; } resultSet.close(); - mcMMO.p.getLogger().info("Updating mcMMO MySQL tables to drop name uniqueness..."); + logger.info("Updating mcMMO MySQL tables to drop name uniqueness..."); statement.execute("ALTER TABLE `" + tablePrefix + "users` " + "DROP INDEX `user`," + "ADD INDEX `user` (`user`(20) ASC)"); @@ -1240,7 +1337,7 @@ public final class SQLDatabaseManager implements DatabaseManager { mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_ALCHEMY); } catch (SQLException ex) { - mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Alchemy..."); + logger.info("Updating mcMMO MySQL tables for Alchemy..."); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD `alchemy` int(10) NOT NULL DEFAULT '0'"); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "experience` ADD `alchemy` int(10) NOT NULL DEFAULT '0'"); } @@ -1252,7 +1349,7 @@ public final class SQLDatabaseManager implements DatabaseManager { mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_BLAST_MINING_COOLDOWN); } catch (SQLException ex) { - mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Blast Mining..."); + logger.info("Updating mcMMO MySQL tables for Blast Mining..."); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "cooldowns` ADD `blast_mining` int(32) NOT NULL DEFAULT '0'"); } } @@ -1263,7 +1360,7 @@ public final class SQLDatabaseManager implements DatabaseManager { mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_UNIQUE_PLAYER_DATA); } catch (SQLException ex) { - mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Chimaera Wing..."); + logger.info("Updating mcMMO MySQL tables for Chimaera Wing..."); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "cooldowns` ADD `chimaera_wing` int(32) NOT NULL DEFAULT '0'"); } } @@ -1274,7 +1371,7 @@ public final class SQLDatabaseManager implements DatabaseManager { mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_FISHING); } catch (SQLException ex) { - mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for Fishing..."); + logger.info("Updating mcMMO MySQL tables for Fishing..."); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD `fishing` int(10) NOT NULL DEFAULT '0'"); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "experience` ADD `fishing` int(10) NOT NULL DEFAULT '0'"); } @@ -1286,7 +1383,7 @@ public final class SQLDatabaseManager implements DatabaseManager { mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_MOB_HEALTHBARS); } catch (SQLException ex) { - mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for mob healthbars..."); + logger.info("Updating mcMMO MySQL tables for mob healthbars..."); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` ADD `mobhealthbar` varchar(50) NOT NULL DEFAULT '" + mcMMO.p.getGeneralConfig().getMobHealthbarDefault() + "'"); } } @@ -1297,7 +1394,7 @@ public final class SQLDatabaseManager implements DatabaseManager { mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.ADD_SCOREBOARD_TIPS); } catch (SQLException ex) { - mcMMO.p.getLogger().info("Updating mcMMO MySQL tables for scoreboard tips..."); + logger.info("Updating mcMMO MySQL tables for scoreboard tips..."); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` ADD `scoreboardtips` int(10) NOT NULL DEFAULT '0' ;"); } } @@ -1310,7 +1407,7 @@ public final class SQLDatabaseManager implements DatabaseManager { resultSet.last(); if (resultSet.getRow() != SkillTools.NON_CHILD_SKILLS.size()) { - mcMMO.p.getLogger().info("Indexing tables, this may take a while on larger databases"); + logger.info("Indexing tables, this may take a while on larger databases"); for (PrimarySkillType skill : SkillTools.NON_CHILD_SKILLS) { String skill_name = skill.name().toLowerCase(Locale.ENGLISH); @@ -1351,7 +1448,7 @@ public final class SQLDatabaseManager implements DatabaseManager { } if (!column_exists) { - mcMMO.p.getLogger().info("Adding UUIDs to mcMMO MySQL user table..."); + logger.info("Adding UUIDs to mcMMO MySQL user table..."); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` ADD `uuid` varchar(36) NULL DEFAULT NULL"); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` ADD UNIQUE INDEX `uuid` (`uuid`) USING BTREE"); @@ -1420,7 +1517,7 @@ public final class SQLDatabaseManager implements DatabaseManager { } if (column_exists) { - mcMMO.p.getLogger().info("Removing party name from users table..."); + logger.info("Removing party name from users table..."); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "users` DROP COLUMN `party`"); } @@ -1454,7 +1551,7 @@ public final class SQLDatabaseManager implements DatabaseManager { } if (!column_exists) { - mcMMO.p.getLogger().info("Adding skill total column to skills table..."); + logger.info("Adding skill total column to skills table..."); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD COLUMN `total` int NOT NULL DEFAULT '0'"); statement.executeUpdate("UPDATE `" + tablePrefix + "skills` SET `total` = (taming+mining+woodcutting+repair+unarmed+herbalism+excavation+archery+swords+axes+acrobatics+fishing+alchemy)"); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "skills` ADD INDEX `idx_total` (`total`) USING BTREE"); @@ -1490,7 +1587,7 @@ public final class SQLDatabaseManager implements DatabaseManager { } if (column_exists) { - mcMMO.p.getLogger().info("Removing Spout HUD type from huds table..."); + logger.info("Removing Spout HUD type from huds table..."); statement.executeUpdate("ALTER TABLE `" + tablePrefix + "huds` DROP COLUMN `hudtype`"); } @@ -1515,7 +1612,7 @@ public final class SQLDatabaseManager implements DatabaseManager { PreparedStatement statement = null; try { - statement = connection.prepareStatement("SELECT id, user FROM " + tablePrefix + "users WHERE uuid = ? OR (uuid IS NULL AND user = ?)"); + statement = connection.prepareStatement("SELECT id, `user` FROM " + tablePrefix + "users WHERE uuid = ? OR (uuid IS NULL AND `user` = ?)"); statement.setString(1, uuid.toString()); statement.setString(2, playerName); resultSet = statement.executeQuery(); @@ -1544,7 +1641,7 @@ public final class SQLDatabaseManager implements DatabaseManager { PreparedStatement statement = null; try { - statement = connection.prepareStatement("SELECT id, user FROM " + tablePrefix + "users WHERE user = ?"); + statement = connection.prepareStatement("SELECT id, `user` FROM " + tablePrefix + "users WHERE `user` = ?"); statement.setString(1, playerName); resultSet = statement.executeQuery(); @@ -1577,7 +1674,7 @@ public final class SQLDatabaseManager implements DatabaseManager { @Override public void onDisable() { - LogUtils.debug(mcMMO.p.getLogger(), "Releasing connection pool resource..."); + LogUtils.debug(logger, "Releasing connection pool resource..."); miscPool.close(); loadPool.close(); savePool.close(); @@ -1619,19 +1716,19 @@ public final class SQLDatabaseManager implements DatabaseManager { */ //Alter users table - mcMMO.p.getLogger().info("SQL Converting tables from latin1 to utf8mb4"); + logger.info("SQL Converting tables from latin1 to utf8mb4"); //Update "user" column try { - mcMMO.p.getLogger().info("Updating user column to new encoding"); + logger.info("Updating user column to new encoding"); statement.executeUpdate(getUpdateUserInUsersTableSQLQuery()); //Update "uuid" column - mcMMO.p.getLogger().info("Updating user column to new encoding"); + logger.info("Updating user column to new encoding"); statement.executeUpdate(getUpdateUUIDInUsersTableSQLQuery()); //Update "mobhealthbar" column - mcMMO.p.getLogger().info("Updating mobhealthbar column to new encoding"); + logger.info("Updating mobhealthbar column to new encoding"); statement.executeUpdate(getUpdateMobHealthBarInHudsTableSQLQuery()); mcMMO.getUpgradeManager().setUpgradeCompleted(UpgradeType.SQL_CHARSET_UTF8MB4); @@ -1645,7 +1742,7 @@ public final class SQLDatabaseManager implements DatabaseManager { private String getUpdateUserInUsersTableSQLQuery() { return "ALTER TABLE\n" + " " + tablePrefix + "users\n" + - " CHANGE user user\n" + + " CHANGE `user` user\n" + " " + USER_VARCHAR + "\n" + " CHARACTER SET utf8mb4\n" + " COLLATE utf8mb4_unicode_ci;"; @@ -1670,4 +1767,28 @@ public final class SQLDatabaseManager implements DatabaseManager { " CHARACTER SET utf8mb4\n" + " COLLATE utf8mb4_unicode_ci;"; } + + public void printAllTablesWithColumns(Connection connection) { + try { + DatabaseMetaData metaData = connection.getMetaData(); + String[] types = {"TABLE"}; + ResultSet tables = metaData.getTables(null, null, "%", types); + + while (tables.next()) { + String tableName = tables.getString("TABLE_NAME"); + System.out.println("Table: " + tableName); + + ResultSet columns = metaData.getColumns(null, null, tableName, "%"); + while (columns.next()) { + String columnName = columns.getString("COLUMN_NAME"); + String columnType = columns.getString("TYPE_NAME"); + System.out.println(" Column: " + columnName + " Type: " + columnType); + } + columns.close(); + } + tables.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } } diff --git a/src/main/java/com/gmail/nossr50/database/flatfile/FlatFileDataUtil.java b/src/main/java/com/gmail/nossr50/database/flatfile/FlatFileDataUtil.java index da41a7acc..285c501fd 100644 --- a/src/main/java/com/gmail/nossr50/database/flatfile/FlatFileDataUtil.java +++ b/src/main/java/com/gmail/nossr50/database/flatfile/FlatFileDataUtil.java @@ -79,6 +79,8 @@ public class FlatFileDataUtil { case SKILLS_TAMING: case SKILLS_FISHING: case SKILLS_ALCHEMY: + case SKILLS_CROSSBOWS: + case SKILLS_TRIDENTS: return String.valueOf(startingLevel); case OVERHAUL_LAST_LOGIN: return String.valueOf(-1L); @@ -90,6 +92,9 @@ public class FlatFileDataUtil { case COOLDOWN_SKULL_SPLITTER: case COOLDOWN_SUPER_BREAKER: case COOLDOWN_BLAST_MINING: + case COOLDOWN_SUPER_SHOTGUN: + case COOLDOWN_TRIDENTS: + case COOLDOWN_ARCHERY: case SCOREBOARD_TIPS: case COOLDOWN_CHIMAERA_WING: case EXP_MINING: @@ -105,6 +110,8 @@ public class FlatFileDataUtil { case EXP_TAMING: case EXP_FISHING: case EXP_ALCHEMY: + case EXP_CROSSBOWS: + case EXP_TRIDENTS: return "0"; case UUID_INDEX: throw new IndexOutOfBoundsException(); //TODO: Add UUID recovery? Might not even be worth it. diff --git a/src/main/java/com/gmail/nossr50/datatypes/party/Party.java b/src/main/java/com/gmail/nossr50/datatypes/party/Party.java index 9b684c742..78ffb9966 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/party/Party.java +++ b/src/main/java/com/gmail/nossr50/datatypes/party/Party.java @@ -6,7 +6,6 @@ import com.gmail.nossr50.datatypes.experience.FormulaType; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.EventUtils; import com.gmail.nossr50.util.Misc; import com.gmail.nossr50.util.sounds.SoundManager; diff --git a/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java b/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java index 80e53ccc7..ea9b09f15 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java +++ b/src/main/java/com/gmail/nossr50/datatypes/player/McMMOPlayer.java @@ -20,7 +20,6 @@ import com.gmail.nossr50.datatypes.skills.ToolType; import com.gmail.nossr50.events.experience.McMMOPlayerPreXpGainEvent; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.party.ShareHandler; import com.gmail.nossr50.runnables.skills.AbilityDisableTask; import com.gmail.nossr50.runnables.skills.ToolLowerTask; @@ -29,7 +28,7 @@ import com.gmail.nossr50.skills.acrobatics.AcrobaticsManager; import com.gmail.nossr50.skills.alchemy.AlchemyManager; import com.gmail.nossr50.skills.archery.ArcheryManager; import com.gmail.nossr50.skills.axes.AxesManager; -import com.gmail.nossr50.skills.child.FamilyTree; +import com.gmail.nossr50.skills.crossbows.CrossbowsManager; import com.gmail.nossr50.skills.excavation.ExcavationManager; import com.gmail.nossr50.skills.fishing.FishingManager; import com.gmail.nossr50.skills.herbalism.HerbalismManager; @@ -39,6 +38,7 @@ import com.gmail.nossr50.skills.salvage.SalvageManager; import com.gmail.nossr50.skills.smelting.SmeltingManager; import com.gmail.nossr50.skills.swords.SwordsManager; import com.gmail.nossr50.skills.taming.TamingManager; +import com.gmail.nossr50.skills.tridents.TridentsManager; import com.gmail.nossr50.skills.unarmed.UnarmedManager; import com.gmail.nossr50.skills.woodcutting.WoodcuttingManager; import com.gmail.nossr50.util.*; @@ -65,10 +65,10 @@ import org.bukkit.plugin.Plugin; import org.checkerframework.checker.nullness.qual.NonNull; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; import java.util.EnumMap; import java.util.Map; -import java.util.Set; import java.util.UUID; public class McMMOPlayer implements Identified { @@ -181,6 +181,9 @@ public class McMMOPlayer implements Identified { case AXES: skillManagers.put(primarySkillType, new AxesManager(this)); break; + case CROSSBOWS: + skillManagers.put(primarySkillType, new CrossbowsManager(this)); + break; case EXCAVATION: skillManagers.put(primarySkillType, new ExcavationManager(this)); break; @@ -208,6 +211,9 @@ public class McMMOPlayer implements Identified { case TAMING: skillManagers.put(primarySkillType, new TamingManager(this)); break; + case TRIDENTS: + skillManagers.put(primarySkillType, new TridentsManager(this)); + break; case UNARMED: skillManagers.put(primarySkillType, new UnarmedManager(this)); break; @@ -227,15 +233,6 @@ public class McMMOPlayer implements Identified { return attackStrength; } -// public void setAttackStrength(double attackStrength) { -// this.attackStrength = attackStrength; -// } - - /*public void hideXpBar(PrimarySkillType primarySkillType) - { - experienceBarManager.hideExperienceBar(primarySkillType); - }*/ - public @NotNull PrimarySkillType getLastSkillShownScoreboard() { return lastSkillShownScoreboard; } @@ -308,6 +305,13 @@ public class McMMOPlayer implements Identified { public AxesManager getAxesManager() { return (AxesManager) skillManagers.get(PrimarySkillType.AXES); } + public CrossbowsManager getCrossbowsManager() { + return (CrossbowsManager) skillManagers.get(PrimarySkillType.CROSSBOWS); + } + + public TridentsManager getTridentsManager() { + return (TridentsManager) skillManagers.get(PrimarySkillType.TRIDENTS); + } public ExcavationManager getExcavationManager() { return (ExcavationManager) skillManagers.get(PrimarySkillType.EXCAVATION); @@ -384,6 +388,7 @@ public class McMMOPlayer implements Identified { * @param isActive True if the ability is active, false otherwise */ public void setAbilityMode(SuperAbilityType ability, boolean isActive) { + // TODO: This should reject "one and done" type abilities abilityMode.put(ability, isActive); } @@ -611,7 +616,7 @@ public class McMMOPlayer implements Identified { } if (SkillTools.isChildSkill(skill)) { - Set parentSkills = FamilyTree.getParents(skill); + var parentSkills = mcMMO.p.getSkillTools().getChildSkillParents(skill); float splitXp = xp / parentSkills.size(); for (PrimarySkillType parentSkill : parentSkills) { @@ -668,7 +673,7 @@ public class McMMOPlayer implements Identified { xp = mcMMOPlayerPreXpGainEvent.getXpGained(); if (SkillTools.isChildSkill(primarySkillType)) { - Set parentSkills = FamilyTree.getParents(primarySkillType); + var parentSkills = mcMMO.p.getSkillTools().getChildSkillParents(primarySkillType); for (PrimarySkillType parentSkill : parentSkills) { applyXpGain(parentSkill, xp / parentSkills.size(), xpGainReason, xpGainSource); @@ -836,14 +841,15 @@ public class McMMOPlayer implements Identified { * @param xp Experience amount to process * @return Modified experience */ - private float modifyXpGain(PrimarySkillType primarySkillType, float xp) { + @VisibleForTesting + float modifyXpGain(PrimarySkillType primarySkillType, float xp) { //TODO: A rare situation can occur where the default Power Level cap can prevent a player with one skill edited to something silly like Integer.MAX_VALUE from gaining XP in any skill, we may need to represent power level with another data type if ((mcMMO.p.getSkillTools().getLevelCap(primarySkillType) <= getSkillLevel(primarySkillType)) || (mcMMO.p.getGeneralConfig().getPowerLevelCap() <= getPowerLevel())) { return 0; } - xp = (float) (xp / ExperienceConfig.getInstance().getFormulaSkillModifier(primarySkillType) * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()); + xp = (float) ((xp * ExperienceConfig.getInstance().getFormulaSkillModifier(primarySkillType)) * ExperienceConfig.getInstance().getExperienceGainsGlobalMultiplier()); if (mcMMO.p.getGeneralConfig().getToolModsEnabled()) { CustomTool tool = mcMMO.getModManager().getTool(player.getInventory().getItemInMainHand()); diff --git a/src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java b/src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java index 65ebf4c85..71abb6418 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java +++ b/src/main/java/com/gmail/nossr50/datatypes/player/PlayerProfile.java @@ -7,14 +7,17 @@ import com.gmail.nossr50.datatypes.skills.PrimarySkillType; import com.gmail.nossr50.datatypes.skills.SuperAbilityType; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.runnables.player.PlayerProfileSaveTask; -import com.gmail.nossr50.skills.child.FamilyTree; import com.gmail.nossr50.util.player.UserManager; import com.gmail.nossr50.util.skills.SkillTools; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.*; +import java.util.EnumMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; import java.util.concurrent.DelayQueue; public class PlayerProfile { @@ -363,7 +366,7 @@ public class PlayerProfile { markProfileDirty(); if (SkillTools.isChildSkill(skill)) { - Set parentSkills = FamilyTree.getParents(skill); + var parentSkills = mcMMO.p.getSkillTools().getChildSkillParents(skill); float dividedXP = (xp / parentSkills.size()); for (PrimarySkillType parentSkill : parentSkills) { @@ -431,8 +434,12 @@ public class PlayerProfile { return mcMMO.getFormulaManager().getXPtoNextLevel(level, formulaType); } - private int getChildSkillLevel(PrimarySkillType primarySkillType) { - Set parents = FamilyTree.getParents(primarySkillType); + private int getChildSkillLevel(@NotNull PrimarySkillType primarySkillType) throws IllegalArgumentException { + if (!SkillTools.isChildSkill(primarySkillType)) { + throw new IllegalArgumentException(primarySkillType + " is not a child skill!"); + } + + ImmutableList parents = mcMMO.p.getSkillTools().getChildSkillParents(primarySkillType); int sum = 0; for (PrimarySkillType parent : parents) { diff --git a/src/main/java/com/gmail/nossr50/datatypes/skills/PrimarySkillType.java b/src/main/java/com/gmail/nossr50/datatypes/skills/PrimarySkillType.java index 5b513c280..9dfecd55b 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/skills/PrimarySkillType.java +++ b/src/main/java/com/gmail/nossr50/datatypes/skills/PrimarySkillType.java @@ -16,6 +16,7 @@ public enum PrimarySkillType { ALCHEMY, ARCHERY, AXES, + CROSSBOWS, EXCAVATION, FISHING, HERBALISM, @@ -25,6 +26,7 @@ public enum PrimarySkillType { SMELTING, SWORDS, TAMING, + TRIDENTS, UNARMED, WOODCUTTING; @@ -130,12 +132,12 @@ public enum PrimarySkillType { /** * WARNING: Being removed in an upcoming update, you should be using mcMMO.getSkillTools() instead * @return the max level of this skill - * @see SkillTools#getXpModifier(com.gmail.nossr50.datatypes.skills.PrimarySkillType) + * @see SkillTools#getXpMultiplier(com.gmail.nossr50.datatypes.skills.PrimarySkillType) * @deprecated this is being removed in an upcoming update, you should be using mcMMO.getSkillTools() instead */ @Deprecated public double getXpModifier() { - return mcMMO.p.getSkillTools().getXpModifier(this); + return mcMMO.p.getSkillTools().getXpMultiplier(this); } /** diff --git a/src/main/java/com/gmail/nossr50/datatypes/skills/SubSkillType.java b/src/main/java/com/gmail/nossr50/datatypes/skills/SubSkillType.java index a11b98eb2..48910bb77 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/skills/SubSkillType.java +++ b/src/main/java/com/gmail/nossr50/datatypes/skills/SubSkillType.java @@ -31,6 +31,11 @@ public enum SubSkillType { AXES_GREATER_IMPACT(1), AXES_SKULL_SPLITTER(1), + /* CROSSBOWS */ + CROSSBOWS_CROSSBOWS_LIMIT_BREAK(10), + CROSSBOWS_TRICK_SHOT(3), + CROSSBOWS_POWERED_SHOT(20), + /* Excavation */ EXCAVATION_ARCHAEOLOGY(8), EXCAVATION_GIGA_DRILL_BREAKER(1), @@ -45,6 +50,7 @@ public enum SubSkillType { /* Herbalism */ HERBALISM_DOUBLE_DROPS(1), + HERBALISM_VERDANT_BOUNTY(1), HERBALISM_FARMERS_DIET(5), HERBALISM_GREEN_TERRA(1), HERBALISM_GREEN_THUMB(4), @@ -57,6 +63,7 @@ public enum SubSkillType { MINING_DEMOLITIONS_EXPERTISE(1), MINING_DOUBLE_DROPS(1), MINING_SUPER_BREAKER(1), + MINING_MOTHER_LODE(1), /* Repair */ REPAIR_ARCANE_FORGING(8), @@ -91,6 +98,10 @@ public enum SubSkillType { TAMING_SHOCK_PROOF(1), TAMING_THICK_FUR(1), + /* Tridents */ + TRIDENTS_IMPALE(10), + TRIDENTS_TRIDENTS_LIMIT_BREAK(10), + /* Unarmed */ UNARMED_ARROW_DEFLECT(1), UNARMED_BERSERK(1), @@ -101,13 +112,11 @@ public enum SubSkillType { UNARMED_UNARMED_LIMIT_BREAK(10), /* Woodcutting */ -/* WOODCUTTING_BARK_SURGEON(3),*/ WOODCUTTING_KNOCK_ON_WOOD(2), WOODCUTTING_HARVEST_LUMBER(1), WOODCUTTING_LEAF_BLOWER(1), -/* WOODCUTTING_NATURES_BOUNTY(3), - WOODCUTTING_SPLINTER(3),*/ - WOODCUTTING_TREE_FELLER(1); + WOODCUTTING_TREE_FELLER(1), + WOODCUTTING_CLEAN_CUTS(1); private final int numRanks; //TODO: SuperAbilityType should also contain flags for active by default? Not sure if it should work that way. @@ -134,7 +143,7 @@ public enum SubSkillType { /** * !!! This relies on the immutable lists in PrimarySkillType being populated !!! * If we add skills, those immutable lists need to be updated - * @return + * @return the parent skill of this subskill */ public PrimarySkillType getParentSkill() { return mcMMO.p.getSkillTools().getPrimarySkillBySubSkill(this); } @@ -204,35 +213,12 @@ public enum SubSkillType { return endResult.toString(); } - public String getWikiName(String subSkillName) { - /* - * Find where to begin our substring (after the prefix) - */ - StringBuilder endResult = new StringBuilder(); - int subStringIndex = getSubStringIndex(subSkillName); - - /* - * Split the string up so we can capitalize each part - */ - String subskillNameWithoutPrefix = subSkillName.substring(subStringIndex); - if(subskillNameWithoutPrefix.contains("_")) - { - String[] splitStrings = subskillNameWithoutPrefix.split("_"); - - for(int i = 0; i < splitStrings.length; i++) - { - if(i+1 >= splitStrings.length) - endResult.append(StringUtils.getCapitalized(splitStrings[i])); - else { - endResult.append(StringUtils.getCapitalized(splitStrings[i])); - endResult.append("_"); - } - } - } else { - endResult.append(StringUtils.getCapitalized(subskillNameWithoutPrefix)); - } - - return endResult.toString(); + public String getWikiUrl() { + // remove the text before the first underscore + int subStringIndex = getSubStringIndex(name()); + String afterPrefix = name().substring(subStringIndex); + // replace _ or spaces with - + return afterPrefix.replace("_", "-").replace(" ", "-").toLowerCase(Locale.ENGLISH); } /** diff --git a/src/main/java/com/gmail/nossr50/datatypes/skills/SuperAbilityType.java b/src/main/java/com/gmail/nossr50/datatypes/skills/SuperAbilityType.java index 6e4de8d78..f876ce53e 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/skills/SuperAbilityType.java +++ b/src/main/java/com/gmail/nossr50/datatypes/skills/SuperAbilityType.java @@ -10,6 +10,12 @@ import org.bukkit.block.BlockState; import org.bukkit.entity.Player; public enum SuperAbilityType { + EXPLOSIVE_SHOT("Archery.Skills.ExplosiveShot.On", + "Archery.Skills.ExplosiveShot.Off", + "Archery.Skills.ExplosiveShot.Other.On", + "Archery.Skills.ExplosiveShot.Refresh", + "Archery.Skills.ExplosiveShot.Other.Off", + "Archery.SubSkill.ExplosiveShot.Name"), BERSERK( "Unarmed.Skills.Berserk.On", "Unarmed.Skills.Berserk.Off", @@ -65,6 +71,20 @@ public enum SuperAbilityType { "Swords.Skills.SS.Refresh", "Swords.Skills.SS.Other.Off", "Swords.SubSkill.SerratedStrikes.Name"), + SUPER_SHOTGUN( + null, + null, + "Crossbows.Skills.SSG.Other.On", + "Crossbows.Skills.SSG.Refresh", + null, + "Crossbows.SubSkill.SuperShotgun.Name"), + TRIDENTS_SUPER_ABILITY( + "Tridents.Skills.TA.On", + "Tridents.Skills.TA.Off", + "Tridents.Skills.TA.Other.On", + "Tridents.Skills.TA.Refresh", + "Tridents.Skills.TA.Other.Off", + "Tridents.SubSkill.TridentAbility.Name"), /** * Has cooldown - but has to share a skill with Super Breaker, so needs special treatment @@ -82,6 +102,7 @@ public enum SuperAbilityType { * Defining their associated SubSkillType definitions * This is a bit of a band-aid fix until the new skill system is in place */ + // TODO: This is stupid static { BERSERK.subSkillTypeDefinition = SubSkillType.UNARMED_BERSERK; SUPER_BREAKER.subSkillTypeDefinition = SubSkillType.MINING_SUPER_BREAKER; @@ -173,35 +194,22 @@ public enum SuperAbilityType { * @param player Player to check permissions for * @return true if the player has permissions, false otherwise */ + // TODO: Add unit tests + // TODO: This is stupid public boolean getPermissions(Player player) { - switch (this) { - case BERSERK: - return Permissions.berserk(player); - - case BLAST_MINING: - return Permissions.remoteDetonation(player); - - case GIGA_DRILL_BREAKER: - return Permissions.gigaDrillBreaker(player); - - case GREEN_TERRA: - return Permissions.greenTerra(player); - - case SERRATED_STRIKES: - return Permissions.serratedStrikes(player); - - case SKULL_SPLITTER: - return Permissions.skullSplitter(player); - - case SUPER_BREAKER: - return Permissions.superBreaker(player); - - case TREE_FELLER: - return Permissions.treeFeller(player); - - default: - return false; - } + return switch (this) { + case BERSERK -> Permissions.berserk(player); + case EXPLOSIVE_SHOT -> Permissions.explosiveShot(player); + case BLAST_MINING -> Permissions.remoteDetonation(player); + case GIGA_DRILL_BREAKER -> Permissions.gigaDrillBreaker(player); + case GREEN_TERRA -> Permissions.greenTerra(player); + case SERRATED_STRIKES -> Permissions.serratedStrikes(player); + case SKULL_SPLITTER -> Permissions.skullSplitter(player); + case SUPER_BREAKER -> Permissions.superBreaker(player); + case SUPER_SHOTGUN -> Permissions.superShotgun(player); + case TREE_FELLER -> Permissions.treeFeller(player); + case TRIDENTS_SUPER_ABILITY -> Permissions.tridentsSuper(player); + }; } /** @@ -211,25 +219,15 @@ public enum SuperAbilityType { * @return true if the block is affected by this ability, false otherwise */ public boolean blockCheck(BlockState blockState) { - switch (this) { - case BERSERK: - return (BlockUtils.affectedByGigaDrillBreaker(blockState) || blockState.getType() == Material.SNOW || mcMMO.getMaterialMapStore().isGlass(blockState.getType())); - - case GIGA_DRILL_BREAKER: - return BlockUtils.affectedByGigaDrillBreaker(blockState); - - case GREEN_TERRA: - return BlockUtils.canMakeMossy(blockState); - - case SUPER_BREAKER: - return BlockUtils.affectedBySuperBreaker(blockState); - - case TREE_FELLER: - return BlockUtils.hasWoodcuttingXP(blockState); - - default: - return false; - } + return switch (this) { + case BERSERK -> + (BlockUtils.affectedByGigaDrillBreaker(blockState) || blockState.getType() == Material.SNOW || mcMMO.getMaterialMapStore().isGlass(blockState.getType())); + case GIGA_DRILL_BREAKER -> BlockUtils.affectedByGigaDrillBreaker(blockState); + case GREEN_TERRA -> BlockUtils.canMakeMossy(blockState); + case SUPER_BREAKER -> BlockUtils.affectedBySuperBreaker(blockState); + case TREE_FELLER -> BlockUtils.hasWoodcuttingXP(blockState); + default -> false; + }; } /** diff --git a/src/main/java/com/gmail/nossr50/datatypes/skills/ToolType.java b/src/main/java/com/gmail/nossr50/datatypes/skills/ToolType.java index 9fdb444e9..dd61867d5 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/skills/ToolType.java +++ b/src/main/java/com/gmail/nossr50/datatypes/skills/ToolType.java @@ -10,7 +10,10 @@ public enum ToolType { HOE("Herbalism.Ability.Lower", "Herbalism.Ability.Ready"), PICKAXE("Mining.Ability.Lower", "Mining.Ability.Ready"), SHOVEL("Excavation.Ability.Lower", "Excavation.Ability.Ready"), - SWORD("Swords.Ability.Lower", "Swords.Ability.Ready"); + SWORD("Swords.Ability.Lower", "Swords.Ability.Ready"), + CROSSBOW("Crossbows.Ability.Lower", "Crossbows.Ability.Ready"), + BOW("Archery.Ability.Lower", "Archery.Ability.Ready"), + TRIDENTS("Tridents.Ability.Lower", "Tridents.Ability.Ready"); private final String lowerTool; private final String raiseTool; @@ -38,6 +41,10 @@ public enum ToolType { switch (this) { case AXE: return ItemUtils.isAxe(itemStack); + case CROSSBOW: + return ItemUtils.isCrossbow(itemStack); + case TRIDENTS: + return ItemUtils.isTrident(itemStack); case FISTS: return itemStack.getType() == Material.AIR; diff --git a/src/main/java/com/gmail/nossr50/datatypes/skills/subskills/acrobatics/Roll.java b/src/main/java/com/gmail/nossr50/datatypes/skills/subskills/acrobatics/Roll.java index 64a3a14fb..15ea49679 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/skills/subskills/acrobatics/Roll.java +++ b/src/main/java/com/gmail/nossr50/datatypes/skills/subskills/acrobatics/Roll.java @@ -14,11 +14,10 @@ import com.gmail.nossr50.util.ItemUtils; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.NotificationManager; import com.gmail.nossr50.util.player.UserManager; -import com.gmail.nossr50.util.random.RandomChanceSkill; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.Probability; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.PerksUtils; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.skills.SkillUtils; import com.gmail.nossr50.util.sounds.SoundManager; import com.gmail.nossr50.util.sounds.SoundType; @@ -33,6 +32,8 @@ import org.bukkit.event.Event; import org.bukkit.event.EventPriority; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; import java.util.Locale; @@ -110,7 +111,7 @@ public class Roll extends AcrobaticsSubSkill { */ @Override public boolean hasPermission(Player player) { - return Permissions.isSubSkillEnabled(player, this); + return Permissions.isSubSkillEnabled(player, getSubSkillType()); } /** @@ -128,14 +129,16 @@ public class Roll extends AcrobaticsSubSkill { float skillValue = playerProfile.getSkillLevel(getPrimarySkill()); boolean isLucky = Permissions.lucky(player, getPrimarySkill()); - String[] rollStrings = RandomChanceUtil.calculateAbilityDisplayValues(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ACROBATICS_ROLL); + String[] rollStrings = ProbabilityUtil.getRNGDisplayValues(player, SubSkillType.ACROBATICS_ROLL); rollChance = rollStrings[0]; rollChanceLucky = rollStrings[1]; /* * Graceful is double the odds of a normal roll */ - String[] gracefulRollStrings = RandomChanceUtil.calculateAbilityDisplayValuesCustom(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, player, SubSkillType.ACROBATICS_ROLL, 2.0D); + Probability probability = getRollProbability(player); + Probability gracefulProbability = Probability.ofPercent(probability.getValue() * 2); + String[] gracefulRollStrings = ProbabilityUtil.getRNGDisplayValues(gracefulProbability); gracefulRollChance = gracefulRollStrings[0]; gracefulRollChanceLucky = gracefulRollStrings[1]; @@ -166,6 +169,11 @@ public class Roll extends AcrobaticsSubSkill { } + @NotNull + private Probability getRollProbability(Player player) { + return ProbabilityUtil.getSubSkillProbability(SubSkillType.ACROBATICS_ROLL, player); + } + @Override public boolean isSuperAbility() { return false; @@ -191,7 +199,8 @@ public class Roll extends AcrobaticsSubSkill { * @param damage The amount of damage initially dealt by the event * @return the modified event damage if the ability was successful, the original event damage otherwise */ - private double rollCheck(Player player, McMMOPlayer mcMMOPlayer, double damage) { + @VisibleForTesting + public double rollCheck(Player player, McMMOPlayer mcMMOPlayer, double damage) { int skillLevel = mcMMOPlayer.getSkillLevel(getPrimarySkill()); @@ -202,7 +211,7 @@ public class Roll extends AcrobaticsSubSkill { double modifiedDamage = calculateModifiedRollDamage(damage, mcMMO.p.getAdvancedConfig().getRollDamageThreshold()); if (!isFatal(player, modifiedDamage) - && RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.ACROBATICS_ROLL, player)) { + && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.ACROBATICS_ROLL, player)) { NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Acrobatics.Roll.Text"); SoundManager.sendCategorizedSound(player, player.getLocation(), SoundType.ROLL_ACTIVATED, SoundCategory.PLAYERS); //player.sendMessage(LocaleLoader.getString("Acrobatics.Roll.Text")); @@ -239,11 +248,11 @@ public class Roll extends AcrobaticsSubSkill { private double gracefulRollCheck(Player player, McMMOPlayer mcMMOPlayer, double damage, int skillLevel) { double modifiedDamage = calculateModifiedRollDamage(damage, mcMMO.p.getAdvancedConfig().getRollDamageThreshold() * 2); - RandomChanceSkill rcs = new RandomChanceSkill(player, subSkillType); - rcs.setSkillLevel(rcs.getSkillLevel() * 2); //Double the effective odds + Probability gracefulProbability = getGracefulProbability(player); if (!isFatal(player, modifiedDamage) - && RandomChanceUtil.checkRandomChanceExecutionSuccess(rcs)) + //TODO: Graceful isn't sending out an event + && ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.ACROBATICS, player, gracefulProbability)) { NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE, "Acrobatics.Ability.Proc"); SoundManager.sendCategorizedSound(player, player.getLocation(), SoundType.ROLL_ACTIVATED, SoundCategory.PLAYERS,0.5F); @@ -263,6 +272,12 @@ public class Roll extends AcrobaticsSubSkill { return damage; } + @NotNull + public static Probability getGracefulProbability(Player player) { + double gracefulOdds = ProbabilityUtil.getSubSkillProbability(SubSkillType.ACROBATICS_ROLL, player).getValue() * 2; + return Probability.ofPercent(gracefulOdds); + } + /** * Check if the player is "farming" Acrobatics XP using * exploits in the game. @@ -412,28 +427,21 @@ public class Roll extends AcrobaticsSubSkill { @Override public Double[] getStats(Player player) { - double playerChanceRoll, playerChanceGrace; + double playerChanceRoll = ProbabilityUtil.getSubSkillProbability(subSkillType, player).getValue(); + double playerChanceGrace = playerChanceRoll * 2; - RandomChanceSkill roll = new RandomChanceSkill(player, getSubSkillType()); - RandomChanceSkill graceful = new RandomChanceSkill(player, getSubSkillType()); - - graceful.setSkillLevel(graceful.getSkillLevel() * 2); //Double odds - - //Calculate - playerChanceRoll = RandomChanceUtil.getRandomChanceExecutionChance(roll); - playerChanceGrace = RandomChanceUtil.getRandomChanceExecutionChance(graceful); + double gracefulOdds = ProbabilityUtil.getSubSkillProbability(subSkillType, player).getValue() * 2; return new Double[]{ playerChanceRoll, playerChanceGrace }; } - public void addFallLocation(Player player) + public void addFallLocation(@NotNull Player player) { UserManager.getPlayer(player).getAcrobaticsManager().addLocationToFallMap(getBlockLocation(player)); } - public Location getBlockLocation(Player player) + public @NotNull Location getBlockLocation(@NotNull Player player) { return player.getLocation().getBlock().getLocation(); } - } diff --git a/src/main/java/com/gmail/nossr50/datatypes/treasure/Treasure.java b/src/main/java/com/gmail/nossr50/datatypes/treasure/Treasure.java index 013849de2..659a94fc1 100644 --- a/src/main/java/com/gmail/nossr50/datatypes/treasure/Treasure.java +++ b/src/main/java/com/gmail/nossr50/datatypes/treasure/Treasure.java @@ -1,25 +1,34 @@ package com.gmail.nossr50.datatypes.treasure; +import com.gmail.nossr50.util.random.Probability; +import com.google.common.base.Objects; import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; public abstract class Treasure { private int xp; private double dropChance; + private @NotNull Probability dropProbability; private int dropLevel; - private ItemStack drop; + private @NotNull ItemStack drop; public Treasure(ItemStack drop, int xp, double dropChance, int dropLevel) { this.drop = drop; this.xp = xp; this.dropChance = dropChance; + this.dropProbability = Probability.ofPercent(dropChance); this.dropLevel = dropLevel; } - public ItemStack getDrop() { + public @NotNull Probability getDropProbability() { + return dropProbability; + } + + public @NotNull ItemStack getDrop() { return drop; } - public void setDrop(ItemStack drop) { + public void setDrop(@NotNull ItemStack drop) { this.drop = drop; } @@ -35,8 +44,9 @@ public abstract class Treasure { return dropChance; } - public void setDropChance(Double dropChance) { + public void setDropChance(double dropChance) { this.dropChance = dropChance; + this.dropProbability = Probability.ofPercent(dropChance); } public int getDropLevel() { @@ -46,4 +56,28 @@ public abstract class Treasure { public void setDropLevel(int dropLevel) { this.dropLevel = dropLevel; } + + @Override + public String toString() { + return "Treasure{" + + "xp=" + xp + + ", dropChance=" + dropChance + + ", dropProbability=" + dropProbability + + ", dropLevel=" + dropLevel + + ", drop=" + drop + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Treasure treasure = (Treasure) o; + return xp == treasure.xp && Double.compare(treasure.dropChance, dropChance) == 0 && dropLevel == treasure.dropLevel && Objects.equal(dropProbability, treasure.dropProbability) && Objects.equal(drop, treasure.drop); + } + + @Override + public int hashCode() { + return Objects.hashCode(xp, dropChance, dropProbability, dropLevel, drop); + } } diff --git a/src/main/java/com/gmail/nossr50/events/skills/McMMOPlayerSkillEvent.java b/src/main/java/com/gmail/nossr50/events/skills/McMMOPlayerSkillEvent.java index e899ea528..6238886c0 100644 --- a/src/main/java/com/gmail/nossr50/events/skills/McMMOPlayerSkillEvent.java +++ b/src/main/java/com/gmail/nossr50/events/skills/McMMOPlayerSkillEvent.java @@ -11,10 +11,10 @@ import org.jetbrains.annotations.NotNull; * Generic event for mcMMO skill handling. */ public abstract class McMMOPlayerSkillEvent extends PlayerEvent { - protected PrimarySkillType skill; + protected @NotNull PrimarySkillType skill; protected int skillLevel; - protected McMMOPlayerSkillEvent(Player player, PrimarySkillType skill) { + protected McMMOPlayerSkillEvent(@NotNull Player player, @NotNull PrimarySkillType skill) { super(player); this.skill = skill; this.skillLevel = UserManager.getPlayer(player).getSkillLevel(skill); @@ -23,7 +23,7 @@ public abstract class McMMOPlayerSkillEvent extends PlayerEvent { /** * @return The skill involved in this event */ - public PrimarySkillType getSkill() { + public @NotNull PrimarySkillType getSkill() { return skill; } diff --git a/src/main/java/com/gmail/nossr50/events/skills/secondaryabilities/SubSkillEvent.java b/src/main/java/com/gmail/nossr50/events/skills/secondaryabilities/SubSkillEvent.java index 7aefb003e..5445bbfcd 100644 --- a/src/main/java/com/gmail/nossr50/events/skills/secondaryabilities/SubSkillEvent.java +++ b/src/main/java/com/gmail/nossr50/events/skills/secondaryabilities/SubSkillEvent.java @@ -16,9 +16,7 @@ public class SubSkillEvent extends McMMOPlayerSkillEvent implements Cancellable * Only skills using the old system will fire this event * @param player target player * @param subSkillType target subskill - * @Deprecated Skills will be using a new system stemming from the AbstractSubSkill class so make sure you check for both events, this event will be removed eventually. */ - @Deprecated public SubSkillEvent(Player player, SubSkillType subSkillType) { super(player, mcMMO.p.getSkillTools().getPrimarySkillBySubSkill(subSkillType)); this.subSkillType = subSkillType; @@ -29,9 +27,7 @@ public class SubSkillEvent extends McMMOPlayerSkillEvent implements Cancellable * @param player target player * @param subSkillType target subskill * @param resultModifier a value multiplied against the final result of the dice roll, typically between 0-1.0 - * @Deprecated Skills will be using a new system stemming from the AbstractSubSkill class so make sure you check for both events, this event will be removed eventually. */ - @Deprecated public SubSkillEvent(Player player, SubSkillType subSkillType, double resultModifier) { super(player, mcMMO.p.getSkillTools().getPrimarySkillBySubSkill(subSkillType)); this.subSkillType = subSkillType; diff --git a/src/main/java/com/gmail/nossr50/events/skills/secondaryabilities/SubSkillRandomCheckEvent.java b/src/main/java/com/gmail/nossr50/events/skills/secondaryabilities/SubSkillRandomCheckEvent.java index c1a90a425..3756c6cdd 100644 --- a/src/main/java/com/gmail/nossr50/events/skills/secondaryabilities/SubSkillRandomCheckEvent.java +++ b/src/main/java/com/gmail/nossr50/events/skills/secondaryabilities/SubSkillRandomCheckEvent.java @@ -1,47 +1,41 @@ -package com.gmail.nossr50.events.skills.secondaryabilities; - -import com.gmail.nossr50.datatypes.skills.SubSkillType; -import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill; -import org.bukkit.entity.Player; - -public class SubSkillRandomCheckEvent extends SubSkillEvent { - private double chance; - - public SubSkillRandomCheckEvent(Player player, SubSkillType ability, double chance) { - super(player, ability); - this.chance = chance; - } - - public SubSkillRandomCheckEvent(Player player, AbstractSubSkill abstractSubSkill, double chance) - { - super(player, abstractSubSkill); - this.chance = chance; - } - - /** - * Gets the activation chance of the ability 0D being no chance, 100.0D being 100% chance - * - * @return The activation chance of the ability - */ - public double getChance() { - return chance; - } - - /** - * Sets the activation chance of the ability [0D-100.0D] - * - * @param chance The activation chance of the ability - */ - public void setChance(double chance) { - this.chance = chance; - } - - /** - * Sets the activation chance of the ability to 100% or 0% - * - * @param success whether it should be successful or not - */ - public void setSuccessful(boolean success) { - this.chance = success ? 100.0D : 0D; - } -} +//package com.gmail.nossr50.events.skills.secondaryabilities; +// +//import com.gmail.nossr50.datatypes.skills.SubSkillType; +//import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill; +//import org.bukkit.entity.Player; +// +//public class SubSkillRandomCheckEvent extends SubSkillEvent { +// private double chance; +// +// public SubSkillRandomCheckEvent(Player player, SubSkillType ability, double chance) { +// super(player, ability); +// this.chance = chance; +// } +// +// /** +// * Gets the activation chance of the ability 0D being no chance, 100.0D being 100% chance +// * +// * @return The activation chance of the ability +// */ +// public double getChance() { +// return chance; +// } +// +// /** +// * Sets the activation chance of the ability [0D-100.0D] +// * +// * @param chance The activation chance of the ability +// */ +// public void setChance(double chance) { +// this.chance = chance; +// } +// +// /** +// * Sets the activation chance of the ability to 100% or 0% +// * +// * @param success whether it should be successful or not +// */ +// public void setSuccessful(boolean success) { +// this.chance = success ? 100.0D : 0D; +// } +//} diff --git a/src/main/java/com/gmail/nossr50/listeners/BlockListener.java b/src/main/java/com/gmail/nossr50/listeners/BlockListener.java index c49c759c9..63585b917 100644 --- a/src/main/java/com/gmail/nossr50/listeners/BlockListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/BlockListener.java @@ -417,7 +417,7 @@ public class BlockListener implements Listener { woodcuttingManager.processWoodcuttingBlockXP(blockState); //Check for bonus drops - woodcuttingManager.processHarvestLumber(blockState); + woodcuttingManager.processBonusDropCheck(blockState); } } diff --git a/src/main/java/com/gmail/nossr50/listeners/CommandListener.java b/src/main/java/com/gmail/nossr50/listeners/CommandListener.java deleted file mode 100644 index 484faa5bb..000000000 --- a/src/main/java/com/gmail/nossr50/listeners/CommandListener.java +++ /dev/null @@ -1,40 +0,0 @@ -//package com.gmail.nossr50.listeners; -// -//import com.gmail.nossr50.datatypes.player.McMMOPlayer; -//import com.gmail.nossr50.datatypes.skills.SuperAbilityType; -//import com.gmail.nossr50.mcMMO; -//import com.gmail.nossr50.util.player.UserManager; -//import com.gmail.nossr50.util.skills.SkillUtils; -//import org.bukkit.Bukkit; -//import org.bukkit.entity.Player; -//import org.bukkit.event.EventHandler; -//import org.bukkit.event.EventPriority; -//import org.bukkit.event.Listener; -//import org.bukkit.event.player.PlayerCommandPreprocessEvent; -// -//public class CommandListener implements Listener { -// -// private final mcMMO pluginRef; -// -// public CommandListener(mcMMO plugin) { -// this.pluginRef = plugin; -// } -// -// @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) -// public void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { -// Player player = event.getPlayer(); -// -// SkillUtils.removeAbilityBoostsFromInventory(player); -// -// McMMOPlayer mmoPlayer = UserManager.getPlayer(player); -// -// if(mmoPlayer == null) -// return; -// -// Bukkit.getServer().getScheduler().runTaskLater(pluginRef, () -> { -// if(mmoPlayer.getAbilityMode(SuperAbilityType.GIGA_DRILL_BREAKER) || mmoPlayer.getAbilityMode(SuperAbilityType.SUPER_BREAKER)) { -// SkillUtils.handleAbilitySpeedIncrease(player); -// } -// }, 5); -// } -//} diff --git a/src/main/java/com/gmail/nossr50/listeners/EntityListener.java b/src/main/java/com/gmail/nossr50/listeners/EntityListener.java index c762e9436..18edee783 100644 --- a/src/main/java/com/gmail/nossr50/listeners/EntityListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/EntityListener.java @@ -11,6 +11,7 @@ import com.gmail.nossr50.metadata.MobMetaFlagType; import com.gmail.nossr50.metadata.MobMetadataService; import com.gmail.nossr50.runnables.TravelingBlockMetaCleanup; import com.gmail.nossr50.skills.archery.Archery; +import com.gmail.nossr50.skills.crossbows.Crossbows; import com.gmail.nossr50.skills.mining.BlastMining; import com.gmail.nossr50.skills.mining.MiningManager; import com.gmail.nossr50.skills.taming.Taming; @@ -19,9 +20,8 @@ import com.gmail.nossr50.skills.unarmed.UnarmedManager; import com.gmail.nossr50.util.*; import com.gmail.nossr50.util.player.NotificationManager; import com.gmail.nossr50.util.player.UserManager; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.CombatUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.worldguard.WorldGuardManager; import com.gmail.nossr50.worldguard.WorldGuardUtils; import org.bukkit.ChatColor; @@ -62,30 +62,6 @@ public class EntityListener implements Listener { mobMetadataService = mcMMO.getMetadataService().getMobMetadataService(); } -// @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) -// public void onBlockDropItemEvent(EntityDropItemEvent event) { -// if(event.getEntity() instanceof Block) { -// Block itemDispensingBlock = (Block) event.getEntity(); -// -// //Is it a berry bush? -// if(itemDispensingBlock.getType().toString().equalsIgnoreCase("sweet_berry_bush")) { -// //Berry Bush Time! -// if (event.getEntity().getMetadata(mcMMO.BONUS_DROPS_METAKEY).size() > 0) { -// Bukkit.broadcastMessage("Pop pop!"); -// BonusDropMeta bonusDropMeta = (BonusDropMeta) event.getEntity().getMetadata(mcMMO.BONUS_DROPS_METAKEY).get(0); -// int bonusCount = bonusDropMeta.asInt(); -// -// for (int i = 0; i < bonusCount; i++) { -// Misc.spawnItemNaturally(event.getEntity().getLocation(), event.getItemDrop().getItemStack(), ItemSpawnReason.BONUS_DROPS); -// } -// } -// } -// -// if(event.getEntity().hasMetadata(mcMMO.BONUS_DROPS_METAKEY)) -// event.getEntity().removeMetadata(mcMMO.BONUS_DROPS_METAKEY, pluginRef); -// } -// } - @EventHandler(priority = EventPriority.MONITOR) public void onEntityTransform(EntityTransformEvent event) { if(event.getEntity() instanceof LivingEntity livingEntity) { @@ -124,7 +100,7 @@ public class EntityListener implements Listener { } } - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = false) public void onEntityShootBow(EntityShootBowEvent event) { /* WORLD BLACKLIST CHECK */ if(WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld())) @@ -132,32 +108,28 @@ public class EntityListener implements Listener { if(event.getEntity() instanceof Player player) { - - /* WORLD GUARD MAIN FLAG CHECK */ - if(WorldGuardUtils.isWorldGuardLoaded()) - { - if(!WorldGuardManager.getInstance().hasMainFlag(player)) - return; - } - Entity projectile = event.getProjectile(); //Should be noted that there are API changes regarding Arrow from 1.13.2 to current versions of the game - if (!(projectile instanceof Arrow)) { + if (!(projectile instanceof Arrow arrow)) { return; } ItemStack bow = event.getBow(); - if (bow != null - && bow.containsEnchantment(Enchantment.ARROW_INFINITE)) { + if (bow == null) + return; + + if (bow.containsEnchantment(Enchantment.ARROW_INFINITE)) { projectile.setMetadata(MetadataConstants.METADATA_KEY_INF_ARROW, MetadataConstants.MCMMO_METADATA_VALUE); } + // Set BowType, Force, and Distance metadata projectile.setMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE, new FixedMetadataValue(pluginRef, Math.min(event.getForce() * mcMMO.p.getAdvancedConfig().getForceMultiplier(), 1.0))); - projectile.setMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE, new FixedMetadataValue(pluginRef, projectile.getLocation())); + projectile.setMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE, new FixedMetadataValue(pluginRef, arrow.getLocation())); + //Cleanup metadata in 1 minute in case normal collection falls through - CombatUtils.delayArrowMetaCleanup((Projectile) projectile); + CombatUtils.delayArrowMetaCleanup(arrow); } } @@ -176,25 +148,28 @@ public class EntityListener implements Listener { return; } - Projectile projectile = event.getEntity(); - EntityType entityType = projectile.getType(); + if(event.getEntity() instanceof Arrow arrow) { + // Delayed metadata cleanup in case other cleanup hooks fail + CombatUtils.delayArrowMetaCleanup(arrow); - if(entityType == EntityType.ARROW || entityType == EntityType.SPECTRAL_ARROW) { - CombatUtils.delayArrowMetaCleanup(projectile); //Cleans up metadata 1 minute from now in case other collection methods fall through + // If fired from an item with multi-shot, we need to track + if(ItemUtils.doesPlayerHaveEnchantmentInHands(player, "multishot")) { + arrow.setMetadata(MetadataConstants.METADATA_KEY_MULTI_SHOT_ARROW, MetadataConstants.MCMMO_METADATA_VALUE); + } - if(!projectile.hasMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE)) - projectile.setMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE, new FixedMetadataValue(pluginRef, 1.0)); + if(!arrow.hasMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE)) + arrow.setMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE, new FixedMetadataValue(pluginRef, 1.0)); - if(!projectile.hasMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE)) - projectile.setMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE, new FixedMetadataValue(pluginRef, projectile.getLocation())); + if(!arrow.hasMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE)) + arrow.setMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE, new FixedMetadataValue(pluginRef, arrow.getLocation())); //Check both hands if(ItemUtils.doesPlayerHaveEnchantmentInHands(player, "piercing")) { return; } - if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.ARCHERY_ARROW_RETRIEVAL, player)) { - projectile.setMetadata(MetadataConstants.METADATA_KEY_TRACKED_ARROW, MetadataConstants.MCMMO_METADATA_VALUE); + if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.ARCHERY_ARROW_RETRIEVAL, player)) { + arrow.setMetadata(MetadataConstants.METADATA_KEY_TRACKED_ARROW, MetadataConstants.MCMMO_METADATA_VALUE); } } } @@ -424,8 +399,8 @@ public class EntityListener implements Listener { } } - if(entityDamageEvent.getDamager() instanceof Projectile) { - CombatUtils.cleanupArrowMetadata((Projectile) entityDamageEvent.getDamager()); + if(entityDamageEvent.getDamager() instanceof Arrow arrow) { + CombatUtils.delayArrowMetaCleanup(arrow); } if(entityDamageEvent.getEntity() instanceof Player player && entityDamageEvent.getDamager() instanceof Player) { @@ -1115,5 +1090,16 @@ public class EntityListener implements Listener { } } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onProjectileHitEvent(ProjectileHitEvent event) { + /* WORLD BLACKLIST CHECK */ + if (WorldBlacklist.isWorldBlacklisted(event.getEntity().getWorld())) + return; + if(event.getEntity() instanceof Arrow arrow) { + if(arrow.isShotFromCrossbow()) { + Crossbows.processCrossbows(event, pluginRef, arrow); + } + } + } } diff --git a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java index a63b7650e..ab1526382 100644 --- a/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/PlayerListener.java @@ -787,6 +787,9 @@ public class PlayerListener implements Listener { } McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); + if (mcMMOPlayer == null) + return; + ItemStack heldItem = player.getInventory().getItemInMainHand(); //Spam Fishing Detection diff --git a/src/main/java/com/gmail/nossr50/locale/LocaleLoader.java b/src/main/java/com/gmail/nossr50/locale/LocaleLoader.java index 20b5e2631..e6a37d924 100644 --- a/src/main/java/com/gmail/nossr50/locale/LocaleLoader.java +++ b/src/main/java/com/gmail/nossr50/locale/LocaleLoader.java @@ -16,6 +16,8 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public final class LocaleLoader { private static final String BUNDLE_ROOT = "com.gmail.nossr50.locale.locale"; @@ -24,6 +26,9 @@ public final class LocaleLoader { private static ResourceBundle bundle = null; private static ResourceBundle filesystemBundle = null; private static ResourceBundle enBundle = null; + // Matches the pattern &#RRGGBB + private static final Pattern hexPattern = Pattern.compile("&#([A-Fa-f0-9]{6})"); + private static final Pattern minecraftHexPattern = Pattern.compile("§x(§[A-Fa-f0-9])(§[A-Fa-f0-9])(§[A-Fa-f0-9])(§[A-Fa-f0-9])(§[A-Fa-f0-9])(§[A-Fa-f0-9])"); private LocaleLoader() {} @@ -48,8 +53,6 @@ public final class LocaleLoader { return formatString(rawMessage, messageArguments); } - //TODO: Remove this hacky crap with something better later - /** * Gets the appropriate TextComponent representation of a formatted string from the Locale files. * @@ -258,9 +261,14 @@ public final class LocaleLoader { @NotNull private static String getExamples() { return """ - This.Is.An.Example.Put.Locale.Keys.Here.One=&aExample text using hex color codes + This.Is.An.Example.Put.Locale.Keys.Here.One=&aExample text using simplified minecraft color codes This.Is.An.Example.Put.Locale.Keys.Here.Two=[[DARK_AQUA]]Example text using our own color codes This.Is.An.Example.Put.Locale.Keys.Here.Three=Example text with no colors + This.Is.An.Example.Put.Locale.Keys.Here.Four=&#FF0000Example text with red color hex code + This.Is.An.Example.Put.Locale.Keys.Here.Five=�FF00Example text with green color hex code + This.Is.An.Example.Put.Locale.Keys.Here.Six=�FFExample text with blue color hex code + This.Is.An.Example.Put.Locale.Keys.Here.Seven=&#FFFF00Example text with yellow color hex code + This.Is.An.Example.Put.Locale.Keys.Here.Eight=&lExample text with bold using simplified minecraft color codes """; } @@ -304,6 +312,10 @@ public final class LocaleLoader { } public static String addColors(String input) { + // First check for hex color codes and insert them + input = translateHexColorCodes(input); + + // Then check for our own color codes input = input.replaceAll("\\Q[[BLACK]]\\E", ChatColor.BLACK.toString()); input = input.replaceAll("\\Q[[DARK_BLUE]]\\E", ChatColor.DARK_BLUE.toString()); input = input.replaceAll("\\Q[[DARK_GREEN]]\\E", ChatColor.DARK_GREEN.toString()); @@ -327,6 +339,7 @@ public final class LocaleLoader { input = input.replaceAll("\\Q[[MAGIC]]\\E", ChatColor.MAGIC.toString()); input = input.replaceAll("\\Q[[RESET]]\\E", ChatColor.RESET.toString()); + // Then check for the typical color codes input = input.replaceAll("\\Q&0\\E", ChatColor.BLACK.toString()); input = input.replaceAll("\\Q&1\\E", ChatColor.DARK_BLUE.toString()); input = input.replaceAll("\\Q&2\\E", ChatColor.DARK_GREEN.toString()); @@ -352,4 +365,52 @@ public final class LocaleLoader { return input; } + + /** + * Translates hex color codes to the appropriate Minecraft color codes. + *

+ * Hex color codes are in the format of &#RRGGBB + * Minecraft color codes are in the format of §x§R§R§G§G§B§B + * Where R, G, and B are the red, green, and blue values respectively. + * The §x is a special character that tells Minecraft to use the following color codes as hex values. + * The §R§R is the red value, the §G§G is the green value, and the §B§B is the blue value. + * Example: §x§R§R§G§G§B§B is the equivalent of the hex color code &#RRGGBB + *

+ * @param messageWithHex The message with hex color codes to translate + * @return The message with the hex color codes translated to Minecraft color codes + */ + public static String translateHexColorCodes(String messageWithHex) { + if(messageWithHex == null) { + return null; + } + + final Matcher matcher = hexPattern.matcher(messageWithHex); + final StringBuilder buffer = new StringBuilder(messageWithHex.length() + 4 * 8); + while (matcher.find()) { + String group = matcher.group(1); + String hexEquivalent = "§x" + + "§" + group.charAt(0) + "§" + group.charAt(1) + + "§" + group.charAt(2) + "§" + group.charAt(3) + + "§" + group.charAt(4) + "§" + group.charAt(5); + matcher.appendReplacement(buffer, hexEquivalent); + } + return matcher.appendTail(buffer).toString(); + } + + // Method to reverse the transformation from Minecraft color codes to hex codes + public static String reverseTranslateHexColorCodes(String minecraftColorString) { + // Matches the Minecraft color pattern: §x§R§R§G§G§B§B + Matcher matcher = minecraftHexPattern.matcher(minecraftColorString); + StringBuilder buffer = new StringBuilder(); + + while (matcher.find()) { + String hexColor = "#" + + matcher.group(1).substring(1) + matcher.group(2).substring(1) + + matcher.group(3).substring(1) + matcher.group(4).substring(1) + + matcher.group(5).substring(1) + matcher.group(6).substring(1); + matcher.appendReplacement(buffer, "&" + hexColor); + } + matcher.appendTail(buffer); + return buffer.toString(); + } } diff --git a/src/main/java/com/gmail/nossr50/mcMMO.java b/src/main/java/com/gmail/nossr50/mcMMO.java index 700699140..a8c1c8601 100644 --- a/src/main/java/com/gmail/nossr50/mcMMO.java +++ b/src/main/java/com/gmail/nossr50/mcMMO.java @@ -31,7 +31,6 @@ import com.gmail.nossr50.runnables.player.ClearRegisteredXPGainTask; import com.gmail.nossr50.runnables.player.PlayerProfileLoadingTask; import com.gmail.nossr50.runnables.player.PowerLevelUpdatingTask; import com.gmail.nossr50.skills.alchemy.Alchemy; -import com.gmail.nossr50.skills.child.ChildConfig; import com.gmail.nossr50.skills.repair.repairables.Repairable; import com.gmail.nossr50.skills.repair.repairables.RepairableManager; import com.gmail.nossr50.skills.repair.repairables.SimpleRepairableManager; @@ -99,7 +98,7 @@ public class mcMMO extends JavaPlugin { private static CommandManager commandManager; //ACF private static TransientEntityTracker transientEntityTracker; - private @NotNull SkillTools skillTools; + private SkillTools skillTools; private static boolean serverShutdownExecuted = false; @@ -141,6 +140,7 @@ public class mcMMO extends JavaPlugin { private GeneralConfig generalConfig; private AdvancedConfig advancedConfig; private PartyConfig partyConfig; + private CustomItemSupportConfig customItemSupportConfig; private FoliaLib foliaLib; private PartyManager partyManager; @@ -186,6 +186,7 @@ public class mcMMO extends JavaPlugin { //Init configs advancedConfig = new AdvancedConfig(getDataFolder()); partyConfig = new PartyConfig(getDataFolder()); + customItemSupportConfig = new CustomItemSupportConfig(getDataFolder()); //Store this value so other plugins can check it isRetroModeEnabled = generalConfig.getIsRetroMode(); @@ -571,8 +572,6 @@ public class mcMMO extends JavaPlugin { SoundConfig.getInstance(); RankConfig.getInstance(); - new ChildConfig(); - List repairables = new ArrayList<>(); if (generalConfig.getToolModsEnabled()) { @@ -809,6 +808,10 @@ public class mcMMO extends JavaPlugin { return partyManager; } + public CustomItemSupportConfig getCustomItemSupportConfig() { + return customItemSupportConfig; + } + public @NotNull FoliaLib getFoliaLib() { return foliaLib; } diff --git a/src/main/java/com/gmail/nossr50/party/PartyManager.java b/src/main/java/com/gmail/nossr50/party/PartyManager.java index 130a9b996..beff259d2 100644 --- a/src/main/java/com/gmail/nossr50/party/PartyManager.java +++ b/src/main/java/com/gmail/nossr50/party/PartyManager.java @@ -352,9 +352,6 @@ public final class PartyManager { * @param password The password for this party, null if there was no password */ public void createParty(@NotNull McMMOPlayer mcMMOPlayer, @NotNull String partyName, @Nullable String password) { - requireNonNull(mcMMOPlayer, "mcMMOPlayer cannot be null!"); - requireNonNull(partyName, "partyName cannot be null!"); - Player player = mcMMOPlayer.getPlayer(); Party party = new Party(new PartyLeader(player.getUniqueId(), player.getName()), partyName.replace(".", ""), password); diff --git a/src/main/java/com/gmail/nossr50/party/ShareHandler.java b/src/main/java/com/gmail/nossr50/party/ShareHandler.java index 38aa0e2c3..24392e6b1 100644 --- a/src/main/java/com/gmail/nossr50/party/ShareHandler.java +++ b/src/main/java/com/gmail/nossr50/party/ShareHandler.java @@ -44,7 +44,9 @@ public final class ShareHandler { nearMembers.add(mcMMOPlayer.getPlayer()); int partySize = nearMembers.size(); - double shareBonus = Math.min(mcMMO.p.getGeneralConfig().getPartyShareBonusBase() + (partySize * mcMMO.p.getGeneralConfig().getPartyShareBonusIncrease()), mcMMO.p.getGeneralConfig().getPartyShareBonusCap()); + double shareBonus = Math.min(mcMMO.p.getGeneralConfig().getPartyShareBonusBase() + + (partySize * mcMMO.p.getGeneralConfig().getPartyShareBonusIncrease()), + mcMMO.p.getGeneralConfig().getPartyShareBonusCap()); float splitXp = (float) (xp / partySize * shareBonus); for (Player member : nearMembers) { diff --git a/src/main/java/com/gmail/nossr50/placeholders/PapiExpansion.java b/src/main/java/com/gmail/nossr50/placeholders/PapiExpansion.java index ff7a90cea..403d18fce 100644 --- a/src/main/java/com/gmail/nossr50/placeholders/PapiExpansion.java +++ b/src/main/java/com/gmail/nossr50/placeholders/PapiExpansion.java @@ -41,6 +41,11 @@ public class PapiExpansion extends PlaceholderExpansion { return "1.0,0"; } + @Override + public boolean persist() { + return true; + } + @Override public String getRequiredPlugin() { return "mcMMO"; diff --git a/src/main/java/com/gmail/nossr50/runnables/SaveTimerTask.java b/src/main/java/com/gmail/nossr50/runnables/SaveTimerTask.java index d9822e191..613f3ecee 100644 --- a/src/main/java/com/gmail/nossr50/runnables/SaveTimerTask.java +++ b/src/main/java/com/gmail/nossr50/runnables/SaveTimerTask.java @@ -2,7 +2,6 @@ package com.gmail.nossr50.runnables; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.runnables.player.PlayerProfileSaveTask; import com.gmail.nossr50.util.CancellableRunnable; import com.gmail.nossr50.util.LogUtils; diff --git a/src/main/java/com/gmail/nossr50/runnables/items/TeleportationWarmup.java b/src/main/java/com/gmail/nossr50/runnables/items/TeleportationWarmup.java index 7490c4117..25d809f59 100644 --- a/src/main/java/com/gmail/nossr50/runnables/items/TeleportationWarmup.java +++ b/src/main/java/com/gmail/nossr50/runnables/items/TeleportationWarmup.java @@ -3,7 +3,6 @@ package com.gmail.nossr50.runnables.items; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.CancellableRunnable; import com.gmail.nossr50.util.EventUtils; import com.gmail.nossr50.util.Misc; diff --git a/src/main/java/com/gmail/nossr50/runnables/party/PartyAutoKickTask.java b/src/main/java/com/gmail/nossr50/runnables/party/PartyAutoKickTask.java index 2db0c3c27..1d662dba9 100644 --- a/src/main/java/com/gmail/nossr50/runnables/party/PartyAutoKickTask.java +++ b/src/main/java/com/gmail/nossr50/runnables/party/PartyAutoKickTask.java @@ -2,7 +2,6 @@ package com.gmail.nossr50.runnables.party; import com.gmail.nossr50.datatypes.party.Party; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.util.CancellableRunnable; import org.bukkit.OfflinePlayer; diff --git a/src/main/java/com/gmail/nossr50/runnables/skills/AbilityCooldownTask.java b/src/main/java/com/gmail/nossr50/runnables/skills/AbilityCooldownTask.java index 69e080972..9c7d8bfd8 100644 --- a/src/main/java/com/gmail/nossr50/runnables/skills/AbilityCooldownTask.java +++ b/src/main/java/com/gmail/nossr50/runnables/skills/AbilityCooldownTask.java @@ -21,9 +21,7 @@ public class AbilityCooldownTask extends CancellableRunnable { return; } - mcMMOPlayer.setAbilityInformed(ability, true); - + mcMMOPlayer.setAbilityInformed(ability, true); // TODO: ?? What does this do again? NotificationManager.sendPlayerInformation(mcMMOPlayer.getPlayer(), NotificationType.ABILITY_REFRESHED, ability.getAbilityRefresh()); - //mcMMOPlayer.getPlayer().sendMessage(ability.getAbilityRefresh()); } } diff --git a/src/main/java/com/gmail/nossr50/runnables/skills/RuptureTask.java b/src/main/java/com/gmail/nossr50/runnables/skills/RuptureTask.java index f2e9c608f..432d5a296 100644 --- a/src/main/java/com/gmail/nossr50/runnables/skills/RuptureTask.java +++ b/src/main/java/com/gmail/nossr50/runnables/skills/RuptureTask.java @@ -1,6 +1,5 @@ package com.gmail.nossr50.runnables.skills; -import com.gmail.nossr50.datatypes.MobHealthbarType; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.events.skills.rupture.McMMOEntityDamageByRuptureEvent; import com.gmail.nossr50.mcMMO; diff --git a/src/main/java/com/gmail/nossr50/skills/acrobatics/AcrobaticsManager.java b/src/main/java/com/gmail/nossr50/skills/acrobatics/AcrobaticsManager.java index a0758bacc..08fd39ab4 100644 --- a/src/main/java/com/gmail/nossr50/skills/acrobatics/AcrobaticsManager.java +++ b/src/main/java/com/gmail/nossr50/skills/acrobatics/AcrobaticsManager.java @@ -14,10 +14,9 @@ import com.gmail.nossr50.util.MetadataConstants; import com.gmail.nossr50.util.Misc; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.NotificationManager; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.ParticleEffectUtils; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.skills.SkillUtils; import org.bukkit.Location; import org.bukkit.entity.Entity; @@ -94,7 +93,8 @@ public class AcrobaticsManager extends SkillManager { double modifiedDamage = Acrobatics.calculateModifiedDodgeDamage(damage, Acrobatics.dodgeDamageModifier); Player player = getPlayer(); - if (!isFatal(modifiedDamage) && RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.ACROBATICS_DODGE, player)) { + if (!isFatal(modifiedDamage) + && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.ACROBATICS_DODGE, player)) { ParticleEffectUtils.playDodgeEffect(player); if (mmoPlayer.useChatNotifications()) { diff --git a/src/main/java/com/gmail/nossr50/skills/archery/ArcheryManager.java b/src/main/java/com/gmail/nossr50/skills/archery/ArcheryManager.java index 56dd26aaa..7cc028e8f 100644 --- a/src/main/java/com/gmail/nossr50/skills/archery/ArcheryManager.java +++ b/src/main/java/com/gmail/nossr50/skills/archery/ArcheryManager.java @@ -10,9 +10,8 @@ import com.gmail.nossr50.util.MetadataConstants; import com.gmail.nossr50.util.Misc; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.NotificationManager; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import org.bukkit.Location; import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; @@ -53,7 +52,7 @@ public class ArcheryManager extends SkillManager { * @param target The {@link LivingEntity} damaged by the arrow * @param arrow The {@link Entity} who shot the arrow */ - public double distanceXpBonusMultiplier(LivingEntity target, Entity arrow) { + public static double distanceXpBonusMultiplier(LivingEntity target, Entity arrow) { //Hacky Fix - some plugins spawn arrows and assign them to players after the ProjectileLaunchEvent fires if(!arrow.hasMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE)) return 1; @@ -89,7 +88,7 @@ public class ArcheryManager extends SkillManager { * @param defender The {@link Player} being affected by the ability */ public double daze(Player defender) { - if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.ARCHERY_DAZE, getPlayer())) { + if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.ARCHERY_DAZE, getPlayer())) { return 0; } @@ -118,10 +117,10 @@ public class ArcheryManager extends SkillManager { * @param oldDamage The raw damage value of this arrow before we modify it */ public double skillShot(double oldDamage) { - if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.ALWAYS_FIRES, SubSkillType.ARCHERY_SKILL_SHOT, getPlayer())) { + if (ProbabilityUtil.isNonRNGSkillActivationSuccessful(SubSkillType.ARCHERY_SKILL_SHOT, getPlayer())) { + return Archery.getSkillShotBonusDamage(getPlayer(), oldDamage); + } else { return oldDamage; } - - return Archery.getSkillShotBonusDamage(getPlayer(), oldDamage); } } diff --git a/src/main/java/com/gmail/nossr50/skills/axes/AxesManager.java b/src/main/java/com/gmail/nossr50/skills/axes/AxesManager.java index d7ce592d8..d80f7292f 100644 --- a/src/main/java/com/gmail/nossr50/skills/axes/AxesManager.java +++ b/src/main/java/com/gmail/nossr50/skills/axes/AxesManager.java @@ -11,8 +11,11 @@ import com.gmail.nossr50.skills.SkillManager; import com.gmail.nossr50.util.ItemUtils; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.NotificationManager; -import com.gmail.nossr50.util.random.RandomChanceUtil; -import com.gmail.nossr50.util.skills.*; +import com.gmail.nossr50.util.random.ProbabilityUtil; +import com.gmail.nossr50.util.skills.CombatUtils; +import com.gmail.nossr50.util.skills.ParticleEffectUtils; +import com.gmail.nossr50.util.skills.RankUtils; +import com.gmail.nossr50.util.skills.SkillUtils; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -66,11 +69,11 @@ public class AxesManager extends SkillManager { * Handle the effects of the Axe Mastery ability */ public double axeMastery() { - if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.ALWAYS_FIRES, SubSkillType.AXES_AXE_MASTERY, getPlayer())) { - return 0; + if (ProbabilityUtil.isNonRNGSkillActivationSuccessful(SubSkillType.AXES_AXE_MASTERY, getPlayer())) { + return Axes.getAxeMasteryBonusDamage(getPlayer()); } - return Axes.getAxeMasteryBonusDamage(getPlayer()); + return 0; } /** @@ -80,7 +83,7 @@ public class AxesManager extends SkillManager { * @param damage The amount of damage initially dealt by the event */ public double criticalHit(LivingEntity target, double damage) { - if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.AXES_CRITICAL_STRIKES, getPlayer())) { + if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.AXES_CRITICAL_STRIKES, getPlayer())) { return 0; } @@ -115,7 +118,7 @@ public class AxesManager extends SkillManager { for (ItemStack armor : target.getEquipment().getArmorContents()) { if (armor != null && ItemUtils.isArmor(armor)) { - if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_STATIC_CHANCE, SubSkillType.AXES_ARMOR_IMPACT, getPlayer())) { + if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.AXES_ARMOR_IMPACT, getPlayer())) { SkillUtils.handleArmorDurabilityChange(armor, durabilityDamage, 1); } } @@ -133,7 +136,7 @@ public class AxesManager extends SkillManager { */ public double greaterImpact(@NotNull LivingEntity target) { //static chance (3rd param) - if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_STATIC_CHANCE, SubSkillType.AXES_GREATER_IMPACT, getPlayer())) { + if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.AXES_GREATER_IMPACT, getPlayer())) { return 0; } diff --git a/src/main/java/com/gmail/nossr50/skills/child/ChildConfig.java b/src/main/java/com/gmail/nossr50/skills/child/ChildConfig.java deleted file mode 100644 index 0e200bafa..000000000 --- a/src/main/java/com/gmail/nossr50/skills/child/ChildConfig.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.gmail.nossr50.skills.child; - -import com.gmail.nossr50.config.BukkitConfig; -import com.gmail.nossr50.datatypes.skills.PrimarySkillType; -import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.util.LogUtils; -import com.gmail.nossr50.util.text.StringUtils; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.util.EnumSet; -import java.util.Locale; - -public class ChildConfig extends BukkitConfig { - public ChildConfig() { - super("child.yml"); - loadKeys(); - } - - @Override - protected void loadKeys() { - config.setDefaults(YamlConfiguration.loadConfiguration(mcMMO.p.getResourceAsReader("child.yml"))); - - FamilyTree.clearRegistrations(); // when reloading, need to clear statics - - for (PrimarySkillType skill : mcMMO.p.getSkillTools().CHILD_SKILLS) { - LogUtils.debug(mcMMO.p.getLogger(), "Finding parents of " + skill.name()); - - EnumSet parentSkills = EnumSet.noneOf(PrimarySkillType.class); - boolean useDefaults = false; // If we had an error we back out and use defaults - - for (String name : config.getStringList(StringUtils.getCapitalized(skill.name()))) { - try { - PrimarySkillType parentSkill = PrimarySkillType.valueOf(name.toUpperCase(Locale.ENGLISH)); - FamilyTree.enforceNotChildSkill(parentSkill); - parentSkills.add(parentSkill); - } - catch (IllegalArgumentException ex) { - mcMMO.p.getLogger().warning(name + " is not a valid skill type, or is a child skill!"); - useDefaults = true; - break; - } - } - - if (useDefaults) { - parentSkills.clear(); - for (String name : config.getDefaults().getStringList(StringUtils.getCapitalized(skill.name()))) { - /* We do less checks in here because it's from inside our jar. - * If they're dedicated enough to have modified it, they can have the errors it may produce. - * Alternatively, this can be used to allow child skills to be parent skills, provided there are no circular dependencies this is an advanced sort of configuration. - */ - parentSkills.add(PrimarySkillType.valueOf(name.toUpperCase(Locale.ENGLISH))); - } - } - - // Register them - for (PrimarySkillType parentSkill : parentSkills) { - LogUtils.debug(mcMMO.p.getLogger(), "Registering " + parentSkill.name() + " as parent of " + skill.name()); - FamilyTree.registerParent(skill, parentSkill); - } - } - - FamilyTree.closeRegistration(); - } -} diff --git a/src/main/java/com/gmail/nossr50/skills/child/FamilyTree.java b/src/main/java/com/gmail/nossr50/skills/child/FamilyTree.java deleted file mode 100644 index 0be533600..000000000 --- a/src/main/java/com/gmail/nossr50/skills/child/FamilyTree.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.gmail.nossr50.skills.child; - -import com.gmail.nossr50.datatypes.skills.PrimarySkillType; -import com.gmail.nossr50.util.skills.SkillTools; - -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.Set; - -public class FamilyTree { - private static final HashMap> tree = new HashMap<>(); - - public static Set getParents(PrimarySkillType childSkill) { - enforceChildSkill(childSkill); - - // We do not check if we have the child skill in question, as not having it would mean we did something wrong, and an NPE is desired. - return tree.get(childSkill); - } - - protected static void registerParent(PrimarySkillType childSkill, PrimarySkillType parentSkill) { - enforceChildSkill(childSkill); - enforceNotChildSkill(parentSkill); - - if (!tree.containsKey(childSkill)) { - tree.put(childSkill, EnumSet.noneOf(PrimarySkillType.class)); - } - - tree.get(childSkill).add(parentSkill); - } - - protected static void closeRegistration() { - for (PrimarySkillType childSkill : tree.keySet()) { - Set immutableSet = Collections.unmodifiableSet(tree.get(childSkill)); - tree.put(childSkill, immutableSet); - } - } - - protected static void clearRegistrations() { - tree.clear(); - } - - protected static void enforceChildSkill(PrimarySkillType skill) { - if (!SkillTools.isChildSkill(skill)) { - throw new IllegalArgumentException(skill.name() + " is not a child skill!"); - } - } - - protected static void enforceNotChildSkill(PrimarySkillType skill) { - if (SkillTools.isChildSkill(skill)) { - throw new IllegalArgumentException(skill.name() + " is a child skill!"); - } - } -} diff --git a/src/main/java/com/gmail/nossr50/skills/crossbows/Crossbows.java b/src/main/java/com/gmail/nossr50/skills/crossbows/Crossbows.java new file mode 100644 index 000000000..a64b023af --- /dev/null +++ b/src/main/java/com/gmail/nossr50/skills/crossbows/Crossbows.java @@ -0,0 +1,41 @@ +package com.gmail.nossr50.skills.crossbows; + +import com.gmail.nossr50.datatypes.player.McMMOPlayer; +import com.gmail.nossr50.util.player.UserManager; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.Player; +import org.bukkit.event.entity.ProjectileHitEvent; +import org.bukkit.plugin.Plugin; + +import static com.gmail.nossr50.util.skills.ProjectileUtils.getNormal; + +/** + * Util class for crossbows. + */ +public class Crossbows { + /** + * Process events that may happen from a crossbow hitting an entity. + * + * @param event the projectile hit event + * @param pluginRef the plugin ref + * @param arrow the arrow + */ + public static void processCrossbows(ProjectileHitEvent event, Plugin pluginRef, Arrow arrow) { + if (arrow.getShooter() instanceof Player) { + McMMOPlayer mmoPlayer = UserManager.getPlayer((Player) arrow.getShooter()); + if (mmoPlayer == null) + return; + + processTrickShot(event, pluginRef, arrow, mmoPlayer); + } + } + + private static void processTrickShot(ProjectileHitEvent event, Plugin pluginRef, Arrow arrow, McMMOPlayer mmoPlayer) { + if(event.getHitBlock() != null && event.getHitBlockFace() != null) { + mmoPlayer.getCrossbowsManager().handleRicochet( + pluginRef, + arrow, + getNormal(event.getHitBlockFace())); + } + } +} diff --git a/src/main/java/com/gmail/nossr50/skills/crossbows/CrossbowsManager.java b/src/main/java/com/gmail/nossr50/skills/crossbows/CrossbowsManager.java new file mode 100644 index 000000000..75d7e5d23 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/skills/crossbows/CrossbowsManager.java @@ -0,0 +1,109 @@ +package com.gmail.nossr50.skills.crossbows; + +import com.gmail.nossr50.datatypes.player.McMMOPlayer; +import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +import com.gmail.nossr50.datatypes.skills.SubSkillType; +import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.skills.SkillManager; +import com.gmail.nossr50.util.MetadataConstants; +import com.gmail.nossr50.util.Permissions; +import com.gmail.nossr50.util.random.ProbabilityUtil; +import com.gmail.nossr50.util.skills.ProjectileUtils; +import com.gmail.nossr50.util.skills.RankUtils; +import org.bukkit.Location; +import org.bukkit.entity.AbstractArrow; +import org.bukkit.entity.Arrow; +import org.bukkit.entity.Player; +import org.bukkit.metadata.FixedMetadataValue; +import org.bukkit.plugin.Plugin; +import org.bukkit.projectiles.ProjectileSource; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; + +import static com.gmail.nossr50.util.skills.CombatUtils.delayArrowMetaCleanup; + +public class CrossbowsManager extends SkillManager { + public CrossbowsManager(McMMOPlayer mmoPlayer) { + super(mmoPlayer, PrimarySkillType.CROSSBOWS); + } + + public void handleRicochet(@NotNull Plugin pluginRef, @NotNull Arrow arrow, @NotNull Vector hitBlockNormal) { + if(!arrow.isShotFromCrossbow()) + return; + + // Check player permission + if (!Permissions.trickShot(mmoPlayer.getPlayer())) { + return; + } + + // TODO: Add an event for this for plugins to hook into + spawnReflectedArrow(pluginRef, arrow, arrow.getLocation(), hitBlockNormal); + } + + private void spawnReflectedArrow(@NotNull Plugin pluginRef, @NotNull Arrow originalArrow, + @NotNull Location origin, @NotNull Vector normal) { + int bounceCount = 0; + + if (originalArrow.hasMetadata(MetadataConstants.METADATA_KEY_BOUNCE_COUNT)) { + bounceCount = originalArrow.getMetadata(MetadataConstants.METADATA_KEY_BOUNCE_COUNT).get(0).asInt(); + if (bounceCount >= getTrickShotMaxBounceCount()) { + return; + } + } + + final ProjectileSource originalArrowShooter = originalArrow.getShooter(); + final Vector arrowInBlockVector = originalArrow.getVelocity(); + final Vector reflectedDirection = arrowInBlockVector.subtract(normal.multiply(2 * arrowInBlockVector.dot(normal))); + final Vector inverseNormal = normal.multiply(-1); + + // check the angle of the arrow against the inverse normal to see if the angle was too shallow + // only checks angle on the first bounce + if (bounceCount == 0 && arrowInBlockVector.angle(inverseNormal) < Math.PI / 4) { + return; + } + + // Spawn new arrow with the reflected direction + Arrow spawnedArrow = originalArrow.getWorld().spawnArrow(origin, reflectedDirection, 1, 1); + ProjectileUtils.copyArrowMetadata(pluginRef, originalArrow, spawnedArrow); + originalArrow.remove(); + // copy metadata from old arrow + spawnedArrow.setShooter(originalArrowShooter); + spawnedArrow.setMetadata(MetadataConstants.METADATA_KEY_BOUNCE_COUNT, + new FixedMetadataValue(pluginRef, bounceCount + 1)); + spawnedArrow.setMetadata(MetadataConstants.METADATA_KEY_SPAWNED_ARROW, + new FixedMetadataValue(pluginRef, originalArrowShooter)); + spawnedArrow.setShotFromCrossbow(true); + + // Don't allow multi-shot or infinite arrows to be picked up + if (spawnedArrow.hasMetadata(MetadataConstants.METADATA_KEY_MULTI_SHOT_ARROW) + || spawnedArrow.hasMetadata(MetadataConstants.METADATA_KEY_INF_ARROW)) { + spawnedArrow.setPickupStatus(AbstractArrow.PickupStatus.DISALLOWED); + } + + // Schedule cleanup of metadata in case metadata cleanup fails + delayArrowMetaCleanup(spawnedArrow); + } + + public int getTrickShotMaxBounceCount() { + return RankUtils.getRank(mmoPlayer, SubSkillType.CROSSBOWS_TRICK_SHOT); + } + + public double getPoweredShotBonusDamage(Player player, double oldDamage) + { + double damageBonusPercent = getDamageBonusPercent(player); + double newDamage = oldDamage + (oldDamage * damageBonusPercent); + return Math.min(newDamage, (oldDamage + mcMMO.p.getAdvancedConfig().getPoweredShotDamageMax())); + } + + public double getDamageBonusPercent(Player player) { + return ((RankUtils.getRank(player, SubSkillType.CROSSBOWS_POWERED_SHOT)) * (mcMMO.p.getAdvancedConfig().getPoweredShotRankDamageMultiplier()) / 100.0D); + } + + public double poweredShot(double oldDamage) { + if (ProbabilityUtil.isNonRNGSkillActivationSuccessful(SubSkillType.CROSSBOWS_POWERED_SHOT, getPlayer())) { + return getPoweredShotBonusDamage(getPlayer(), oldDamage); + } else { + return oldDamage; + } + } +} diff --git a/src/main/java/com/gmail/nossr50/skills/excavation/ExcavationManager.java b/src/main/java/com/gmail/nossr50/skills/excavation/ExcavationManager.java index 876634b46..e3cf20fd3 100644 --- a/src/main/java/com/gmail/nossr50/skills/excavation/ExcavationManager.java +++ b/src/main/java/com/gmail/nossr50/skills/excavation/ExcavationManager.java @@ -2,6 +2,7 @@ package com.gmail.nossr50.skills.excavation; import com.gmail.nossr50.api.ItemSpawnReason; import com.gmail.nossr50.datatypes.experience.XPGainReason; +import com.gmail.nossr50.datatypes.experience.XPGainSource; import com.gmail.nossr50.datatypes.player.McMMOPlayer; import com.gmail.nossr50.datatypes.skills.PrimarySkillType; import com.gmail.nossr50.datatypes.skills.SubSkillType; @@ -10,15 +11,19 @@ import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.skills.SkillManager; import com.gmail.nossr50.util.Misc; import com.gmail.nossr50.util.Permissions; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; import com.gmail.nossr50.util.skills.SkillUtils; import org.bukkit.Location; import org.bukkit.block.BlockState; import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; import java.util.List; +import static java.util.Objects.requireNonNull; + public class ExcavationManager extends SkillManager { public ExcavationManager(McMMOPlayer mcMMOPlayer) { super(mcMMOPlayer, PrimarySkillType.EXCAVATION); @@ -31,9 +36,9 @@ public class ExcavationManager extends SkillManager { */ public void excavationBlockCheck(BlockState blockState) { int xp = Excavation.getBlockXP(blockState); - + requireNonNull(blockState, "excavationBlockCheck: blockState cannot be null"); if (Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.EXCAVATION_ARCHAEOLOGY)) { - List treasures = Excavation.getTreasures(blockState); + List treasures = getTreasures(blockState); if (!treasures.isEmpty()) { int skillLevel = getSkillLevel(); @@ -41,21 +46,35 @@ public class ExcavationManager extends SkillManager { for (ExcavationTreasure treasure : treasures) { if (skillLevel >= treasure.getDropLevel() - && RandomChanceUtil.checkRandomChanceExecutionSuccess(getPlayer(), PrimarySkillType.EXCAVATION, treasure.getDropChance())) { - - //Spawn Vanilla XP orbs if a dice roll succeeds - if(RandomChanceUtil.rollDice(getArchaelogyExperienceOrbChance(), 100)) { - Misc.spawnExperienceOrb(location, getExperienceOrbsReward()); - } - - xp += treasure.getXp(); - Misc.spawnItem(getPlayer(), location, treasure.getDrop(), ItemSpawnReason.EXCAVATION_TREASURE); + && ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.EXCAVATION, getPlayer(), treasure.getDropProbability())) { + processExcavationBonusesOnBlock(blockState, treasure, location); } } } } - applyXpGain(xp, XPGainReason.PVE); + applyXpGain(xp, XPGainReason.PVE, XPGainSource.SELF); + } + + @VisibleForTesting + public List getTreasures(@NotNull BlockState blockState) { + requireNonNull(blockState, "blockState cannot be null"); + return Excavation.getTreasures(blockState); + } + + @VisibleForTesting + public void processExcavationBonusesOnBlock(BlockState blockState, ExcavationTreasure treasure, Location location) { + //Spawn Vanilla XP orbs if a dice roll succeeds + if(ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.EXCAVATION, getPlayer(), getArchaelogyExperienceOrbChance())) { + Misc.spawnExperienceOrb(location, getExperienceOrbsReward()); + } + + int xp = 0; + xp += treasure.getXp(); + Misc.spawnItem(getPlayer(), location, treasure.getDrop(), ItemSpawnReason.EXCAVATION_TREASURE); + if (xp > 0) { + applyXpGain(xp, XPGainReason.PVE, XPGainSource.SELF); + } } public int getExperienceOrbsReward() { diff --git a/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java b/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java index 7f3767986..6063add73 100644 --- a/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/fishing/FishingManager.java @@ -18,8 +18,7 @@ import com.gmail.nossr50.skills.SkillManager; import com.gmail.nossr50.util.*; import com.gmail.nossr50.util.compat.layers.skills.MasterAnglerCompatibilityLayer; import com.gmail.nossr50.util.player.NotificationManager; -import com.gmail.nossr50.util.random.RandomChanceSkillStatic; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.CombatUtils; import com.gmail.nossr50.util.skills.RankUtils; import com.gmail.nossr50.util.skills.SkillUtils; @@ -61,11 +60,14 @@ public class FishingManager extends SkillManager { } public boolean canShake(Entity target) { - return target instanceof LivingEntity && RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.FISHING_SHAKE) && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.FISHING_SHAKE); + return target instanceof LivingEntity && RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.FISHING_SHAKE) + && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.FISHING_SHAKE); } public boolean canMasterAngler() { - return mcMMO.getCompatibilityManager().getMasterAnglerCompatibilityLayer() != null && getSkillLevel() >= RankUtils.getUnlockLevel(SubSkillType.FISHING_MASTER_ANGLER) && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.FISHING_MASTER_ANGLER); + return mcMMO.getCompatibilityManager().getMasterAnglerCompatibilityLayer() != null + && getSkillLevel() >= RankUtils.getUnlockLevel(SubSkillType.FISHING_MASTER_ANGLER) + && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.FISHING_MASTER_ANGLER); } // public void setFishingRodCastTimestamp() @@ -477,8 +479,8 @@ public class FishingManager extends SkillManager { * * @param target The {@link LivingEntity} affected by the ability */ - public void shakeCheck(LivingEntity target) { - if (RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getShakeChance(), getPlayer(), SubSkillType.FISHING_SHAKE))) { + public void shakeCheck(@NotNull LivingEntity target) { + if (ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.FISHING, getPlayer(), getShakeChance())) { List possibleDrops = Fishing.findPossibleDrops(target); if (possibleDrops == null || possibleDrops.isEmpty()) { diff --git a/src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java b/src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java index d97b12db8..c75f03a1a 100644 --- a/src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java +++ b/src/main/java/com/gmail/nossr50/skills/herbalism/HerbalismManager.java @@ -20,10 +20,8 @@ import com.gmail.nossr50.runnables.skills.DelayedHerbalismXPCheckTask; import com.gmail.nossr50.skills.SkillManager; import com.gmail.nossr50.util.*; import com.gmail.nossr50.util.player.NotificationManager; -import com.gmail.nossr50.util.random.RandomChanceSkillStatic; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.skills.SkillUtils; import com.gmail.nossr50.util.sounds.SoundManager; import com.gmail.nossr50.util.sounds.SoundType; @@ -35,6 +33,7 @@ import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; import org.bukkit.block.data.Ageable; import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Item; import org.bukkit.entity.Player; import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.inventory.ItemStack; @@ -43,6 +42,9 @@ import org.jetbrains.annotations.NotNull; import java.util.*; +import static com.gmail.nossr50.util.ItemUtils.hasItemIncludingOffHand; +import static com.gmail.nossr50.util.ItemUtils.removeItemIncludingOffHand; + public class HerbalismManager extends SkillManager { public HerbalismManager(McMMOPlayer mcMMOPlayer) { super(mcMMOPlayer, PrimarySkillType.HERBALISM); @@ -650,7 +652,7 @@ public class HerbalismManager extends SkillManager { * @return true if the ability was successful, false otherwise */ public boolean processGreenThumbBlocks(BlockState blockState) { - if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.HERBALISM_GREEN_THUMB, getPlayer())) { + if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.HERBALISM_GREEN_THUMB, getPlayer())) { NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE_FAILED, "Herbalism.Ability.GTh.Fail"); return false; } @@ -665,7 +667,7 @@ public class HerbalismManager extends SkillManager { * @return true if the ability was successful, false otherwise */ public boolean processHylianLuck(BlockState blockState) { - if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.HERBALISM_HYLIAN_LUCK, getPlayer())) { + if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.HERBALISM_HYLIAN_LUCK, getPlayer())) { return false; } @@ -684,7 +686,7 @@ public class HerbalismManager extends SkillManager { for (HylianTreasure treasure : treasures) { if (skillLevel >= treasure.getDropLevel() - && RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(treasure.getDropChance(), getPlayer(), SubSkillType.HERBALISM_HYLIAN_LUCK))) { + && ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.HERBALISM, player, treasure.getDropChance())) { if (!EventUtils.simulateBlockBreak(blockState.getBlock(), player)) { return false; } @@ -721,7 +723,7 @@ public class HerbalismManager extends SkillManager { playerInventory.removeItem(new ItemStack(Material.RED_MUSHROOM)); player.updateInventory(); - if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.HERBALISM_SHROOM_THUMB, player)) { + if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.HERBALISM_SHROOM_THUMB, player)) { NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE_FAILED, "Herbalism.Ability.ShroomThumb.Fail"); return false; } @@ -747,13 +749,14 @@ public class HerbalismManager extends SkillManager { * @param blockState The {@link BlockState} to check ability activation for * @param greenTerra boolean to determine if greenTerra is active or not */ - private boolean processGreenThumbPlants(BlockState blockState, BlockBreakEvent blockBreakEvent, boolean greenTerra) { + private boolean processGreenThumbPlants(@NotNull BlockState blockState, @NotNull BlockBreakEvent blockBreakEvent, + boolean greenTerra) { if (!ItemUtils.isHoe(blockBreakEvent.getPlayer().getInventory().getItemInMainHand()) && !ItemUtils.isAxe(blockBreakEvent.getPlayer().getInventory().getItemInMainHand())) { return false; } - BlockData blockData = blockState.getBlockData(); + final BlockData blockData = blockState.getBlockData(); if (!(blockData instanceof Ageable ageable)) { return false; @@ -761,73 +764,54 @@ public class HerbalismManager extends SkillManager { //If the ageable is NOT mature and the player is NOT using a hoe, abort - Player player = getPlayer(); - PlayerInventory playerInventory = player.getInventory(); - Material seed; + final Player player = getPlayer(); + final Material replantMaterial; - switch (blockState.getType().getKey().getKey().toLowerCase(Locale.ROOT)) { - case "carrots": - seed = Material.matchMaterial("CARROT"); - break; - - case "wheat": - seed = Material.matchMaterial("WHEAT_SEEDS"); - break; - - case "nether_wart": - seed = Material.getMaterial("NETHER_WART"); - break; - - case "potatoes": - seed = Material.matchMaterial("POTATO"); - break; - - case "beetroots": - seed = Material.matchMaterial("BEETROOT_SEEDS"); - break; - - case "cocoa": - seed = Material.matchMaterial("COCOA_BEANS"); - break; - - case "torchflower": - seed = Material.matchMaterial("TORCHFLOWER_SEEDS"); - break; - default: + switch (blockState.getType().getKey().getKey().toLowerCase(Locale.ENGLISH)) { + case "carrots" -> replantMaterial = Material.matchMaterial("CARROT"); + case "wheat" -> replantMaterial = Material.matchMaterial("WHEAT_SEEDS"); + case "nether_wart" -> replantMaterial = Material.getMaterial("NETHER_WART"); + case "potatoes" -> replantMaterial = Material.matchMaterial("POTATO"); + case "beetroots" -> replantMaterial = Material.matchMaterial("BEETROOT_SEEDS"); + case "cocoa" -> replantMaterial = Material.matchMaterial("COCOA_BEANS"); + case "torchflower" -> replantMaterial = Material.matchMaterial("TORCHFLOWER_SEEDS"); + default -> { return false; + } } - - ItemStack seedStack = new ItemStack(seed); + if (replantMaterial == null) { + return false; + } if (ItemUtils.isAxe(blockBreakEvent.getPlayer().getInventory().getItemInMainHand()) - && blockState.getType() != Material.COCOA) { + && blockState.getType() != Material.COCOA) { return false; } - if (!greenTerra && !RandomChanceUtil.checkRandomChanceExecutionSuccess(player, SubSkillType.HERBALISM_GREEN_THUMB, true)) { + if (!greenTerra && !ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.HERBALISM_GREEN_THUMB, player)) { return false; } - if (!playerInventory.containsAtLeast(seedStack, 1)) { + if (!hasItemIncludingOffHand(player, replantMaterial)) { return false; } - if (!processGrowingPlants(blockState, ageable, blockBreakEvent, greenTerra)) { - return false; - } - - if(EventUtils.callSubSkillBlockEvent(player, SubSkillType.HERBALISM_GREEN_THUMB, blockState.getBlock()).isCancelled()) { + if(EventUtils.callSubSkillBlockEvent(player, SubSkillType.HERBALISM_GREEN_THUMB, blockState.getBlock()) + .isCancelled()) { return false; } else { - playerInventory.removeItem(seedStack); - player.updateInventory(); // Needed until replacement available + if (!processGrowingPlants(blockState, ageable, blockBreakEvent, greenTerra)) { + return false; + } + // remove the item from the player's inventory + removeItemIncludingOffHand(player, replantMaterial, 1); + // player.updateInventory(); // Needed until replacement available + //Play sound SoundManager.sendSound(player, player.getLocation(), SoundType.ITEM_CONSUMED); return true; } - -// new HerbalismBlockUpdaterTask(blockState).runTaskLater(mcMMO.p, 0); } private boolean processGrowingPlants(BlockState blockState, Ageable ageable, BlockBreakEvent blockBreakEvent, boolean greenTerra) { diff --git a/src/main/java/com/gmail/nossr50/skills/mining/MiningManager.java b/src/main/java/com/gmail/nossr50/skills/mining/MiningManager.java index 030104d4f..b91274ce4 100644 --- a/src/main/java/com/gmail/nossr50/skills/mining/MiningManager.java +++ b/src/main/java/com/gmail/nossr50/skills/mining/MiningManager.java @@ -13,7 +13,7 @@ import com.gmail.nossr50.runnables.skills.AbilityCooldownTask; import com.gmail.nossr50.skills.SkillManager; import com.gmail.nossr50.util.*; import com.gmail.nossr50.util.player.NotificationManager; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; import com.gmail.nossr50.util.skills.SkillUtils; import org.apache.commons.lang.math.RandomUtils; @@ -35,7 +35,7 @@ public class MiningManager extends SkillManager { public static final String BUDDING_AMETHYST = "budding_amethyst"; - public MiningManager(McMMOPlayer mcMMOPlayer) { + public MiningManager(@NotNull McMMOPlayer mcMMOPlayer) { super(mcMMOPlayer, PrimarySkillType.MINING); } @@ -70,6 +70,11 @@ public class MiningManager extends SkillManager { return RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.MINING_DOUBLE_DROPS) && Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.MINING_DOUBLE_DROPS); } + public boolean canMotherLode() { + return Permissions.canUseSubSkill(getPlayer(), SubSkillType.MINING_MOTHER_LODE); + } + + /** * Process double drops & XP gain for Mining. * @@ -96,9 +101,32 @@ public class MiningManager extends SkillManager { if(silkTouch && !mcMMO.p.getAdvancedConfig().getDoubleDropSilkTouchEnabled()) return; + //Mining mastery allows for a chance of triple drops + if(canMotherLode()) { + //Triple Drops failed so do a normal double drops check + if(!processTripleDrops(blockState)) { + processDoubleDrops(blockState); + } + } else { + //If the user has no mastery, proceed with normal double drop routine + processDoubleDrops(blockState); + } + } + + private boolean processTripleDrops(@NotNull BlockState blockState) { //TODO: Make this readable - if (RandomChanceUtil.checkRandomChanceExecutionSuccess(getPlayer(), SubSkillType.MINING_DOUBLE_DROPS, true)) { - boolean useTriple = mmoPlayer.getAbilityMode(mcMMO.p.getSkillTools().getSuperAbility(skill)) && mcMMO.p.getAdvancedConfig().getAllowMiningTripleDrops(); + if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.MINING_MOTHER_LODE, getPlayer())) { + BlockUtils.markDropsAsBonus(blockState, 2); + return true; + } else { + return false; + } + } + + private void processDoubleDrops(@NotNull BlockState blockState) { + //TODO: Make this readable + if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.MINING_DOUBLE_DROPS, getPlayer())) { + boolean useTriple = mmoPlayer.getAbilityMode(SuperAbilityType.SUPER_BREAKER) && mcMMO.p.getAdvancedConfig().getAllowMiningTripleDrops(); BlockUtils.markDropsAsBonus(blockState, useTriple); } } diff --git a/src/main/java/com/gmail/nossr50/skills/repair/RepairManager.java b/src/main/java/com/gmail/nossr50/skills/repair/RepairManager.java index 284f875c6..08a6ce8e8 100644 --- a/src/main/java/com/gmail/nossr50/skills/repair/RepairManager.java +++ b/src/main/java/com/gmail/nossr50/skills/repair/RepairManager.java @@ -14,10 +14,8 @@ import com.gmail.nossr50.util.EventUtils; import com.gmail.nossr50.util.Misc; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.NotificationManager; -import com.gmail.nossr50.util.random.RandomChanceSkillStatic; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.skills.SkillUtils; import com.gmail.nossr50.util.sounds.SoundManager; import com.gmail.nossr50.util.sounds.SoundType; @@ -67,10 +65,19 @@ public class RepairManager extends SkillManager { public void handleRepair(ItemStack item) { Player player = getPlayer(); Repairable repairable = mcMMO.getRepairableManager().getRepairable(item.getType()); + if (item.getItemMeta() != null) { + if(item.getItemMeta().hasCustomModelData()) { + if(!mcMMO.p.getCustomItemSupportConfig().isCustomRepairAllowed()) { + NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE_FAILED, + "Anvil.Repair.Reject.CustomModelData"); + return; + } + } - if (item.getItemMeta().isUnbreakable()) { - NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE_FAILED, "Anvil.Unbreakable"); - return; + if (item.getItemMeta().isUnbreakable()) { + NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE_FAILED, "Anvil.Unbreakable"); + return; + } } // Permissions checks on material and item types @@ -322,7 +329,7 @@ public class RepairManager extends SkillManager { if(!RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.REPAIR_SUPER_REPAIR)) return false; - if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.REPAIR_SUPER_REPAIR, getPlayer())) { + if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.REPAIR_SUPER_REPAIR, getPlayer())) { NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Repair.Skills.FeltEasy"); return true; } @@ -373,10 +380,10 @@ public class RepairManager extends SkillManager { Enchantment enchantment = enchant.getKey(); - if (RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getKeepEnchantChance(), getPlayer(), SubSkillType.REPAIR_ARCANE_FORGING))) { + if (ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.REPAIR, getPlayer(), getKeepEnchantChance())) { if (ArcaneForging.arcaneForgingDowngrades && enchantLevel > 1 - && (!RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(100 - getDowngradeEnchantChance(), getPlayer(), SubSkillType.REPAIR_ARCANE_FORGING)))) { + && (!ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.REPAIR, getPlayer(), 100 - getDowngradeEnchantChance()))) { item.addUnsafeEnchantment(enchantment, enchantLevel - 1); downgraded = true; } diff --git a/src/main/java/com/gmail/nossr50/skills/salvage/SalvageManager.java b/src/main/java/com/gmail/nossr50/skills/salvage/SalvageManager.java index 4e4c04ff3..cffd2bf37 100644 --- a/src/main/java/com/gmail/nossr50/skills/salvage/SalvageManager.java +++ b/src/main/java/com/gmail/nossr50/skills/salvage/SalvageManager.java @@ -14,8 +14,7 @@ import com.gmail.nossr50.util.EventUtils; import com.gmail.nossr50.util.Misc; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.NotificationManager; -import com.gmail.nossr50.util.random.RandomChanceSkillStatic; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; import com.gmail.nossr50.util.skills.SkillUtils; import com.gmail.nossr50.util.sounds.SoundManager; @@ -63,14 +62,19 @@ public class SalvageManager extends SkillManager { } public void handleSalvage(Location location, ItemStack item) { - Player player = getPlayer(); + final Player player = getPlayer(); - Salvageable salvageable = mcMMO.getSalvageableManager().getSalvageable(item.getType()); - ItemMeta meta = item.getItemMeta(); - - if (meta != null && meta.isUnbreakable()) { - NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE_FAILED, "Anvil.Unbreakable"); - return; + final Salvageable salvageable = mcMMO.getSalvageableManager().getSalvageable(item.getType()); + final ItemMeta meta = item.getItemMeta(); + if (meta != null) { + if (meta.hasCustomModelData() && !mcMMO.p.getCustomItemSupportConfig().isCustomSalvageAllowed()) { + NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE_FAILED, "Anvil.Salvage.Reject.CustomModelData"); + return; + } + if (meta.isUnbreakable()) { + NotificationManager.sendPlayerInformation(player, NotificationType.SUBSKILL_MESSAGE_FAILED, "Anvil.Unbreakable"); + return; + } } // Permissions checks on material and item types @@ -121,7 +125,7 @@ public class SalvageManager extends SkillManager { for(int x = 0; x < potentialSalvageYield-1; x++) { - if(RandomChanceUtil.rollDice(chanceOfSuccess, 100)) { + if(ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.SALVAGE, player, chanceOfSuccess)) { chanceOfSuccess-=3; chanceOfSuccess = Math.max(chanceOfSuccess, 90); @@ -191,30 +195,6 @@ public class SalvageManager extends SkillManager { return RankUtils.getRank(getPlayer(), SubSkillType.SALVAGE_ARCANE_SALVAGE); } - /*public double getExtractFullEnchantChance() { - int skillLevel = getSkillLevel(); - - for (Tier tier : Tier.values()) { - if (skillLevel >= tier.getLevel()) { - return tier.getExtractFullEnchantChance(); - } - } - - return 0; - } - - public double getExtractPartialEnchantChance() { - int skillLevel = getSkillLevel(); - - for (Tier tier : Tier.values()) { - if (skillLevel >= tier.getLevel()) { - return tier.getExtractPartialEnchantChance(); - } - } - - return 0; - }*/ - public double getExtractFullEnchantChance() { if(Permissions.hasSalvageEnchantBypassPerk(getPlayer())) return 100.0D; @@ -252,12 +232,12 @@ public class SalvageManager extends SkillManager { if (!Salvage.arcaneSalvageEnchantLoss || Permissions.hasSalvageEnchantBypassPerk(player) - || RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getExtractFullEnchantChance(), getPlayer(), SubSkillType.SALVAGE_ARCANE_SALVAGE))) { + || ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.SALVAGE, player, getExtractFullEnchantChance())) { enchantMeta.addStoredEnchant(enchant.getKey(), enchantLevel, true); } else if (enchantLevel > 1 && Salvage.arcaneSalvageDowngrades - && RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getExtractPartialEnchantChance(), getPlayer(), SubSkillType.SALVAGE_ARCANE_SALVAGE))) { + && ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.SALVAGE, player, getExtractPartialEnchantChance())) { enchantMeta.addStoredEnchant(enchant.getKey(), enchantLevel - 1, true); downgraded = true; } else { diff --git a/src/main/java/com/gmail/nossr50/skills/smelting/SmeltingManager.java b/src/main/java/com/gmail/nossr50/skills/smelting/SmeltingManager.java index 98d5a5a6f..9bb4a6e39 100644 --- a/src/main/java/com/gmail/nossr50/skills/smelting/SmeltingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/smelting/SmeltingManager.java @@ -8,9 +8,8 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.skills.SkillManager; import com.gmail.nossr50.util.Permissions; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import org.bukkit.block.Furnace; import org.bukkit.event.inventory.FurnaceBurnEvent; import org.bukkit.event.inventory.FurnaceSmeltEvent; @@ -25,7 +24,7 @@ public class SmeltingManager extends SkillManager { public boolean isSecondSmeltSuccessful() { return Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.SMELTING_SECOND_SMELT) - && RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.SMELTING_SECOND_SMELT, getPlayer()); + && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.SMELTING_SECOND_SMELT, getPlayer()); } /** diff --git a/src/main/java/com/gmail/nossr50/skills/swords/SwordsManager.java b/src/main/java/com/gmail/nossr50/skills/swords/SwordsManager.java index a95f1b9a4..d68d98e8d 100644 --- a/src/main/java/com/gmail/nossr50/skills/swords/SwordsManager.java +++ b/src/main/java/com/gmail/nossr50/skills/swords/SwordsManager.java @@ -14,10 +14,9 @@ import com.gmail.nossr50.util.ItemUtils; import com.gmail.nossr50.util.MetadataConstants; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.NotificationManager; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.CombatUtils; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import org.bukkit.entity.Entity; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; @@ -76,7 +75,8 @@ public class SwordsManager extends SkillManager { return; //Don't apply bleed } - if (RandomChanceUtil.rollDice(mcMMO.p.getAdvancedConfig().getRuptureChanceToApplyOnHit(getRuptureRank()), 100)) { + double ruptureOdds = mcMMO.p.getAdvancedConfig().getRuptureChanceToApplyOnHit(getRuptureRank()); + if (ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.SWORDS, this.getPlayer(), ruptureOdds)) { if (target instanceof Player defender) { @@ -141,7 +141,8 @@ public class SwordsManager extends SkillManager { * @param damage The amount of damage initially dealt by the event */ public void counterAttackChecks(@NotNull LivingEntity attacker, double damage) { - if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.SWORDS_COUNTER_ATTACK, getPlayer())) { + + if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.SWORDS_COUNTER_ATTACK, getPlayer())) { CombatUtils.dealDamage(attacker, damage / Swords.counterAttackModifier, getPlayer()); NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Swords.Combat.Countered"); diff --git a/src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java b/src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java index 1359a041b..dcee2e0bd 100644 --- a/src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/taming/TamingManager.java @@ -15,11 +15,9 @@ import com.gmail.nossr50.skills.SkillManager; import com.gmail.nossr50.util.Misc; import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.NotificationManager; -import com.gmail.nossr50.util.random.RandomChanceSkillStatic; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.ParticleEffectUtils; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.sounds.SoundManager; import com.gmail.nossr50.util.sounds.SoundType; import com.gmail.nossr50.util.text.StringUtils; @@ -145,7 +143,7 @@ public class TamingManager extends SkillManager { * @param damage The damage being absorbed by the wolf */ public void fastFoodService(@NotNull Wolf wolf, double damage) { - if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_STATIC_CHANCE, SubSkillType.TAMING_FAST_FOOD_SERVICE, getPlayer())) { + if (!ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.TAMING_FAST_FOOD_SERVICE, getPlayer())) { return; } @@ -165,12 +163,6 @@ public class TamingManager extends SkillManager { * @param damage The initial damage */ public double gore(@NotNull LivingEntity target, double damage) { -// if (target instanceof Player) { -// NotificationManager.sendPlayerInformation((Player)target, NotificationType.SUBSKILL_MESSAGE, "Combat.StruckByGore"); -// } -// -// NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Combat.Gore"); - damage = (damage * Taming.goreModifier) - damage; return damage; @@ -270,7 +262,7 @@ public class TamingManager extends SkillManager { if(!RankUtils.hasUnlockedSubskill(getPlayer(), SubSkillType.TAMING_PUMMEL)) return; - if(!RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(mcMMO.p.getAdvancedConfig().getPummelChance(), getPlayer(), SubSkillType.TAMING_PUMMEL))) + if(!ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.TAMING, getPlayer(), mcMMO.p.getAdvancedConfig().getPummelChance())) return; ParticleEffectUtils.playGreaterImpactEffect(target); @@ -380,17 +372,12 @@ public class TamingManager extends SkillManager { } private void spawnCOTWEntity(CallOfTheWildType callOfTheWildType, Location spawnLocation, EntityType entityType) { - switch(callOfTheWildType) { - case CAT: + switch (callOfTheWildType) { + case CAT -> //Entity type is needed for cats because in 1.13 and below we spawn ocelots, in 1.14 and above we spawn cats - spawnCat(spawnLocation, entityType); - break; - case HORSE: - spawnHorse(spawnLocation); - break; - case WOLF: - spawnWolf(spawnLocation); - break; + spawnCat(spawnLocation, entityType); + case HORSE -> spawnHorse(spawnLocation); + case WOLF -> spawnWolf(spawnLocation); } } diff --git a/src/main/java/com/gmail/nossr50/skills/tridents/TridentsManager.java b/src/main/java/com/gmail/nossr50/skills/tridents/TridentsManager.java new file mode 100644 index 000000000..bd4296d0f --- /dev/null +++ b/src/main/java/com/gmail/nossr50/skills/tridents/TridentsManager.java @@ -0,0 +1,35 @@ +package com.gmail.nossr50.skills.tridents; + +import com.gmail.nossr50.datatypes.player.McMMOPlayer; +import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +import com.gmail.nossr50.datatypes.skills.SubSkillType; +import com.gmail.nossr50.datatypes.skills.ToolType; +import com.gmail.nossr50.skills.SkillManager; +import com.gmail.nossr50.util.Permissions; +import com.gmail.nossr50.util.skills.RankUtils; + +public class TridentsManager extends SkillManager { + public TridentsManager(McMMOPlayer mmoPlayer) { + super(mmoPlayer, PrimarySkillType.TRIDENTS); + } + + /** + * Checks if the player can activate the Super Ability for Tridents + * @return true if the player can activate the Super Ability, false otherwise + */ + public boolean canActivateAbility() { + return mmoPlayer.getToolPreparationMode(ToolType.TRIDENTS) && Permissions.tridentsSuper(getPlayer()); + } + + public double impaleDamageBonus() { + int rank = RankUtils.getRank(getPlayer(), SubSkillType.TRIDENTS_IMPALE); + + if(rank > 1) { + return (1.0D + (rank * .5D)); + } else if(rank == 1) { + return 1.0D; + } + + return 0.0D; + } +} diff --git a/src/main/java/com/gmail/nossr50/skills/unarmed/UnarmedManager.java b/src/main/java/com/gmail/nossr50/skills/unarmed/UnarmedManager.java index 93dc5eef5..dbc8060ef 100644 --- a/src/main/java/com/gmail/nossr50/skills/unarmed/UnarmedManager.java +++ b/src/main/java/com/gmail/nossr50/skills/unarmed/UnarmedManager.java @@ -12,9 +12,8 @@ import com.gmail.nossr50.skills.SkillManager; import com.gmail.nossr50.util.*; import com.gmail.nossr50.util.player.NotificationManager; import com.gmail.nossr50.util.player.UserManager; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import org.bukkit.Material; import org.bukkit.block.BlockState; import org.bukkit.entity.Item; @@ -68,7 +67,7 @@ public class UnarmedManager extends SkillManager { } public boolean blockCrackerCheck(@NotNull BlockState blockState) { - if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.ALWAYS_FIRES, SubSkillType.UNARMED_BLOCK_CRACKER, getPlayer())) { + if (!ProbabilityUtil.isNonRNGSkillActivationSuccessful(SubSkillType.UNARMED_BLOCK_CRACKER, getPlayer())) { return false; } @@ -99,7 +98,7 @@ public class UnarmedManager extends SkillManager { * @param defender The defending player */ public void disarmCheck(@NotNull Player defender) { - if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.UNARMED_DISARM, getPlayer()) && !hasIronGrip(defender)) { + if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.UNARMED_DISARM, getPlayer()) && !hasIronGrip(defender)) { if (EventUtils.callDisarmEvent(defender).isCancelled()) { return; } @@ -122,7 +121,7 @@ public class UnarmedManager extends SkillManager { * Check for arrow deflection. */ public boolean deflectCheck() { - if (RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.UNARMED_ARROW_DEFLECT, getPlayer())) { + if (ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.UNARMED_ARROW_DEFLECT, getPlayer())) { NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Combat.ArrowDeflect"); return true; } @@ -145,11 +144,11 @@ public class UnarmedManager extends SkillManager { * Handle the effects of the Iron Arm ability */ public double calculateSteelArmStyleDamage() { - if (!RandomChanceUtil.isActivationSuccessful(SkillActivationType.ALWAYS_FIRES, SubSkillType.UNARMED_STEEL_ARM_STYLE, getPlayer())) { - return 0; + if (ProbabilityUtil.isNonRNGSkillActivationSuccessful(SubSkillType.UNARMED_STEEL_ARM_STYLE, getPlayer())) { + return getSteelArmStyleDamage(); } - return getSteelArmStyleDamage(); + return 0; } public double getSteelArmStyleDamage() { @@ -179,7 +178,7 @@ public class UnarmedManager extends SkillManager { private boolean hasIronGrip(@NotNull Player defender) { if (!Misc.isNPCEntityExcludingVillagers(defender) && Permissions.isSubSkillEnabled(defender, SubSkillType.UNARMED_IRON_GRIP) - && RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.UNARMED_IRON_GRIP, defender)) { + && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.UNARMED_IRON_GRIP, defender)) { NotificationManager.sendPlayerInformation(defender, NotificationType.SUBSKILL_MESSAGE, "Unarmed.Ability.IronGrip.Defender"); NotificationManager.sendPlayerInformation(getPlayer(), NotificationType.SUBSKILL_MESSAGE, "Unarmed.Ability.IronGrip.Attacker"); diff --git a/src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java b/src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java index c59a7e7ac..dd198131e 100644 --- a/src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java @@ -14,10 +14,9 @@ import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.skills.SkillManager; import com.gmail.nossr50.util.*; import com.gmail.nossr50.util.player.NotificationManager; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import com.gmail.nossr50.util.skills.CombatUtils; import com.gmail.nossr50.util.skills.RankUtils; -import com.gmail.nossr50.util.skills.SkillActivationType; import com.gmail.nossr50.util.skills.SkillUtils; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -35,7 +34,9 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +//TODO: Seems to not be using the item drop event for bonus drops, may want to change that.. or may not be able to be changed? public class WoodcuttingManager extends SkillManager { private boolean treeFellerReachedThreshold = false; private static int treeFellerThreshold; //TODO: Shared setting, will be removed in 2.2 @@ -68,21 +69,46 @@ public class WoodcuttingManager extends SkillManager { && ItemUtils.isAxe(heldItem); } - private boolean checkHarvestLumberActivation(@NotNull Material material) { + private boolean checkHarvestLumberActivation(Material material) { return Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER) && RankUtils.hasReachedRank(1, getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER) - && RandomChanceUtil.isActivationSuccessful(SkillActivationType.RANDOM_LINEAR_100_SCALE_WITH_CAP, SubSkillType.WOODCUTTING_HARVEST_LUMBER, getPlayer()) + && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.WOODCUTTING_HARVEST_LUMBER, getPlayer()) + && mcMMO.p.getGeneralConfig().getDoubleDropsEnabled(PrimarySkillType.WOODCUTTING, material); + } + + private boolean checkCleanCutsActivation(Material material) { + return Permissions.isSubSkillEnabled(getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER) + && RankUtils.hasReachedRank(1, getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER) + && ProbabilityUtil.isSkillRNGSuccessful(SubSkillType.WOODCUTTING_CLEAN_CUTS, getPlayer()) && mcMMO.p.getGeneralConfig().getDoubleDropsEnabled(PrimarySkillType.WOODCUTTING, material); } /** - * Begins Woodcutting + * Processes bonus drops for a block * * @param blockState Block being broken */ - public void processHarvestLumber(@NotNull BlockState blockState) { - if (checkHarvestLumberActivation(blockState.getType())) { - spawnHarvestLumberBonusDrops(blockState); + public void processBonusDropCheck(@NotNull BlockState blockState) { + //TODO: Why isn't this using the item drop event? Potentially because of Tree Feller? This should be adjusted either way. + if(mcMMO.p.getGeneralConfig().getDoubleDropsEnabled(PrimarySkillType.WOODCUTTING, blockState.getType())) { + //Mastery enabled for player + if(Permissions.canUseSubSkill(getPlayer(), SubSkillType.WOODCUTTING_CLEAN_CUTS)) { + if(checkCleanCutsActivation(blockState.getType())) { + //Triple drops + spawnHarvestLumberBonusDrops(blockState); + spawnHarvestLumberBonusDrops(blockState); + } else { + //Harvest Lumber Check + if(checkHarvestLumberActivation(blockState.getType())) { + spawnHarvestLumberBonusDrops(blockState); + } + } + //No Mastery (no Clean Cuts) + } else if (Permissions.canUseSubSkill(getPlayer(), SubSkillType.WOODCUTTING_HARVEST_LUMBER)) { + if(checkHarvestLumberActivation(blockState.getType())) { + spawnHarvestLumberBonusDrops(blockState); + } + } } } @@ -295,24 +321,23 @@ public class WoodcuttingManager extends SkillManager { Misc.spawnItemsFromCollection(getPlayer(), Misc.getBlockCenter(blockState), block.getDrops(itemStack), ItemSpawnReason.TREE_FELLER_DISPLACED_BLOCK); //Bonus Drops / Harvest lumber checks - processHarvestLumber(blockState); + processBonusDropCheck(blockState); } else if (BlockUtils.isNonWoodPartOfTree(blockState)) { - //Drop displaced non-woodcutting XP blocks - - if(RankUtils.hasUnlockedSubskill(player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD)) { + // 90% of the time do not drop leaf blocks + if (ThreadLocalRandom.current().nextInt(100) > 90) { Misc.spawnItemsFromCollection(getPlayer(), Misc.getBlockCenter(blockState), block.getDrops(itemStack), ItemSpawnReason.TREE_FELLER_DISPLACED_BLOCK); + } + //Drop displaced non-woodcutting XP blocks + if(RankUtils.hasUnlockedSubskill(player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD)) { if(RankUtils.hasReachedRank(2, player, SubSkillType.WOODCUTTING_KNOCK_ON_WOOD)) { if(mcMMO.p.getAdvancedConfig().isKnockOnWoodXPOrbEnabled()) { - if(RandomChanceUtil.rollDice(10, 100)) { + if(ProbabilityUtil.isStaticSkillRNGSuccessful(PrimarySkillType.WOODCUTTING, player, 10)) { int randOrbCount = Math.max(1, Misc.getRandom().nextInt(100)); Misc.spawnExperienceOrb(blockState.getLocation(), randOrbCount); } } } - - } else { - Misc.spawnItemsFromCollection(getPlayer(), Misc.getBlockCenter(blockState), block.getDrops(itemStack), ItemSpawnReason.TREE_FELLER_DISPLACED_BLOCK, 1); } } @@ -381,6 +406,10 @@ public class WoodcuttingManager extends SkillManager { * @param blockState Block being broken */ protected void spawnHarvestLumberBonusDrops(@NotNull BlockState blockState) { - Misc.spawnItemsFromCollection(getPlayer(), Misc.getBlockCenter(blockState), blockState.getBlock().getDrops(getPlayer().getInventory().getItemInMainHand()), ItemSpawnReason.BONUS_DROPS); + Misc.spawnItemsFromCollection( + getPlayer(), + Misc.getBlockCenter(blockState), + blockState.getBlock().getDrops(getPlayer().getInventory().getItemInMainHand()), + ItemSpawnReason.BONUS_DROPS); } } diff --git a/src/main/java/com/gmail/nossr50/util/BlockUtils.java b/src/main/java/com/gmail/nossr50/util/BlockUtils.java index 7ef3cbc81..6edb342a5 100644 --- a/src/main/java/com/gmail/nossr50/util/BlockUtils.java +++ b/src/main/java/com/gmail/nossr50/util/BlockUtils.java @@ -7,8 +7,7 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.skills.repair.Repair; import com.gmail.nossr50.skills.salvage.Salvage; -import com.gmail.nossr50.util.random.RandomChanceSkill; -import com.gmail.nossr50.util.random.RandomChanceUtil; +import com.gmail.nossr50.util.random.ProbabilityUtil; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.block.Block; @@ -98,7 +97,7 @@ public final class BlockUtils { */ public static boolean checkDoubleDrops(Player player, BlockState blockState, PrimarySkillType skillType, SubSkillType subSkillType) { if (mcMMO.p.getGeneralConfig().getDoubleDropsEnabled(skillType, blockState.getType()) && Permissions.isSubSkillEnabled(player, subSkillType)) { - return RandomChanceUtil.checkRandomChanceExecutionSuccess(new RandomChanceSkill(player, subSkillType, true)); + return ProbabilityUtil.isSkillRNGSuccessful(subSkillType, player); } return false; @@ -231,22 +230,22 @@ public final class BlockUtils { return mcMMO.getMaterialMapStore().isTreeFellerDestructible(material); } - /** - * Determine if a given block should be affected by Flux Mining - * - * @param blockState The {@link BlockState} of the block to check - * @return true if the block should affected by Flux Mining, false otherwise - */ - public static boolean affectedByFluxMining(BlockState blockState) { - switch (blockState.getType()) { - case IRON_ORE: - case GOLD_ORE: - return true; - - default: - return false; - } - } +// /** +// * Determine if a given block should be affected by Flux Mining +// * +// * @param blockState The {@link BlockState} of the block to check +// * @return true if the block should affected by Flux Mining, false otherwise +// */ +// public static boolean affectedByFluxMining(BlockState blockState) { +// switch (blockState.getType()) { +// case IRON_ORE: +// case GOLD_ORE: +// return true; +// +// default: +// return false; +// } +// } /** * Determine if a given block can activate Herbalism abilities diff --git a/src/main/java/com/gmail/nossr50/util/EventUtils.java b/src/main/java/com/gmail/nossr50/util/EventUtils.java index c28467e65..f53fde6df 100644 --- a/src/main/java/com/gmail/nossr50/util/EventUtils.java +++ b/src/main/java/com/gmail/nossr50/util/EventUtils.java @@ -183,8 +183,7 @@ public final class EventUtils { * @param subSkillType target subskill * @return the event after it has been fired */ - @Deprecated - public static @NotNull SubSkillEvent callSubSkillEvent(Player player, SubSkillType subSkillType) { + public static @NotNull SubSkillEvent callSubSkillEvent(@NotNull Player player, @NotNull SubSkillType subSkillType) { SubSkillEvent event = new SubSkillEvent(player, subSkillType); mcMMO.p.getServer().getPluginManager().callEvent(event); @@ -198,7 +197,6 @@ public final class EventUtils { * @param block associated block * @return the event after it has been fired */ - @Deprecated public static @NotNull SubSkillBlockEvent callSubSkillBlockEvent(@NotNull Player player, @NotNull SubSkillType subSkillType, @NotNull Block block) { SubSkillBlockEvent event = new SubSkillBlockEvent(player, subSkillType, block); mcMMO.p.getServer().getPluginManager().callEvent(event); @@ -399,7 +397,7 @@ public final class EventUtils { McMMOPlayer mmoPlayer = UserManager.getPlayer(player); if(mmoPlayer == null) return true; - + McMMOPlayerXpGainEvent event = new McMMOPlayerXpGainEvent(player, skill, xpGained, xpGainReason); mcMMO.p.getServer().getPluginManager().callEvent(event); diff --git a/src/main/java/com/gmail/nossr50/util/ItemUtils.java b/src/main/java/com/gmail/nossr50/util/ItemUtils.java index 4787e0159..6055582e3 100644 --- a/src/main/java/com/gmail/nossr50/util/ItemUtils.java +++ b/src/main/java/com/gmail/nossr50/util/ItemUtils.java @@ -36,14 +36,70 @@ public final class ItemUtils { * @param item Item to check * @return true if the item is a bow, false otherwise */ + // TODO: Unit tests public static boolean isBow(@NotNull ItemStack item) { return mcMMO.getMaterialMapStore().isBow(item.getType().getKey().getKey()); } + /** + * Checks if a player has an item in their inventory or offhand. + * + * @param player Player to check + * @param material Material to check for + * @return true if the player has the item in their inventory or offhand, false otherwise + */ + public static boolean hasItemIncludingOffHand(Player player, Material material) { + // Checks main inventory / item bar + boolean containsInMain = player.getInventory().contains(material); + + if (containsInMain) { + return true; + } + + return player.getInventory().getItemInOffHand().getType() == material; + } + + /** + * Removes an item from a player's inventory, including their offhand. + * + * @param player Player to remove the item from + * @param material Material to remove + * @param amount Amount of the material to remove + */ + public static void removeItemIncludingOffHand(@NotNull Player player, @NotNull Material material, int amount) { + // Checks main inventory / item bar + if (player.getInventory().contains(material)) { + player.getInventory().removeItem(new ItemStack(material, amount)); + return; + } + + // Check off-hand + final ItemStack offHandItem = player.getInventory().getItemInOffHand(); + if (offHandItem.getType() == material) { + int newAmount = offHandItem.getAmount() - amount; + if (newAmount > 0) { + offHandItem.setAmount(newAmount); + } else { + player.getInventory().setItemInOffHand(new ItemStack(Material.AIR)); + } + } + } + + // TODO: Unit tests public static boolean isCrossbow(@NotNull ItemStack item) { return mcMMO.getMaterialMapStore().isCrossbow(item.getType().getKey().getKey()); } + // TODO: Unit tests + public static boolean isBowOrCrossbow(@NotNull ItemStack item) { + return isBow(item) || isCrossbow(item); + } + + // TODO: Unit tests + public static boolean isTrident(@NotNull ItemStack item) { + return mcMMO.getMaterialMapStore().isTrident(item.getType().getKey().getKey()); + } + public static boolean hasItemInEitherHand(@NotNull Player player, Material material) { return player.getInventory().getItemInMainHand().getType() == material || player.getInventory().getItemInOffHand().getType() == material; } diff --git a/src/main/java/com/gmail/nossr50/util/MaterialMapStore.java b/src/main/java/com/gmail/nossr50/util/MaterialMapStore.java index 6a54cd608..b0f623602 100644 --- a/src/main/java/com/gmail/nossr50/util/MaterialMapStore.java +++ b/src/main/java/com/gmail/nossr50/util/MaterialMapStore.java @@ -10,9 +10,6 @@ import java.util.Locale; /** * Stores hash tables for item and block names * This allows for better support across multiple versions of Minecraft - * - * This is a temporary class, mcMMO is spaghetti and I'l clean it up later - * */ public class MaterialMapStore { @@ -52,7 +49,6 @@ public class MaterialMapStore { private final @NotNull HashSet bows; private final @NotNull HashSet crossbows; private final @NotNull HashSet tools; - private final @NotNull HashSet enchantables; private final @NotNull HashSet ores; @@ -820,6 +816,14 @@ public class MaterialMapStore { return crossbows.contains(id); } + public boolean isTrident(@NotNull Material material) { + return isTrident(material.getKey().getKey()); + } + + public boolean isTrident(@NotNull String id) { + return tridents.contains(id); + } + public boolean isLeatherArmor(@NotNull Material material) { return isLeatherArmor(material.getKey().getKey()); } diff --git a/src/main/java/com/gmail/nossr50/util/MetadataConstants.java b/src/main/java/com/gmail/nossr50/util/MetadataConstants.java index 090e04d17..39f9479ce 100644 --- a/src/main/java/com/gmail/nossr50/util/MetadataConstants.java +++ b/src/main/java/com/gmail/nossr50/util/MetadataConstants.java @@ -14,6 +14,9 @@ public class MetadataConstants { * Take great care if you ever modify the value of these keys */ public static final @NotNull String METADATA_KEY_REPLANT = "mcMMO: Recently Replanted"; + public static final @NotNull String METADATA_KEY_SPAWNED_ARROW = "mcMMO: Spawned Arrow"; + public static final @NotNull String METADATA_KEY_MULTI_SHOT_ARROW = "mcMMO: Multi-shot Arrow"; + public static final @NotNull String METADATA_KEY_BOUNCE_COUNT = "mcMMO: Arrow Bounce Count"; public static final @NotNull String METADATA_KEY_EXPLOSION_FROM_RUPTURE = "mcMMO: Rupture Explosion"; public static final @NotNull String METADATA_KEY_FISH_HOOK_REF = "mcMMO: Fish Hook Tracker"; public static final @NotNull String METADATA_KEY_DODGE_TRACKER = "mcMMO: Dodge Tracker"; diff --git a/src/main/java/com/gmail/nossr50/util/Misc.java b/src/main/java/com/gmail/nossr50/util/Misc.java index bbd9bb999..2467a5540 100644 --- a/src/main/java/com/gmail/nossr50/util/Misc.java +++ b/src/main/java/com/gmail/nossr50/util/Misc.java @@ -343,4 +343,26 @@ public final class Misc { experienceOrb.setExperience(orbExpValue); } } + +// public static void hackyUnitTest(@NotNull McMMOPlayer normalPlayer) { +// mcMMO.p.getLogger().info("Starting hacky unit test..."); +// int iterations = 1000000; +// double ratioDivisor = 10000; //10000 because we run the test 1,000,000 times +// double expectedFailRate = 100.0D - RandomChanceUtil.getRandomChanceExecutionSuccess(normalPlayer.getPlayer(), SubSkillType.MINING_MOTHER_LODE, true); +// +// double win = 0, loss = 0; +// for(int x = 0; x < iterations; x++) { +// if(RandomChanceUtil.checkRandomChanceExecutionSuccess(normalPlayer.getPlayer(), SubSkillType.MINING_MOTHER_LODE, true)) { +// win++; +// } else { +// loss++; +// } +// } +// +// double lossRatio = (loss / ratioDivisor); +// mcMMO.p.getLogger().info("Expected Fail Rate: "+expectedFailRate); +// mcMMO.p.getLogger().info("Loss Ratio for hacky test: "+lossRatio); +//// Assert.assertEquals(lossRatio, expectedFailRate, 0.01D); +// } + } diff --git a/src/main/java/com/gmail/nossr50/util/MobHealthbarUtils.java b/src/main/java/com/gmail/nossr50/util/MobHealthbarUtils.java index 3d956afe7..5da29a656 100644 --- a/src/main/java/com/gmail/nossr50/util/MobHealthbarUtils.java +++ b/src/main/java/com/gmail/nossr50/util/MobHealthbarUtils.java @@ -26,7 +26,7 @@ public final class MobHealthbarUtils { EntityDamageEvent lastDamageCause = player.getLastDamageCause(); String replaceString = lastDamageCause instanceof EntityDamageByEntityEvent ? StringUtils.getPrettyEntityTypeString(((EntityDamageByEntityEvent) lastDamageCause).getDamager().getType()) : "a mob"; - return deathMessage.replaceAll("(?:(\u00A7(?:[0-9A-FK-ORa-fk-or]))*(?:[\u2764\u25A0]{1,10})){1,2}", replaceString); + return deathMessage.replaceAll("(?:(§(?:[0-9A-FK-ORa-fk-or]))*(?:[❤■]{1,10})){1,2}", replaceString); } /** diff --git a/src/main/java/com/gmail/nossr50/util/Permissions.java b/src/main/java/com/gmail/nossr50/util/Permissions.java index c405f84ab..6dcd9b800 100644 --- a/src/main/java/com/gmail/nossr50/util/Permissions.java +++ b/src/main/java/com/gmail/nossr50/util/Permissions.java @@ -5,16 +5,18 @@ import com.gmail.nossr50.datatypes.skills.ItemType; import com.gmail.nossr50.datatypes.skills.MaterialType; import com.gmail.nossr50.datatypes.skills.PrimarySkillType; import com.gmail.nossr50.datatypes.skills.SubSkillType; -import com.gmail.nossr50.datatypes.skills.subskills.AbstractSubSkill; import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.util.skills.RankUtils; import org.bukkit.Material; import org.bukkit.Server; import org.bukkit.World; import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; import org.bukkit.permissions.Permissible; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; import org.bukkit.plugin.PluginManager; +import org.jetbrains.annotations.NotNull; import java.util.Locale; @@ -164,10 +166,14 @@ public final class Permissions { * SKILLS */ - public static boolean skillEnabled(Permissible permissible, PrimarySkillType skill) {return permissible.hasPermission("mcmmo.skills." + skill.toString().toLowerCase(Locale.ENGLISH)); } + public static boolean skillEnabled(Permissible permissible, PrimarySkillType skill) { + return permissible.hasPermission("mcmmo.skills." + skill.toString().toLowerCase(Locale.ENGLISH)); + } + public static boolean vanillaXpBoost(Permissible permissible, PrimarySkillType skill) { return permissible.hasPermission("mcmmo.ability." + skill.toString().toLowerCase(Locale.ENGLISH) + ".vanillaxpboost"); } - public static boolean isSubSkillEnabled(Permissible permissible, SubSkillType subSkillType) { return permissible.hasPermission(subSkillType.getPermissionNodeAddress()); } - public static boolean isSubSkillEnabled(Permissible permissible, AbstractSubSkill abstractSubSkill) { return permissible.hasPermission(abstractSubSkill.getPermissionNode()); } + public static boolean isSubSkillEnabled(Permissible permissible, SubSkillType subSkillType) { + return permissible.hasPermission(subSkillType.getPermissionNodeAddress()); + } /* ACROBATICS */ public static boolean dodge(Permissible permissible) { return permissible.hasPermission("mcmmo.ability.acrobatics.dodge"); } @@ -179,6 +185,7 @@ public final class Permissions { public static boolean concoctions(Permissible permissible) { return permissible.hasPermission("mcmmo.ability.alchemy.concoctions"); } /* ARCHERY */ + public static boolean explosiveShot(Permissible permissible) { return permissible.hasPermission("mcmmo.ability.archery.explosiveshot"); } public static boolean arrowRetrieval(Permissible permissible) { return permissible.hasPermission("mcmmo.ability.archery.trackarrows"); } public static boolean daze(Permissible permissible) { return permissible.hasPermission("mcmmo.ability.archery.daze"); } @@ -225,6 +232,20 @@ public final class Permissions { /* WOODCUTTING */ public static boolean treeFeller(Permissible permissible) { return permissible.hasPermission("mcmmo.ability.woodcutting.treefeller"); } + /* CROSSBOWS */ + public static boolean superShotgun(Permissible permissible) { + return permissible.hasPermission("mcmmo.ability.crossbows.supershotgun"); + } + public static boolean trickShot(Permissible permissible) { return permissible.hasPermission("mcmmo.ability.crossbows.trickshot"); } + public static boolean poweredShot(Permissible permissible) { return permissible.hasPermission("mcmmo.ability.crossbows.poweredshot"); } + + /* TRIDENTS */ + public static boolean tridentsSuper(Permissible permissible) { + return false; + // return permissible.hasPermission("mcmmo.ability.tridents.superability"); + } + public static boolean tridentsLimitBreak(Permissible permissible) { return permissible.hasPermission("mcmmo.ability.tridents.superability"); } + /* * PARTY */ @@ -256,4 +277,15 @@ public final class Permissions { permission.setDefault(permissionDefault); pluginManager.addPermission(permission); } + + /** + * Checks if a player can use a skill + * + * @param player target player + * @param subSkillType target subskill + * @return true if the player has permission and has the skill unlocked + */ + public static boolean canUseSubSkill(@NotNull Player player, @NotNull SubSkillType subSkillType) { + return isSubSkillEnabled(player, subSkillType) && RankUtils.hasUnlockedSubskill(player, subSkillType); + } } diff --git a/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java b/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java index 371124b06..0946b4cc5 100644 --- a/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java +++ b/src/main/java/com/gmail/nossr50/util/commands/CommandRegistrationManager.java @@ -42,8 +42,8 @@ public final class CommandRegistrationManager { command.setDescription(LocaleLoader.getString("Commands.Description.Skill", StringUtils.getCapitalized(localizedName))); command.setPermission("mcmmo.commands." + commandName); command.setPermissionMessage(permissionsMessage); - command.setUsage(LocaleLoader.getString("Commands.Usage.0", localizedName)); - command.setUsage(command.getUsage() + "\n" + LocaleLoader.getString("Commands.Usage.2", localizedName, "?", "[" + LocaleLoader.getString("Commands.Usage.Page") + "]")); + command.setUsage(LocaleLoader.getString("Commands.Usage.0", commandName)); + command.setUsage(command.getUsage() + "\n" + LocaleLoader.getString("Commands.Usage.2", commandName, "?", "[" + LocaleLoader.getString("Commands.Usage.Page") + "]")); switch (skill) { case ACROBATICS: @@ -61,6 +61,9 @@ public final class CommandRegistrationManager { case AXES: command.setExecutor(new AxesCommand()); break; + case CROSSBOWS: + command.setExecutor(new CrossbowsCommand()); + break; case EXCAVATION: command.setExecutor(new ExcavationCommand()); @@ -97,6 +100,9 @@ public final class CommandRegistrationManager { case TAMING: command.setExecutor(new TamingCommand()); break; + case TRIDENTS: + command.setExecutor(new TridentsCommand()); + break; case UNARMED: command.setExecutor(new UnarmedCommand()); diff --git a/src/main/java/com/gmail/nossr50/util/random/InvalidActivationException.java b/src/main/java/com/gmail/nossr50/util/random/InvalidActivationException.java deleted file mode 100644 index 677d3e63e..000000000 --- a/src/main/java/com/gmail/nossr50/util/random/InvalidActivationException.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.gmail.nossr50.util.random; - -public class InvalidActivationException extends Exception { - //Weee -} diff --git a/src/main/java/com/gmail/nossr50/util/random/Probability.java b/src/main/java/com/gmail/nossr50/util/random/Probability.java new file mode 100644 index 000000000..3b9225b97 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/util/random/Probability.java @@ -0,0 +1,97 @@ +package com.gmail.nossr50.util.random; + +import com.gmail.nossr50.api.exceptions.ValueOutOfBoundsException; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.ThreadLocalRandom; + +public interface Probability { + /** + * A Probability that always fails. + */ + Probability ALWAYS_FAILS = () -> 0; + + /** + * A Probability that always succeeds. + */ + Probability ALWAYS_SUCCEEDS = () -> 1; + + /** + * The value of this Probability + * Should return a result between 0 and 1 (inclusive) + * A value of 1 or greater represents something that will always succeed + * A value of around 0.5 represents something that succeeds around half the time + * A value of 0 represents something that will always fail + * + * @return the value of probability + */ + double getValue(); + + /** + * Create a new Probability of a percentage. + * This method takes a percentage and creates a Probability of equivalent odds. + * + * A value of 100 would represent 100% chance of success, + * A value of 50 would represent 50% chance of success, + * A value of 0 would represent 0% chance of success, + * A value of 1 would represent 1% chance of success, + * A value of 0.5 would represent 0.5% chance of success, + * A value of 0.01 would represent 0.01% chance of success. + * + * @param percentage the value of the probability + * @return a new Probability with the given value + */ + static @NotNull Probability ofPercent(double percentage) { + if (percentage < 0) { + throw new ValueOutOfBoundsException("Value should never be negative for Probability! This suggests a coding mistake, contact the devs!"); + } + + // Convert to a 0-1 floating point representation + double probabilityValue = percentage / 100.0D; + return new ProbabilityImpl(probabilityValue); + } + + /** + * Create a new Probability of a value. + * This method takes a value between 0 and 1 and creates a Probability of equivalent odds. + * A value of 1 or greater represents something that will always succeed. + * A value of around 0.5 represents something that succeeds around half the time. + * A value of 0 represents something that will always fail. + * @param value the value of the probability + * @return a new Probability with the given value + */ + static @NotNull Probability ofValue(double value) { + return new ProbabilityImpl(value); + } + + /** + * Simulates a "roll of the dice" + * If the value passed is higher than the "random" value, than it is a successful roll + * + * @param probabilityValue probability value + * @return true for succeeding, false for failing + */ + static private boolean isSuccessfulRoll(double probabilityValue) { + return (probabilityValue) >= ThreadLocalRandom.current().nextDouble(1D); + } + + /** + * Simulate an outcome on a probability and return true or false for the result of that outcome + * + * @return true if the probability succeeded, false if it failed + */ + default boolean evaluate() { + return isSuccessfulRoll(getValue()); + } + + /** + * Modify and then Simulate an outcome on a probability and return true or false for the result of that outcome + * + * @param probabilityMultiplier probability will be multiplied by this before success is checked + * @return true if the probability succeeded, false if it failed + */ + default boolean evaluate(double probabilityMultiplier) { + double probabilityValue = getValue() * probabilityMultiplier; + return isSuccessfulRoll(probabilityValue); + } +} diff --git a/src/main/java/com/gmail/nossr50/util/random/ProbabilityImpl.java b/src/main/java/com/gmail/nossr50/util/random/ProbabilityImpl.java new file mode 100644 index 000000000..b62110567 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/util/random/ProbabilityImpl.java @@ -0,0 +1,53 @@ +package com.gmail.nossr50.util.random; + +import com.gmail.nossr50.api.exceptions.ValueOutOfBoundsException; +import com.google.common.base.Objects; + +public class ProbabilityImpl implements Probability { + + private final double probabilityValue; + + /** + * Create a probability from a static value. + * A value of 0 represents a 0% chance of success, + * A value of 1 represents a 100% chance of success. + * A value of 0.5 represents a 50% chance of success. + * A value of 0.01 represents a 1% chance of success. + * And so on. + * + * @param value the value of the probability between 0 and 100 + */ + public ProbabilityImpl(double value) throws ValueOutOfBoundsException { + if (value < 0) { + throw new ValueOutOfBoundsException("Value should never be negative for Probability!" + + " This suggests a coding mistake, contact the devs!"); + } + + probabilityValue = value; + } + + @Override + public double getValue() { + return probabilityValue; + } + + @Override + public String toString() { + return "ProbabilityImpl{" + + "probabilityValue=" + probabilityValue + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProbabilityImpl that = (ProbabilityImpl) o; + return Double.compare(that.probabilityValue, probabilityValue) == 0; + } + + @Override + public int hashCode() { + return Objects.hashCode(probabilityValue); + } +} diff --git a/src/main/java/com/gmail/nossr50/util/random/ProbabilityUtil.java b/src/main/java/com/gmail/nossr50/util/random/ProbabilityUtil.java new file mode 100644 index 000000000..4836efedc --- /dev/null +++ b/src/main/java/com/gmail/nossr50/util/random/ProbabilityUtil.java @@ -0,0 +1,268 @@ +package com.gmail.nossr50.util.random; + +import com.gmail.nossr50.datatypes.player.McMMOPlayer; +import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +import com.gmail.nossr50.datatypes.skills.SubSkillType; +import com.gmail.nossr50.events.skills.secondaryabilities.SubSkillEvent; +import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.util.EventUtils; +import com.gmail.nossr50.util.Permissions; +import com.gmail.nossr50.util.player.UserManager; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.text.DecimalFormat; + +public class ProbabilityUtil { + public static final @NotNull DecimalFormat percent = new DecimalFormat("##0.00%"); + public static final double LUCKY_MODIFIER = 1.333D; + + /** + * Return a chance of success in "percentage" format, show to the player in UI elements + * + * @param player target player + * @param subSkillType target subskill + * @param isLucky whether to apply luck modifiers + * + * @return "percentage" representation of success + */ + public static double chanceOfSuccessPercentage(@NotNull Player player, + @NotNull SubSkillType subSkillType, + boolean isLucky) { + Probability probability = getSubSkillProbability(subSkillType, player); + //Probability values are on a 0-1 scale and need to be "transformed" into a 1-100 scale + double percentageValue = probability.getValue(); //Doesn't need to be scaled + + //Apply lucky modifier + if(isLucky) { + percentageValue *= LUCKY_MODIFIER; + } + + return percentageValue; + } + + public static double chanceOfSuccessPercentage(@NotNull Probability probability, boolean isLucky) { + //Probability values are on a 0-1 scale and need to be "transformed" into a 1-100 scale + double percentageValue = probability.getValue(); + + //Apply lucky modifier + if(isLucky) { + percentageValue *= LUCKY_MODIFIER; + } + + return percentageValue; + } + + static Probability getStaticRandomChance(@NotNull SubSkillType subSkillType) throws InvalidStaticChance { + return switch (subSkillType) { + case AXES_ARMOR_IMPACT -> Probability.ofPercent(mcMMO.p.getAdvancedConfig().getImpactChance()); + case AXES_GREATER_IMPACT -> Probability.ofPercent(mcMMO.p.getAdvancedConfig().getGreaterImpactChance()); + case TAMING_FAST_FOOD_SERVICE -> Probability.ofPercent(mcMMO.p.getAdvancedConfig().getFastFoodChance()); + default -> throw new InvalidStaticChance(); + }; + } + + static SkillProbabilityType getProbabilityType(@NotNull SubSkillType subSkillType) { + SkillProbabilityType skillProbabilityType = SkillProbabilityType.DYNAMIC_CONFIGURABLE; + + if(subSkillType == SubSkillType.TAMING_FAST_FOOD_SERVICE + || subSkillType == SubSkillType.AXES_ARMOR_IMPACT + || subSkillType == SubSkillType.AXES_GREATER_IMPACT) + skillProbabilityType = SkillProbabilityType.STATIC_CONFIGURABLE; + + return skillProbabilityType; + } + + static @NotNull Probability ofSubSkill(@Nullable Player player, + @NotNull SubSkillType subSkillType) { + switch (getProbabilityType(subSkillType)) { + case DYNAMIC_CONFIGURABLE: + double probabilityCeiling; + double skillLevel; + double maxBonusLevel; // If a skill level is equal to the cap, it has the full probability + + if (player != null) { + McMMOPlayer mmoPlayer = UserManager.getPlayer(player); + if (mmoPlayer == null) { + return Probability.ofPercent(0); + } + skillLevel = mmoPlayer.getSkillLevel(subSkillType.getParentSkill()); + } else { + skillLevel = 0; + } + + //Probability ceiling is configurable in this type + probabilityCeiling = mcMMO.p.getAdvancedConfig().getMaximumProbability(subSkillType); + //The xCeiling is configurable in this type + maxBonusLevel = mcMMO.p.getAdvancedConfig().getMaxBonusLevel(subSkillType); + return calculateCurrentSkillProbability(skillLevel, 0, probabilityCeiling, maxBonusLevel); + case STATIC_CONFIGURABLE: + try { + return getStaticRandomChance(subSkillType); + } catch (InvalidStaticChance invalidStaticChance) { + invalidStaticChance.printStackTrace(); + } + default: + throw new RuntimeException("No case in switch statement for Skill Probability Type!"); + } + } + + /** + * This is one of several Skill RNG check methods + * This helper method is for specific {@link SubSkillType}, which help mcMMO understand where the RNG values used in our calculations come from this {@link SubSkillType} + *

+ * 1) Determine where the RNG values come from for the passed {@link SubSkillType} + * NOTE: In the config file, there are values which are static and which are more dynamic, this is currently a bit hardcoded and will need to be updated manually + *

+ * 2) Determine whether to use Lucky multiplier and influence the outcome + *

+ * 3) Creates a {@link Probability} and pipes it to {@link ProbabilityUtil} which processes the result and returns it + *

+ * This also calls a {@link SubSkillEvent} which can be cancelled, if it is cancelled this will return false + * The outcome of the probability can also be modified by this event that is called + * + * @param subSkillType target subskill + * @param player target player, can be null (null players are given odds equivalent to a player with no levels or luck) + * @return true if the Skill RNG succeeds, false if it fails + */ + public static boolean isSkillRNGSuccessful(@NotNull SubSkillType subSkillType, @NotNull Player player) { + final Probability probability = getSkillProbability(subSkillType, player); + + //Luck + boolean isLucky = Permissions.lucky(player, subSkillType.getParentSkill()); + + if(isLucky) { + return probability.evaluate(LUCKY_MODIFIER); + } else { + return probability.evaluate(); + } + } + + /** + * Returns the {@link Probability} for a specific {@link SubSkillType} for a specific {@link Player}. + * This does not take into account perks such as lucky for the player. + * This is affected by other plugins who can listen to the {@link SubSkillEvent} and cancel it or mutate it. + * + * @param subSkillType the target subskill + * @param player the target player + * @return the probability for this skill + */ + public static Probability getSkillProbability(@NotNull SubSkillType subSkillType, @NotNull Player player) { + //Process probability + Probability probability = getSubSkillProbability(subSkillType, player); + + //Send out event + SubSkillEvent subSkillEvent = EventUtils.callSubSkillEvent(player, subSkillType); + + if(subSkillEvent.isCancelled()) { + return Probability.ALWAYS_FAILS; + } + + //Result modifier + double resultModifier = subSkillEvent.getResultModifier(); + + //Mutate probability + if(resultModifier != 1.0D) + probability = Probability.ofPercent(probability.getValue() * resultModifier); + + return probability; + } + + /** + * This is one of several Skill RNG check methods + * This helper method is specific to static value RNG, which can be influenced by a player's Luck + * + * @param primarySkillType the related primary skill + * @param player the target player can be null (null players have the worst odds) + * @param probabilityPercentage the probability of this player succeeding in "percentage" format (0-100 inclusive) + * @return true if the RNG succeeds, false if it fails + */ + public static boolean isStaticSkillRNGSuccessful(@NotNull PrimarySkillType primarySkillType, @Nullable Player player, double probabilityPercentage) { + //Grab a probability converted from a "percentage" value + Probability probability = Probability.ofPercent(probabilityPercentage); + + return isStaticSkillRNGSuccessful(primarySkillType, player, probability); + } + + /** + * This is one of several Skill RNG check methods + * This helper method is specific to static value RNG, which can be influenced by a player's Luck + * + * @param primarySkillType the related primary skill + * @param player the target player, can be null (null players have the worst odds) + * @param probability the probability of this player succeeding + * @return true if the RNG succeeds, false if it fails + */ + public static boolean isStaticSkillRNGSuccessful(@NotNull PrimarySkillType primarySkillType, @Nullable Player player, @NotNull Probability probability) { + boolean isLucky = player != null && Permissions.lucky(player, primarySkillType); + + if(isLucky) { + return probability.evaluate(LUCKY_MODIFIER); + } else { + return probability.evaluate(); + } + } + + /** + * Skills activate without RNG, this allows other plugins to prevent that activation + * @param subSkillType target subskill + * @param player target player + * @return true if the skill succeeds (wasn't cancelled by any other plugin) + */ + public static boolean isNonRNGSkillActivationSuccessful(@NotNull SubSkillType subSkillType, @NotNull Player player) { + return !EventUtils.callSubSkillEvent(player, subSkillType).isCancelled(); + } + + /** + * Grab the {@link Probability} for a specific {@link SubSkillType} for a specific {@link Player} + * + * @param subSkillType target subskill + * @param player target player + * @return the Probability of this skill succeeding + */ + public static @NotNull Probability getSubSkillProbability(@NotNull SubSkillType subSkillType, @Nullable Player player) { + return ProbabilityUtil.ofSubSkill(player, subSkillType); + } + + public static @NotNull String[] getRNGDisplayValues(@NotNull Player player, @NotNull SubSkillType subSkill) { + double firstValue = chanceOfSuccessPercentage(player, subSkill, false); + double secondValue = chanceOfSuccessPercentage(player, subSkill, true); + + return new String[]{percent.format(firstValue), percent.format(secondValue)}; + } + + public static @NotNull String[] getRNGDisplayValues(@NotNull Probability probability) { + double firstValue = chanceOfSuccessPercentage(probability, false); + double secondValue = chanceOfSuccessPercentage(probability, true); + + return new String[]{percent.format(firstValue), percent.format(secondValue)}; + } + + /** + * Helper function to calculate what probability a given skill has at a certain level + * @param skillLevel the skill level currently between the floor and the ceiling + * @param floor the minimum odds this skill can have + * @param ceiling the maximum odds this skill can have + * @param maxBonusLevel the maximum level this skill can have to reach the ceiling + * + * @return the probability of success for this skill at this level + */ + public static Probability calculateCurrentSkillProbability(double skillLevel, double floor, + double ceiling, double maxBonusLevel) { + // The odds of success are between the value of the floor and the value of the ceiling. + // If the skill has a maxBonusLevel of 500 on this skill, then at skill level 500 you would have the full odds, + // at skill level 250 it would be half odds. + + if (skillLevel >= maxBonusLevel || maxBonusLevel <= 0) { + // Avoid divide by zero bugs + // Max benefit has been reached, should always succeed + return Probability.ofPercent(ceiling); + } + + double odds = (skillLevel / maxBonusLevel) * 100D; + + // make sure the odds aren't lower or higher than the floor or ceiling + return Probability.ofPercent(Math.min(Math.max(floor, odds), ceiling)); + } +} diff --git a/src/main/java/com/gmail/nossr50/util/random/RandomChanceExecution.java b/src/main/java/com/gmail/nossr50/util/random/RandomChanceExecution.java deleted file mode 100644 index e5d51d740..000000000 --- a/src/main/java/com/gmail/nossr50/util/random/RandomChanceExecution.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.gmail.nossr50.util.random; - -public interface RandomChanceExecution { - /** - * Gets the XPos used in the formula for success - * - * @return value of x for our success probability graph - */ - double getXPos(); - - /** - * The maximum odds for this RandomChanceExecution - * For example, if this value is 10, then 10% odds would be the maximum and would be achieved only when xPos equaled the LinearCurvePeak - * - * @return maximum probability odds from 0.00 (no chance of ever happened) to 100.0 (probability can be guaranteed) - */ - double getProbabilityCap(); -} diff --git a/src/main/java/com/gmail/nossr50/util/random/RandomChanceSkill.java b/src/main/java/com/gmail/nossr50/util/random/RandomChanceSkill.java deleted file mode 100644 index 92d91ec83..000000000 --- a/src/main/java/com/gmail/nossr50/util/random/RandomChanceSkill.java +++ /dev/null @@ -1,176 +0,0 @@ -package com.gmail.nossr50.util.random; - -import com.gmail.nossr50.datatypes.player.McMMOPlayer; -import com.gmail.nossr50.datatypes.skills.SubSkillType; -import com.gmail.nossr50.util.Permissions; -import com.gmail.nossr50.util.player.UserManager; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class RandomChanceSkill implements RandomChanceExecution { - protected final double probabilityCap; - protected final boolean isLucky; - protected int skillLevel; - protected final double resultModifier; - protected final double maximumBonusLevelCap; - - public RandomChanceSkill(@Nullable Player player, @NotNull SubSkillType subSkillType, double resultModifier) { - this.probabilityCap = RandomChanceUtil.LINEAR_CURVE_VAR; - - final McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); - if (player != null && mcMMOPlayer != null) { - this.skillLevel = mcMMOPlayer.getSkillLevel(subSkillType.getParentSkill()); - } else { - this.skillLevel = 0; - } - - if (player != null) - isLucky = Permissions.lucky(player, subSkillType.getParentSkill()); - else - isLucky = false; - - this.resultModifier = resultModifier; - this.maximumBonusLevelCap = RandomChanceUtil.getMaxBonusLevelCap(subSkillType); - } - - public RandomChanceSkill(@Nullable Player player, @NotNull SubSkillType subSkillType) { - this.probabilityCap = RandomChanceUtil.LINEAR_CURVE_VAR; - - final McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); - if (player != null && mcMMOPlayer != null) { - this.skillLevel = mcMMOPlayer.getSkillLevel(subSkillType.getParentSkill()); - } else { - this.skillLevel = 0; - } - - if (player != null) - isLucky = Permissions.lucky(player, subSkillType.getParentSkill()); - else - isLucky = false; - - this.resultModifier = 1.0D; - this.maximumBonusLevelCap = RandomChanceUtil.getMaxBonusLevelCap(subSkillType); - } - - public RandomChanceSkill(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap) { - if (hasCap) - this.probabilityCap = RandomChanceUtil.getMaximumProbability(subSkillType); - else - this.probabilityCap = RandomChanceUtil.LINEAR_CURVE_VAR; - - final McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); - if (player != null && mcMMOPlayer != null) { - this.skillLevel = mcMMOPlayer.getSkillLevel(subSkillType.getParentSkill()); - } else { - this.skillLevel = 0; - } - - if (player != null) - isLucky = Permissions.lucky(player, subSkillType.getParentSkill()); - else - isLucky = false; - - this.resultModifier = 1.0D; - this.maximumBonusLevelCap = RandomChanceUtil.getMaxBonusLevelCap(subSkillType); - } - - public RandomChanceSkill(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap, boolean luckyOverride) { - if (hasCap) - this.probabilityCap = RandomChanceUtil.getMaximumProbability(subSkillType); - else - this.probabilityCap = RandomChanceUtil.LINEAR_CURVE_VAR; - - final McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); - if (player != null && mcMMOPlayer != null) { - this.skillLevel = mcMMOPlayer.getSkillLevel(subSkillType.getParentSkill()); - } else { - this.skillLevel = 0; - } - - isLucky = luckyOverride; - - this.resultModifier = 1.0D; - this.maximumBonusLevelCap = RandomChanceUtil.getMaxBonusLevelCap(subSkillType); - } - - public RandomChanceSkill(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap, double resultModifier) { - if (hasCap) - this.probabilityCap = RandomChanceUtil.getMaximumProbability(subSkillType); - else - this.probabilityCap = RandomChanceUtil.LINEAR_CURVE_VAR; - - final McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); - if (player != null && mcMMOPlayer != null) { - this.skillLevel = mcMMOPlayer.getSkillLevel(subSkillType.getParentSkill()); - } else { - this.skillLevel = 0; - } - - if (player != null) - isLucky = Permissions.lucky(player, subSkillType.getParentSkill()); - else - isLucky = false; - - this.resultModifier = resultModifier; - this.maximumBonusLevelCap = RandomChanceUtil.getMaxBonusLevelCap(subSkillType); - } - - /** - * Gets the skill level of the player who owns this RandomChanceSkill - * - * @return the current skill level relating to this RandomChanceSkill - */ - public int getSkillLevel() { - return skillLevel; - } - - /** - * Modify the skill level used for this skill's RNG calculations - * - * @param newSkillLevel new skill level - */ - public void setSkillLevel(int newSkillLevel) { - skillLevel = newSkillLevel; - } - - /** - * The maximum bonus level for this skill - * This is when the skills level no longer increases the odds of success - * For example, a value of 25 will mean the success chance no longer grows after skill level 25 - * - * @return the maximum bonus from skill level for this skill - */ - public double getMaximumBonusLevelCap() { - return maximumBonusLevelCap; - } - - /** - * Gets the XPos used in the formula for success - * - * @return value of x for our success probability graph - */ - @Override - public double getXPos() { - return getSkillLevel(); - } - - /** - * The maximum odds for this RandomChanceExecution - * For example, if this value is 10, then 10% odds would be the maximum and would be achieved only when xPos equaled the LinearCurvePeak - * - * @return maximum probability odds from 0.00 (no chance of ever happened) to 100.0 (probability can be guaranteed) - */ - @Override - public double getProbabilityCap() { - return probabilityCap; - } - - public boolean isLucky() { - return isLucky; - } - - public double getResultModifier() { - return resultModifier; - } -} diff --git a/src/main/java/com/gmail/nossr50/util/random/RandomChanceSkillStatic.java b/src/main/java/com/gmail/nossr50/util/random/RandomChanceSkillStatic.java deleted file mode 100644 index c96b71d6b..000000000 --- a/src/main/java/com/gmail/nossr50/util/random/RandomChanceSkillStatic.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.gmail.nossr50.util.random; - -import com.gmail.nossr50.datatypes.skills.SubSkillType; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class RandomChanceSkillStatic extends RandomChanceSkill { - private final double xPos; - - public RandomChanceSkillStatic(double xPos, @Nullable Player player, @NotNull SubSkillType subSkillType) { - super(player, subSkillType); - - this.xPos = xPos; - } - - public RandomChanceSkillStatic(double xPos, @Nullable Player player, @NotNull SubSkillType subSkillType, boolean luckyOverride) { - super(player, subSkillType, false, luckyOverride); - - this.xPos = xPos; - } - - public RandomChanceSkillStatic(double xPos, @Nullable Player player, @NotNull SubSkillType subSkillType, double resultModifier) { - super(player, subSkillType, resultModifier); - - this.xPos = xPos; - } - - /** - * Gets the XPos used in the formula for success - * - * @return value of x for our success probability graph - */ - @Override - public double getXPos() { - return xPos; - } - - /** - * The maximum odds for this RandomChanceExecution - * For example, if this value is 10, then 10% odds would be the maximum and would be achieved only when xPos equaled the LinearCurvePeak - * - * @return maximum probability odds from 0.00 (no chance of ever happened) to 100.0 (probability can be guaranteed) - */ - @Override - public double getProbabilityCap() { - return probabilityCap; - } - - /** - * The maximum bonus level for this skill - * This is when the skills level no longer increases the odds of success - * For example, a value of 25 will mean the success chance no longer grows after skill level 25 - * - * @return the maximum bonus from skill level for this skill - */ - @Override - public double getMaximumBonusLevelCap() { - return 100; - } -} diff --git a/src/main/java/com/gmail/nossr50/util/random/RandomChanceStatic.java b/src/main/java/com/gmail/nossr50/util/random/RandomChanceStatic.java deleted file mode 100644 index 0b09a4a3c..000000000 --- a/src/main/java/com/gmail/nossr50/util/random/RandomChanceStatic.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.gmail.nossr50.util.random; - -public class RandomChanceStatic implements RandomChanceExecution { - private final double xPos; - private final double probabilityCap; - private final boolean isLucky; - - public RandomChanceStatic(double xPos, double probabilityCap, boolean isLucky) { - this.xPos = xPos; - this.probabilityCap = probabilityCap; - this.isLucky = isLucky; - } - - /** - * Gets the XPos used in the formula for success - * - * @return value of x for our success probability graph - */ - @Override - public double getXPos() { - return xPos; - } - - /** - * The maximum odds for this RandomChanceExecution - * For example, if this value is 10, then 10% odds would be the maximum and would be achieved only when xPos equaled the LinearCurvePeak - * - * @return maximum probability odds from 0.00 (no chance of ever happened) to 100.0 (probability can be guaranteed) - */ - @Override - public double getProbabilityCap() { - return probabilityCap; - } - - public boolean isLucky() { - return isLucky; - } -} diff --git a/src/main/java/com/gmail/nossr50/util/random/RandomChanceUtil.java b/src/main/java/com/gmail/nossr50/util/random/RandomChanceUtil.java deleted file mode 100644 index 0191172c1..000000000 --- a/src/main/java/com/gmail/nossr50/util/random/RandomChanceUtil.java +++ /dev/null @@ -1,337 +0,0 @@ -package com.gmail.nossr50.util.random; - -import com.gmail.nossr50.datatypes.skills.PrimarySkillType; -import com.gmail.nossr50.datatypes.skills.SubSkillType; -import com.gmail.nossr50.events.skills.secondaryabilities.SubSkillEvent; -import com.gmail.nossr50.events.skills.secondaryabilities.SubSkillRandomCheckEvent; -import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.util.EventUtils; -import com.gmail.nossr50.util.Permissions; -import com.gmail.nossr50.util.skills.SkillActivationType; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.text.DecimalFormat; -import java.util.concurrent.ThreadLocalRandom; - -public class RandomChanceUtil { - public static final @NotNull DecimalFormat percent = new DecimalFormat("##0.00%"); - //public static final DecimalFormat decimal = new DecimalFormat("##0.00"); - public static final double LINEAR_CURVE_VAR = 100.0D; - public static final double LUCKY_MODIFIER = 1.333D; - - /** - * This method is the final step in determining if a Sub-Skill / Secondary Skill in mcMMO successfully activates either from chance or otherwise - * Random skills check for success based on numbers and then fire a cancellable event, if that event is not cancelled they succeed - * non-RNG skills just fire the cancellable event and succeed if they go uncancelled - * - * @param skillActivationType this value represents what kind of activation procedures this sub-skill uses - * @param subSkillType The identifier for this specific sub-skill - * @param player The owner of this sub-skill - * @return returns true if all conditions are met and the event is not cancelled - */ - public static boolean isActivationSuccessful(@NotNull SkillActivationType skillActivationType, @NotNull SubSkillType subSkillType, @Nullable Player player) { - switch (skillActivationType) { - case RANDOM_LINEAR_100_SCALE_WITH_CAP: - return checkRandomChanceExecutionSuccess(player, subSkillType, true); - case RANDOM_STATIC_CHANCE: - return checkRandomStaticChanceExecutionSuccess(player, subSkillType); - case ALWAYS_FIRES: - SubSkillEvent event = EventUtils.callSubSkillEvent(player, subSkillType); - return !event.isCancelled(); - default: - return false; - } - } - - public static double getActivationChance(@NotNull SkillActivationType skillActivationType, @NotNull SubSkillType subSkillType, @Nullable Player player, boolean luckyOverride) { - switch (skillActivationType) { - case RANDOM_LINEAR_100_SCALE_WITH_CAP: - return getRandomChanceExecutionSuccess(player, subSkillType, true, luckyOverride); - case RANDOM_STATIC_CHANCE: - return getRandomStaticChanceExecutionSuccess(player, subSkillType, luckyOverride); - default: - return 0.1337; - } - } - - /** - * Checks whether or not the random chance succeeds - * - * @return true if the random chance succeeds - */ - public static boolean checkRandomChanceExecutionSuccess(@NotNull Player player, @NotNull PrimarySkillType primarySkillType, double chance) { - //Check the odds - chance *= 100; - - chance = addLuck(player, primarySkillType, chance); - - /* - * Stuff like treasures can specify a drop chance from 0.05 to 100 - * Because of that we need to use a large int bound and multiply the chance by 100 - */ - return rollDice(chance, 10000); - } - - public static boolean rollDice(double chanceOfSuccess, int bound) { - return rollDice(chanceOfSuccess, bound, 1.0F); - } - - public static boolean rollDice(double chanceOfSuccess, int bound, double resultModifier) { - return chanceOfSuccess > (ThreadLocalRandom.current().nextInt(bound) * resultModifier); - } - - /** - * Used for stuff like Excavation, Fishing, etc... - * - * @param randomChance - * @return - */ - public static boolean checkRandomChanceExecutionSuccess(@NotNull RandomChanceSkillStatic randomChance, double resultModifier) { - double chanceOfSuccess = calculateChanceOfSuccess(randomChance); - - //Check the odds - return rollDice(chanceOfSuccess, 100, resultModifier); - } - - /** - * Used for stuff like Excavation, Fishing, etc... - * - * @param randomChance - * @return - */ - public static boolean checkRandomChanceExecutionSuccess(@NotNull RandomChanceSkillStatic randomChance) { - return checkRandomChanceExecutionSuccess(randomChance, 1.0F); - } - - public static boolean checkRandomChanceExecutionSuccess(@NotNull RandomChanceSkill randomChance) { - double chanceOfSuccess = calculateChanceOfSuccess(randomChance); - - //Check the odds - return rollDice(chanceOfSuccess, 100); - } - - - /*public static double getRandomChanceExecutionChance(RandomChanceSkill randomChance) - { - double chanceOfSuccess = calculateChanceOfSuccess(randomChance); - return chanceOfSuccess; - }*/ - - /** - * Gets the Static Chance for something to activate - * - * @param randomChance - * @return - */ - public static double getRandomChanceExecutionChance(@NotNull RandomChanceExecution randomChance) { - return getChanceOfSuccess(randomChance.getXPos(), randomChance.getProbabilityCap(), LINEAR_CURVE_VAR); - } - - public static double getRandomChanceExecutionChance(@NotNull RandomChanceExecution randomChance, boolean luckyOverride) { - return getChanceOfSuccess(randomChance.getXPos(), randomChance.getProbabilityCap(), LINEAR_CURVE_VAR); - } - - public static double getRandomChanceExecutionChance(@NotNull RandomChanceStatic randomChance) { - double chanceOfSuccess = getChanceOfSuccess(randomChance.getXPos(), randomChance.getProbabilityCap(), LINEAR_CURVE_VAR); - - chanceOfSuccess = addLuck(randomChance.isLucky(), chanceOfSuccess); - - return chanceOfSuccess; - } - - /*private static double calculateChanceOfSuccess(RandomChanceStatic randomChance) { - double chanceOfSuccess = getChanceOfSuccess(randomChance.getXPos(), randomChance.getProbabilityCap()); - return chanceOfSuccess; - }*/ - - public static double calculateChanceOfSuccess(@NotNull RandomChanceSkill randomChance) { - double skillLevel = randomChance.getSkillLevel(); - double maximumProbability = randomChance.getProbabilityCap(); - double maximumBonusLevel = randomChance.getMaximumBonusLevelCap(); - - double chanceOfSuccess; - - if (skillLevel >= maximumBonusLevel) { - //Chance of success is equal to the maximum probability if the maximum bonus level has been reached - chanceOfSuccess = maximumProbability; - } else { - //Get chance of success - chanceOfSuccess = getChanceOfSuccess(randomChance.getXPos(), maximumProbability, maximumBonusLevel); - } - - //Add Luck - chanceOfSuccess = addLuck(randomChance.isLucky(), chanceOfSuccess); - - return chanceOfSuccess; - } - - public static double calculateChanceOfSuccess(@NotNull RandomChanceSkillStatic randomChance) { - double chanceOfSuccess = getChanceOfSuccess(randomChance.getXPos(), 100, 100); - - //Add Luck - chanceOfSuccess = addLuck(randomChance.isLucky(), chanceOfSuccess); - - return chanceOfSuccess; - } - - /** - * The formula for RNG success is determined like this - * maximum probability * ( x / maxlevel ) - * - * @return the chance of success from 0-100 (100 = guaranteed) - */ - private static int getChanceOfSuccess(double skillLevel, double maxProbability, double maxLevel) { - //return (int) (x / (y / LINEAR_CURVE_VAR)); - return (int) (maxProbability * (skillLevel / maxLevel)); - // max probability * (weight/maxlevel) = chance of success - } - - private static int getChanceOfSuccess(double x, double y) { - return (int) (x / (y / LINEAR_CURVE_VAR)); - // max probability * (weight/maxlevel) = chance of success - } - - public static double getRandomChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap) { - RandomChanceSkill rcs = new RandomChanceSkill(player, subSkillType, hasCap); - return calculateChanceOfSuccess(rcs); - } - - public static double getRandomChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap, boolean luckyOverride) { - RandomChanceSkill rcs = new RandomChanceSkill(player, subSkillType, hasCap, luckyOverride); - return calculateChanceOfSuccess(rcs); - } - - public static double getRandomStaticChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean luckyOverride) { - try { - return getRandomChanceExecutionChance(new RandomChanceSkillStatic(getStaticRandomChance(subSkillType), player, subSkillType, luckyOverride)); - } catch (InvalidStaticChance invalidStaticChance) { - //Catch invalid static skills - invalidStaticChance.printStackTrace(); - } - - return 0.1337; //Puts on shades - } - - public static boolean checkRandomChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap) { - return checkRandomChanceExecutionSuccess(new RandomChanceSkill(player, subSkillType, hasCap)); - } - - public static boolean checkRandomChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType) { - return checkRandomChanceExecutionSuccess(new RandomChanceSkill(player, subSkillType)); - } - - public static boolean checkRandomChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType, boolean hasCap, double resultModifier) { - return checkRandomChanceExecutionSuccess(new RandomChanceSkill(player, subSkillType, hasCap, resultModifier)); - } - - public static boolean checkRandomChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType, double resultModifier) { - return checkRandomChanceExecutionSuccess(new RandomChanceSkill(player, subSkillType, resultModifier)); - } - - - public static boolean checkRandomStaticChanceExecutionSuccess(@Nullable Player player, @NotNull SubSkillType subSkillType) { - try { - return checkRandomChanceExecutionSuccess(new RandomChanceSkillStatic(getStaticRandomChance(subSkillType), player, subSkillType)); - } catch (InvalidStaticChance invalidStaticChance) { - //Catch invalid static skills - invalidStaticChance.printStackTrace(); - } - - return false; - } - - /** - * Grabs static activation rolls for Secondary Abilities - * - * @param subSkillType The secondary ability to grab properties of - * @return The static activation roll involved in the RNG calculation - * @throws InvalidStaticChance if the skill has no defined static chance this exception will be thrown and you should know you're a naughty boy - */ - public static double getStaticRandomChance(@NotNull SubSkillType subSkillType) throws InvalidStaticChance { - switch (subSkillType) { - case AXES_ARMOR_IMPACT: - return mcMMO.p.getAdvancedConfig().getImpactChance(); - case AXES_GREATER_IMPACT: - return mcMMO.p.getAdvancedConfig().getGreaterImpactChance(); - case TAMING_FAST_FOOD_SERVICE: - return mcMMO.p.getAdvancedConfig().getFastFoodChance(); - default: - throw new InvalidStaticChance(); - } - } - - public static boolean sendSkillEvent(Player player, SubSkillType subSkillType, double activationChance) { - SubSkillRandomCheckEvent event = new SubSkillRandomCheckEvent(player, subSkillType, activationChance); - return !event.isCancelled(); - } - - public static String @NotNull [] calculateAbilityDisplayValues(@NotNull SkillActivationType skillActivationType, @NotNull Player player, @NotNull SubSkillType subSkillType) { - double successChance = getActivationChance(skillActivationType, subSkillType, player, false); - double successChanceLucky = getActivationChance(skillActivationType, subSkillType, player, true); - - String[] displayValues = new String[2]; - - boolean isLucky = Permissions.lucky(player, subSkillType.getParentSkill()); - - displayValues[0] = percent.format(Math.min(successChance, 100.0D) / 100.0D); - displayValues[1] = isLucky ? percent.format(Math.min(successChanceLucky, 100.0D) / 100.0D) : null; - - return displayValues; - } - - public static String @NotNull [] calculateAbilityDisplayValuesStatic(@NotNull Player player, @NotNull PrimarySkillType primarySkillType, double chance) { - RandomChanceStatic rcs = new RandomChanceStatic(chance, LINEAR_CURVE_VAR, false); - double successChance = getRandomChanceExecutionChance(rcs); - - RandomChanceStatic rcs_lucky = new RandomChanceStatic(chance, LINEAR_CURVE_VAR, true); - double successChance_lucky = getRandomChanceExecutionChance(rcs_lucky); - - String[] displayValues = new String[2]; - - boolean isLucky = Permissions.lucky(player, primarySkillType); - - displayValues[0] = percent.format(Math.min(successChance, 100.0D) / 100.0D); - displayValues[1] = isLucky ? percent.format(Math.min(successChance_lucky, 100.0D) / 100.0D) : null; - - return displayValues; - } - - public static String @NotNull [] calculateAbilityDisplayValuesCustom(@NotNull SkillActivationType skillActivationType, @NotNull Player player, @NotNull SubSkillType subSkillType, double multiplier) { - double successChance = getActivationChance(skillActivationType, subSkillType, player, false); - double successChanceLucky = getActivationChance(skillActivationType, subSkillType, player, true); - //TODO: Most likely incorrectly displays the value for graceful roll but gonna ignore for now... - successChance *= multiplier; //Currently only used for graceful roll - String[] displayValues = new String[2]; - - boolean isLucky = Permissions.lucky(player, subSkillType.getParentSkill()); - - displayValues[0] = percent.format(Math.min(successChance, 100.0D) / 100.0D); - displayValues[1] = isLucky ? percent.format(Math.min(successChanceLucky, 100.0D) / 100.0D) : null; - - return displayValues; - } - - public static double addLuck(@NotNull Player player, @NotNull PrimarySkillType primarySkillType, double chance) { - if (Permissions.lucky(player, primarySkillType)) - return chance * LUCKY_MODIFIER; - else - return chance; - } - - public static double addLuck(boolean isLucky, double chance) { - if (isLucky) - return chance * LUCKY_MODIFIER; - else - return chance; - } - - public static double getMaximumProbability(@NotNull SubSkillType subSkillType) { - return mcMMO.p.getAdvancedConfig().getMaximumProbability(subSkillType); - } - - public static double getMaxBonusLevelCap(@NotNull SubSkillType subSkillType) { - return mcMMO.p.getAdvancedConfig().getMaxBonusLevel(subSkillType); - } -} diff --git a/src/main/java/com/gmail/nossr50/util/random/SkillProbabilityType.java b/src/main/java/com/gmail/nossr50/util/random/SkillProbabilityType.java new file mode 100644 index 000000000..95814f6a1 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/util/random/SkillProbabilityType.java @@ -0,0 +1,6 @@ +package com.gmail.nossr50.util.random; + +public enum SkillProbabilityType { + DYNAMIC_CONFIGURABLE, //Has multiple values used for calculation (taken from config files) + STATIC_CONFIGURABLE, //A single value used for calculations (taken from config files) +} diff --git a/src/main/java/com/gmail/nossr50/util/scoreboards/ScoreboardWrapper.java b/src/main/java/com/gmail/nossr50/util/scoreboards/ScoreboardWrapper.java index 1237ce086..4d2d51560 100644 --- a/src/main/java/com/gmail/nossr50/util/scoreboards/ScoreboardWrapper.java +++ b/src/main/java/com/gmail/nossr50/util/scoreboards/ScoreboardWrapper.java @@ -11,7 +11,6 @@ import com.gmail.nossr50.events.scoreboard.ScoreboardEventReason; import com.gmail.nossr50.events.scoreboard.ScoreboardObjectiveEventReason; import com.gmail.nossr50.locale.LocaleLoader; import com.gmail.nossr50.mcMMO; -import com.gmail.nossr50.skills.child.FamilyTree; import com.gmail.nossr50.util.LogUtils; import com.gmail.nossr50.util.Misc; import com.gmail.nossr50.util.player.NotificationManager; @@ -495,7 +494,7 @@ public class ScoreboardWrapper { sidebarObjective.getScore(ScoreboardManager.LABEL_REMAINING_XP).setScore(mcMMOPlayer.getXpToLevel(targetSkill) - currentXP); } else { - for (PrimarySkillType parentSkill : FamilyTree.getParents(targetSkill)) { + for (PrimarySkillType parentSkill : mcMMO.p.getSkillTools().getChildSkillParents(targetSkill)) { sidebarObjective.getScore(ScoreboardManager.skillLabels.get(parentSkill)).setScore(mcMMOPlayer.getSkillLevel(parentSkill)); } } diff --git a/src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java b/src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java index 1d5eea33f..fd2d240c2 100644 --- a/src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java +++ b/src/main/java/com/gmail/nossr50/util/skills/CombatUtils.java @@ -10,13 +10,13 @@ import com.gmail.nossr50.datatypes.skills.SubSkillType; import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.metadata.MobMetaFlagType; import com.gmail.nossr50.metadata.MobMetadataService; -import com.gmail.nossr50.party.PartyManager; import com.gmail.nossr50.runnables.skills.AwardCombatXpTask; import com.gmail.nossr50.skills.acrobatics.AcrobaticsManager; import com.gmail.nossr50.skills.archery.ArcheryManager; import com.gmail.nossr50.skills.axes.AxesManager; import com.gmail.nossr50.skills.swords.SwordsManager; import com.gmail.nossr50.skills.taming.TamingManager; +import com.gmail.nossr50.skills.tridents.TridentsManager; import com.gmail.nossr50.skills.unarmed.UnarmedManager; import com.gmail.nossr50.util.*; import com.gmail.nossr50.util.player.NotificationManager; @@ -45,7 +45,6 @@ public final class CombatUtils { return mcMMO.getMetadataService().getMobMetadataService(); } - //Likely.. because who knows what plugins are throwing around public static boolean isDamageLikelyFromNormalCombat(@NotNull DamageCause damageCause) { return switch (damageCause) { case ENTITY_ATTACK, ENTITY_SWEEP_ATTACK, PROJECTILE -> true; @@ -112,6 +111,108 @@ public final class CombatUtils { } } } + private static void processTridentCombatMelee(@NotNull LivingEntity target, @NotNull Player player, @NotNull EntityDamageByEntityEvent event) { + if (event.getCause() == DamageCause.THORNS) { + return; + } + + double boostedDamage = event.getDamage(); + + McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); + + //Make sure the profiles been loaded + if(mcMMOPlayer == null) { + return; + } + + TridentsManager tridentsManager = mcMMOPlayer.getTridentsManager(); + +// if (tridentsManager.canActivateAbility()) { +// mcMMOPlayer.checkAbilityActivation(PrimarySkillType.TRIDENTS); +// } + + if (SkillUtils.canUseSubskill(player, SubSkillType.TRIDENTS_IMPALE)) { + boostedDamage += (tridentsManager.impaleDamageBonus() * mcMMOPlayer.getAttackStrength()); + } + + if(canUseLimitBreak(player, target, SubSkillType.TRIDENTS_TRIDENTS_LIMIT_BREAK)) { + boostedDamage += (getLimitBreakDamage(player, target, SubSkillType.TRIDENTS_TRIDENTS_LIMIT_BREAK) * mcMMOPlayer.getAttackStrength()); + } + + event.setDamage(boostedDamage); + processCombatXP(mcMMOPlayer, target, PrimarySkillType.TRIDENTS); + + printFinalDamageDebug(player, event, mcMMOPlayer); + } + + private static void processTridentCombatRanged(@NotNull Trident trident, @NotNull LivingEntity target, @NotNull Player player, @NotNull EntityDamageByEntityEvent event) { + if (event.getCause() == DamageCause.THORNS) { + return; + } + + double boostedDamage = event.getDamage(); + + McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); + + //Make sure the profiles been loaded + if(mcMMOPlayer == null) { + return; + } + + TridentsManager tridentsManager = mcMMOPlayer.getTridentsManager(); + + if (SkillUtils.canUseSubskill(player, SubSkillType.TRIDENTS_IMPALE)) { + boostedDamage += (tridentsManager.impaleDamageBonus() * mcMMOPlayer.getAttackStrength()); + } + + if(canUseLimitBreak(player, target, SubSkillType.TRIDENTS_TRIDENTS_LIMIT_BREAK)) { + boostedDamage += (getLimitBreakDamage(player, target, SubSkillType.TRIDENTS_TRIDENTS_LIMIT_BREAK) * mcMMOPlayer.getAttackStrength()); + } + + event.setDamage(boostedDamage); + processCombatXP(mcMMOPlayer, target, PrimarySkillType.TRIDENTS); + + printFinalDamageDebug(player, event, mcMMOPlayer); + } + + private static void processCrossbowsCombat(@NotNull LivingEntity target, @NotNull Player player, + @NotNull EntityDamageByEntityEvent event, @NotNull Arrow arrow) { + double initialDamage = event.getDamage(); + + McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); + + //Make sure the profiles been loaded + if(mcMMOPlayer == null) { + delayArrowMetaCleanup(arrow); + return; + } + + double boostedDamage = event.getDamage(); + + if (SkillUtils.canUseSubskill(player, SubSkillType.CROSSBOWS_POWERED_SHOT)) { + //Not Additive + boostedDamage = mcMMOPlayer.getCrossbowsManager().poweredShot(initialDamage); + } + + if(canUseLimitBreak(player, target, SubSkillType.CROSSBOWS_CROSSBOWS_LIMIT_BREAK)) { + boostedDamage+=getLimitBreakDamage(player, target, SubSkillType.CROSSBOWS_CROSSBOWS_LIMIT_BREAK); + } + + double distanceMultiplier = ArcheryManager.distanceXpBonusMultiplier(target, arrow); + double forceMultiplier = 1.0; + + event.setDamage(boostedDamage); + processCombatXP(mcMMOPlayer, target, PrimarySkillType.CROSSBOWS, forceMultiplier * distanceMultiplier); + + printFinalDamageDebug(player, event, mcMMOPlayer, + "Distance Multiplier: "+distanceMultiplier, + "Force Multiplier: "+forceMultiplier, + "Initial Damage: "+initialDamage, + "Final Damage: "+boostedDamage); + + //Clean data + delayArrowMetaCleanup(arrow); + } private static void processAxeCombat(@NotNull LivingEntity target, @NotNull Player player, @NotNull EntityDamageByEntityEvent event) { if (event.getCause() == DamageCause.THORNS) { @@ -240,14 +341,15 @@ public final class CombatUtils { } - private static void processArcheryCombat(@NotNull LivingEntity target, @NotNull Player player, @NotNull EntityDamageByEntityEvent event, @NotNull Projectile arrow) { + private static void processArcheryCombat(@NotNull LivingEntity target, @NotNull Player player, + @NotNull EntityDamageByEntityEvent event, @NotNull Arrow arrow) { double initialDamage = event.getDamage(); McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); //Make sure the profiles been loaded if(mcMMOPlayer == null) { - cleanupArrowMetadata(arrow); + delayArrowMetaCleanup(arrow); return; } @@ -273,7 +375,7 @@ public final class CombatUtils { boostedDamage+=getLimitBreakDamage(player, target, SubSkillType.ARCHERY_ARCHERY_LIMIT_BREAK); } - double distanceMultiplier = archeryManager.distanceXpBonusMultiplier(target, arrow); + double distanceMultiplier = ArcheryManager.distanceXpBonusMultiplier(target, arrow); double forceMultiplier = 1.0; //Hacky Fix - some plugins spawn arrows and assign them to players after the ProjectileLaunchEvent fires if(arrow.hasMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE)) @@ -288,7 +390,7 @@ public final class CombatUtils { "Initial Damage: "+initialDamage, "Final Damage: "+boostedDamage); //Clean data - cleanupArrowMetadata(arrow); + delayArrowMetaCleanup(arrow); } /** @@ -300,6 +402,10 @@ public final class CombatUtils { Entity painSource = event.getDamager(); EntityType entityType = painSource.getType(); + if (target instanceof ArmorStand) { + return; + } + if (target instanceof Player player) { if(ExperienceConfig.getInstance().isNPCInteractionPrevented()) { if (Misc.isNPCEntityExcludingVillagers(target)) { @@ -383,6 +489,15 @@ public final class CombatUtils { processUnarmedCombat(target, player, event); } } + else if (ItemUtils.isTrident(heldItem)) { + if (!mcMMO.p.getSkillTools().canCombatSkillsTrigger(PrimarySkillType.TRIDENTS, target)) { + return; + } + + if (mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.TRIDENTS)) { + processTridentCombatMelee(target, player, event); + } + } } else if (entityType == EntityType.WOLF) { @@ -396,20 +511,36 @@ public final class CombatUtils { } } } - else if (entityType == EntityType.ARROW || entityType == EntityType.SPECTRAL_ARROW) { - Projectile arrow = (Projectile) painSource; + else if (painSource instanceof Trident trident) { + ProjectileSource projectileSource = trident.getShooter(); + + if (projectileSource instanceof Player player) { + if (!Misc.isNPCEntityExcludingVillagers(player)) { + if(mcMMO.p.getSkillTools().canCombatSkillsTrigger(PrimarySkillType.TRIDENTS, target)) { + processTridentCombatRanged(trident, target, player, event); + } + } + } + } + else if (painSource instanceof Arrow arrow) { ProjectileSource projectileSource = arrow.getShooter(); + boolean isCrossbow = arrow.isShotFromCrossbow(); + if (projectileSource instanceof Player player) { - if (projectileSource instanceof Player player && mcMMO.p.getSkillTools().canCombatSkillsTrigger(PrimarySkillType.ARCHERY, target)) { - - if (!Misc.isNPCEntityExcludingVillagers(player) && mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.ARCHERY)) { - processArcheryCombat(target, player, event, arrow); + if (!Misc.isNPCEntityExcludingVillagers(player)) { + if(!isCrossbow && mcMMO.p.getSkillTools().canCombatSkillsTrigger(PrimarySkillType.ARCHERY, target)) { + processArcheryCombat(target, player, event, arrow); + } else if(isCrossbow && mcMMO.p.getSkillTools().canCombatSkillsTrigger(PrimarySkillType.CROSSBOWS, target)) { + processCrossbowsCombat(target, player, event, arrow); + } } else { //Cleanup Arrow - cleanupArrowMetadata(arrow); + delayArrowMetaCleanup(arrow); } - if (target.getType() != EntityType.CREEPER && !Misc.isNPCEntityExcludingVillagers(player) && mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.TAMING)) { + if (target.getType() != EntityType.CREEPER + && !Misc.isNPCEntityExcludingVillagers(player) + && mcMMO.p.getSkillTools().doesPlayerHaveSkillPermission(player, PrimarySkillType.TAMING)) { McMMOPlayer mcMMOPlayer = UserManager.getPlayer(player); if(mcMMOPlayer == null) @@ -420,7 +551,6 @@ public final class CombatUtils { } } } - } /** @@ -621,7 +751,7 @@ public final class CombatUtils { } public static boolean hasIgnoreDamageMetadata(@NotNull LivingEntity target) { - return target.getMetadata(MetadataConstants.METADATA_KEY_CUSTOM_DAMAGE).size() != 0; + return target.hasMetadata(MetadataConstants.METADATA_KEY_CUSTOM_DAMAGE); } public static void dealNoInvulnerabilityTickDamageRupture(@NotNull LivingEntity target, double damage, Entity attacker, int toolTier) { @@ -630,35 +760,6 @@ public final class CombatUtils { } dealNoInvulnerabilityTickDamage(target, damage, attacker); - -// //IFrame storage -//// int noDamageTicks = target.getNoDamageTicks(); -// -//// String debug = "BLEED DMG RESULT: INC DMG:"+damage+", HP-Before:"+target.getHealth()+", HP-After:"; -// -//// double incDmg = getFakeDamageFinalResult(attacker, target, DamageCause.ENTITY_ATTACK, damage); -// -//// double newHealth = Math.max(0, target.getHealth() - incDmg); -// -// //Don't kill things with a stone or wooden weapon -//// if(toolTier < 3 && newHealth == 0) -//// return; -// -// target.setMetadata(mcMMO.CUSTOM_DAMAGE_METAKEY, mcMMO.metadataValue); -// -// if(newHealth == 0 && !(target instanceof Player)) -// { -// target.damage(99999, attacker); -// } -// else -// { -//// Vector beforeRuptureVec = new Vector(target.getVelocity().getX(), target.getVelocity().getY(), target.getVelocity().getZ()); ; -// target.damage(damage, attacker); -//// debug+=target.getHealth(); -// Bukkit.broadcastMessage(debug); -//// target.setNoDamageTicks(noDamageTicks); //Do not add additional IFrames -//// target.setVelocity(beforeRuptureVec); -// } } /** @@ -741,14 +842,16 @@ public final class CombatUtils { XPGainReason xpGainReason; if (target instanceof Player defender) { - if (!ExperienceConfig.getInstance().getExperienceGainsPlayerVersusPlayerEnabled() || + if (!ExperienceConfig.getInstance().getExperienceGainsPlayerVersusPlayerEnabled() + || (mcMMO.p.getPartyConfig().isPartyEnabled() && mcMMO.p.getPartyManager().inSameParty(mcMMOPlayer.getPlayer(), (Player) target))) { return; } xpGainReason = XPGainReason.PVP; - if (defender.isOnline() && SkillUtils.cooldownExpired(mcMMOPlayer.getRespawnATS(), Misc.PLAYER_RESPAWN_COOLDOWN_SECONDS)) { + if (defender.isOnline() + && SkillUtils.cooldownExpired(mcMMOPlayer.getRespawnATS(), Misc.PLAYER_RESPAWN_COOLDOWN_SECONDS)) { baseXP = 20 * ExperienceConfig.getInstance().getPlayerVersusPlayerXP(); } } @@ -807,7 +910,7 @@ public final class CombatUtils { baseXP *= multiplier; - if (baseXP != 0) { + if (baseXP > 0) { mcMMO.p.getFoliaLib().getImpl().runAtEntity(mcMMOPlayer.getPlayer(), new AwardCombatXpTask(mcMMOPlayer, primarySkillType, baseXP, target, xpGainReason)); } } @@ -945,31 +1048,12 @@ public final class CombatUtils { } } - /** - * Clean up metadata from a projectile - * - * @param entity projectile - */ - public static void cleanupArrowMetadata(@NotNull Projectile entity) { - if(entity.hasMetadata(MetadataConstants.METADATA_KEY_INF_ARROW)) { - entity.removeMetadata(MetadataConstants.METADATA_KEY_INF_ARROW, mcMMO.p); - } - - if(entity.hasMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE)) { - entity.removeMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE, mcMMO.p); - } - - if(entity.hasMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE)) { - entity.removeMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE, mcMMO.p); - } - } - /** * Clean up metadata from a projectile after a minute has passed * - * @param entity the projectile + * @param arrow the projectile */ - public static void delayArrowMetaCleanup(@NotNull Projectile entity) { - mcMMO.p.getFoliaLib().getImpl().runLater(() -> cleanupArrowMetadata(entity), 20*60); + public static void delayArrowMetaCleanup(@NotNull Arrow arrow) { + mcMMO.p.getFoliaLib().getImpl().runLater(() -> ProjectileUtils.cleanupProjectileMetadata(arrow), 20*120); } } diff --git a/src/main/java/com/gmail/nossr50/util/skills/ProjectileUtils.java b/src/main/java/com/gmail/nossr50/util/skills/ProjectileUtils.java new file mode 100644 index 000000000..f64bac7a3 --- /dev/null +++ b/src/main/java/com/gmail/nossr50/util/skills/ProjectileUtils.java @@ -0,0 +1,84 @@ +package com.gmail.nossr50.util.skills; + +import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.util.MetadataConstants; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Arrow; +import org.bukkit.metadata.FixedMetadataValue; +import org.bukkit.plugin.Plugin; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; + +public class ProjectileUtils { + public static Vector getNormal(BlockFace blockFace) { + return switch (blockFace) { + case UP -> new Vector(0, 1, 0); + case DOWN -> new Vector(0, -1, 0); + case NORTH -> new Vector(0, 0, -1); + case SOUTH -> new Vector(0, 0, 1); + case EAST -> new Vector(1, 0, 0); + case WEST -> new Vector(-1, 0, 0); + default -> new Vector(0, 0, 0); + }; + } + + /** + * Clean up all possible mcMMO related metadata for a projectile + * + * @param arrow projectile + */ + // TODO: Add test + public static void cleanupProjectileMetadata(@NotNull Arrow arrow) { + if(arrow.hasMetadata(MetadataConstants.METADATA_KEY_INF_ARROW)) { + arrow.removeMetadata(MetadataConstants.METADATA_KEY_INF_ARROW, mcMMO.p); + } + + if(arrow.hasMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE)) { + arrow.removeMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE, mcMMO.p); + } + + if(arrow.hasMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE)) { + arrow.removeMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE, mcMMO.p); + } + + if(arrow.hasMetadata(MetadataConstants.METADATA_KEY_SPAWNED_ARROW)) { + arrow.removeMetadata(MetadataConstants.METADATA_KEY_SPAWNED_ARROW, mcMMO.p); + } + + if(arrow.hasMetadata(MetadataConstants.METADATA_KEY_MULTI_SHOT_ARROW)) { + arrow.removeMetadata(MetadataConstants.METADATA_KEY_MULTI_SHOT_ARROW, mcMMO.p); + } + + if(arrow.hasMetadata(MetadataConstants.METADATA_KEY_BOUNCE_COUNT)) { + arrow.removeMetadata(MetadataConstants.METADATA_KEY_BOUNCE_COUNT, mcMMO.p); + } + } + + public static void copyArrowMetadata(@NotNull Plugin pluginRef, @NotNull Arrow arrowToCopy, @NotNull Arrow newArrow) { + if(arrowToCopy.hasMetadata(MetadataConstants.METADATA_KEY_INF_ARROW)) { + newArrow.setMetadata(MetadataConstants.METADATA_KEY_INF_ARROW, + arrowToCopy.getMetadata(MetadataConstants.METADATA_KEY_INF_ARROW).get(0)); + } + + if(arrowToCopy.hasMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE)) { + newArrow.setMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE, + new FixedMetadataValue(pluginRef, + arrowToCopy.getMetadata(MetadataConstants.METADATA_KEY_BOW_FORCE).get(0).asDouble())); + } + + if(arrowToCopy.hasMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE)) { + newArrow.setMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE, + arrowToCopy.getMetadata(MetadataConstants.METADATA_KEY_ARROW_DISTANCE).get(0)); + } + + if(arrowToCopy.hasMetadata(MetadataConstants.METADATA_KEY_SPAWNED_ARROW)) { + newArrow.setMetadata(MetadataConstants.METADATA_KEY_SPAWNED_ARROW, + arrowToCopy.getMetadata(MetadataConstants.METADATA_KEY_SPAWNED_ARROW).get(0)); + } + + if(arrowToCopy.hasMetadata(MetadataConstants.METADATA_KEY_MULTI_SHOT_ARROW)) { + newArrow.setMetadata(MetadataConstants.METADATA_KEY_MULTI_SHOT_ARROW, + arrowToCopy.getMetadata(MetadataConstants.METADATA_KEY_MULTI_SHOT_ARROW).get(0)); + } + } +} diff --git a/src/main/java/com/gmail/nossr50/util/skills/RankUtils.java b/src/main/java/com/gmail/nossr50/util/skills/RankUtils.java index ec4d56481..e06497128 100644 --- a/src/main/java/com/gmail/nossr50/util/skills/RankUtils.java +++ b/src/main/java/com/gmail/nossr50/util/skills/RankUtils.java @@ -24,7 +24,7 @@ public class RankUtils { * * @param plugin plugin instance ref * @param mcMMOPlayer target player - * @param primarySkillType + * @param primarySkillType the skill to check * @param newLevel the new level of this skill */ public static void executeSkillUnlockNotifications(Plugin plugin, McMMOPlayer mcMMOPlayer, PrimarySkillType primarySkillType, int newLevel) @@ -55,6 +55,9 @@ public class RankUtils { } } + /** + * Reset the interval between skill unlock notifications + */ public static void resetUnlockDelayTimer() { count = 0; diff --git a/src/main/java/com/gmail/nossr50/util/skills/SkillTools.java b/src/main/java/com/gmail/nossr50/util/skills/SkillTools.java index 416926fe4..39ddfbd24 100644 --- a/src/main/java/com/gmail/nossr50/util/skills/SkillTools.java +++ b/src/main/java/com/gmail/nossr50/util/skills/SkillTools.java @@ -30,6 +30,8 @@ public class SkillTools { public final @NotNull ImmutableSet EXACT_SUBSKILL_NAMES; public final @NotNull ImmutableList CHILD_SKILLS; public final static @NotNull ImmutableList NON_CHILD_SKILLS; + public final static @NotNull ImmutableList SALVAGE_PARENTS; + public final static @NotNull ImmutableList SMELTING_PARENTS; public final @NotNull ImmutableList COMBAT_SKILLS; public final @NotNull ImmutableList GATHERING_SKILLS; public final @NotNull ImmutableList MISC_SKILLS; @@ -50,9 +52,11 @@ public class SkillTools { } NON_CHILD_SKILLS = ImmutableList.copyOf(tempNonChildSkills); + SALVAGE_PARENTS = ImmutableList.of(PrimarySkillType.REPAIR, PrimarySkillType.FISHING); + SMELTING_PARENTS = ImmutableList.of(PrimarySkillType.MINING, PrimarySkillType.REPAIR); } - public SkillTools(@NotNull mcMMO pluginRef) { + public SkillTools(@NotNull mcMMO pluginRef) throws InvalidSkillException { this.pluginRef = pluginRef; /* @@ -140,26 +144,38 @@ public class SkillTools { */ List childSkills = new ArrayList<>(); -// List nonChildSkills = new ArrayList<>(); for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { if (isChildSkill(primarySkillType)) childSkills.add(primarySkillType); -// } { -// nonChildSkills.add(primarySkillType); -// } } CHILD_SKILLS = ImmutableList.copyOf(childSkills); -// NON_CHILD_SKILLS = ImmutableList.copyOf(nonChildSkills); /* * Build categorized skill lists */ - COMBAT_SKILLS = ImmutableList.of(PrimarySkillType.ARCHERY, PrimarySkillType.AXES, PrimarySkillType.SWORDS, PrimarySkillType.TAMING, PrimarySkillType.UNARMED); - GATHERING_SKILLS = ImmutableList.of(PrimarySkillType.EXCAVATION, PrimarySkillType.FISHING, PrimarySkillType.HERBALISM, PrimarySkillType.MINING, PrimarySkillType.WOODCUTTING); - MISC_SKILLS = ImmutableList.of(PrimarySkillType.ACROBATICS, PrimarySkillType.ALCHEMY, PrimarySkillType.REPAIR, PrimarySkillType.SALVAGE, PrimarySkillType.SMELTING); + COMBAT_SKILLS = ImmutableList.of( + PrimarySkillType.ARCHERY, + PrimarySkillType.AXES, + PrimarySkillType.CROSSBOWS, + PrimarySkillType.SWORDS, + PrimarySkillType.TAMING, + PrimarySkillType.TRIDENTS, + PrimarySkillType.UNARMED); + GATHERING_SKILLS = ImmutableList.of( + PrimarySkillType.EXCAVATION, + PrimarySkillType.FISHING, + PrimarySkillType.HERBALISM, + PrimarySkillType.MINING, + PrimarySkillType.WOODCUTTING); + MISC_SKILLS = ImmutableList.of( + PrimarySkillType.ACROBATICS, + PrimarySkillType.ALCHEMY, + PrimarySkillType.REPAIR, + PrimarySkillType.SALVAGE, + PrimarySkillType.SMELTING); /* * Build formatted/localized/etc string lists @@ -171,25 +187,18 @@ public class SkillTools { } private @NotNull PrimarySkillType getSuperAbilityParent(SuperAbilityType superAbilityType) throws InvalidSkillException { - switch(superAbilityType) { - case BERSERK: - return PrimarySkillType.UNARMED; - case GREEN_TERRA: - return PrimarySkillType.HERBALISM; - case TREE_FELLER: - return PrimarySkillType.WOODCUTTING; - case SUPER_BREAKER: - case BLAST_MINING: - return PrimarySkillType.MINING; - case SKULL_SPLITTER: - return PrimarySkillType.AXES; - case SERRATED_STRIKES: - return PrimarySkillType.SWORDS; - case GIGA_DRILL_BREAKER: - return PrimarySkillType.EXCAVATION; - default: - throw new InvalidSkillException("No parent defined for super ability! "+superAbilityType.toString()); - } + return switch (superAbilityType) { + case BERSERK -> PrimarySkillType.UNARMED; + case GREEN_TERRA -> PrimarySkillType.HERBALISM; + case TREE_FELLER -> PrimarySkillType.WOODCUTTING; + case SUPER_BREAKER, BLAST_MINING -> PrimarySkillType.MINING; + case SKULL_SPLITTER -> PrimarySkillType.AXES; + case SERRATED_STRIKES -> PrimarySkillType.SWORDS; + case GIGA_DRILL_BREAKER -> PrimarySkillType.EXCAVATION; + case SUPER_SHOTGUN -> PrimarySkillType.CROSSBOWS; + case TRIDENTS_SUPER_ABILITY -> PrimarySkillType.TRIDENTS; + case EXPLOSIVE_SHOT -> PrimarySkillType.ARCHERY; + }; } /** @@ -319,24 +328,19 @@ public class SkillTools { } public Set getSubSkills(PrimarySkillType primarySkillType) { - //TODO: Cache this! return primarySkillChildrenMap.get(primarySkillType); } - public double getXpModifier(PrimarySkillType primarySkillType) { + public double getXpMultiplier(PrimarySkillType primarySkillType) { return ExperienceConfig.getInstance().getFormulaSkillModifier(primarySkillType); } // TODO: This is a little "hacky", we probably need to add something to distinguish child skills in the enum, or to use another enum for them public static boolean isChildSkill(PrimarySkillType primarySkillType) { - switch (primarySkillType) { - case SALVAGE: - case SMELTING: - return true; - - default: - return false; - } + return switch (primarySkillType) { + case SALVAGE, SMELTING -> true; + default -> false; + }; } /** @@ -345,8 +349,7 @@ public class SkillTools { * @return the localized name for a {@link PrimarySkillType} */ public String getLocalizedSkillName(PrimarySkillType primarySkillType) { - //TODO: Replace with current impl - return StringUtils.getCapitalized(LocaleLoader.getString(StringUtils.getCapitalized(primarySkillType.toString()) + ".SkillName")); + return LocaleLoader.getString(StringUtils.getCapitalized(primarySkillType.toString()) + ".SkillName"); } public boolean doesPlayerHaveSkillPermission(Player player, PrimarySkillType primarySkillType) { @@ -401,34 +404,7 @@ public class SkillTools { * @return true if the player has permissions, false otherwise */ public boolean superAbilityPermissionCheck(SuperAbilityType superAbilityType, Player player) { - switch (superAbilityType) { - case BERSERK: - return Permissions.berserk(player); - - case BLAST_MINING: - return Permissions.remoteDetonation(player); - - case GIGA_DRILL_BREAKER: - return Permissions.gigaDrillBreaker(player); - - case GREEN_TERRA: - return Permissions.greenTerra(player); - - case SERRATED_STRIKES: - return Permissions.serratedStrikes(player); - - case SKULL_SPLITTER: - return Permissions.skullSplitter(player); - - case SUPER_BREAKER: - return Permissions.superBreaker(player); - - case TREE_FELLER: - return Permissions.treeFeller(player); - - default: - return false; - } + return superAbilityType.getPermissions(player); } public @NotNull List getChildSkills() { @@ -450,4 +426,17 @@ public class SkillTools { public @NotNull ImmutableList getMiscSkills() { return MISC_SKILLS; } + + public @NotNull ImmutableList getChildSkillParents(PrimarySkillType childSkill) + throws IllegalArgumentException { + switch (childSkill) { + case SALVAGE -> { + return SALVAGE_PARENTS; + } + case SMELTING -> { + return SMELTING_PARENTS; + } + default -> throw new IllegalArgumentException("Skill " + childSkill + " is not a child skill"); + } + } } diff --git a/src/main/java/com/gmail/nossr50/util/skills/SkillUtils.java b/src/main/java/com/gmail/nossr50/util/skills/SkillUtils.java index 6a15963ef..b0d406ae6 100644 --- a/src/main/java/com/gmail/nossr50/util/skills/SkillUtils.java +++ b/src/main/java/com/gmail/nossr50/util/skills/SkillUtils.java @@ -13,6 +13,7 @@ import com.gmail.nossr50.mcMMO; import com.gmail.nossr50.metadata.ItemMetadataService; import com.gmail.nossr50.util.ItemUtils; import com.gmail.nossr50.util.Misc; +import com.gmail.nossr50.util.Permissions; import com.gmail.nossr50.util.player.NotificationManager; import com.gmail.nossr50.util.player.UserManager; import com.gmail.nossr50.util.text.StringUtils; @@ -264,8 +265,8 @@ public final class SkillUtils { return false; } - - + + /** * Modify the durability of an ItemStack, using Armor specific formula for unbreaking enchant damage reduction * @@ -352,4 +353,14 @@ public final class SkillUtils { return quantity; } + + /** + * Checks if a player can use a skill + * @param player target player + * @param subSkillType target subskill + * @return true if the player has permission and has the skill unlocked + */ + public static boolean canUseSubskill(Player player, @NotNull SubSkillType subSkillType) { + return Permissions.isSubSkillEnabled(player, subSkillType) && RankUtils.hasUnlockedSubskill(player, subSkillType); + } } diff --git a/src/main/java/com/gmail/nossr50/util/text/TextComponentFactory.java b/src/main/java/com/gmail/nossr50/util/text/TextComponentFactory.java index 81ac8da28..89981bb85 100644 --- a/src/main/java/com/gmail/nossr50/util/text/TextComponentFactory.java +++ b/src/main/java/com/gmail/nossr50/util/text/TextComponentFactory.java @@ -19,6 +19,7 @@ import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.ChatColor; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -56,18 +57,23 @@ public class TextComponentFactory { return Component.text(text); } - public static void sendPlayerSubSkillWikiLink(Player player, String subskillformatted) { + public static String getSubSkillWikiLink(SubSkillType subSkillType) { + return "https://wiki.mcmmo.org/en/skills/" + + subSkillType.getParentSkill().toString().toLowerCase(Locale.ENGLISH) + "#" + + subSkillType.getWikiUrl().toLowerCase(Locale.ENGLISH); + } + + public static void sendPlayerSubSkillWikiLink(Player player, String subskillformatted, SubSkillType subSkillType) { if (!mcMMO.p.getGeneralConfig().getUrlLinksEnabled()) return; TextComponent.Builder wikiLinkComponent = Component.text().content(LocaleLoader.getString("Overhaul.mcMMO.MmoInfo.Wiki")); wikiLinkComponent.decoration(TextDecoration.UNDERLINED, true); - String wikiUrl = "https://wiki.mcmmo.org/" + subskillformatted; + final String subSkillWikiLink = getSubSkillWikiLink(subSkillType); + wikiLinkComponent.clickEvent(ClickEvent.openUrl(subSkillWikiLink)); - wikiLinkComponent.clickEvent(ClickEvent.openUrl(wikiUrl)); - - TextComponent.Builder componentBuilder = Component.text().content(subskillformatted).append(Component.newline()).append(Component.text(wikiUrl)).color(NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, true); + TextComponent.Builder componentBuilder = Component.text().content(subskillformatted).append(Component.newline()).append(Component.text(subSkillWikiLink)).color(NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, true); wikiLinkComponent.hoverEvent(HoverEvent.showText(componentBuilder.build())); @@ -133,38 +139,37 @@ public class TextComponentFactory { TextComponent.Builder webTextComponent; switch (webLinks) { - case WEBSITE: + case WEBSITE -> { webTextComponent = Component.text().content(LocaleLoader.getString("JSON.Hover.AtSymbolURL")); TextUtils.addChildWebComponent(webTextComponent, "Web"); webTextComponent.clickEvent(getUrlClickEvent(McMMOUrl.urlWebsite)); - break; - case SPIGOT: + } + case SPIGOT -> { webTextComponent = Component.text().content(LocaleLoader.getString("JSON.Hover.AtSymbolURL")); TextUtils.addChildWebComponent(webTextComponent, "Spigot"); webTextComponent.clickEvent(getUrlClickEvent(McMMOUrl.urlSpigot)); - break; - case DISCORD: + } + case DISCORD -> { webTextComponent = Component.text().content(LocaleLoader.getString("JSON.Hover.AtSymbolURL")); TextUtils.addChildWebComponent(webTextComponent, "Discord"); webTextComponent.clickEvent(getUrlClickEvent(McMMOUrl.urlDiscord)); - break; - case PATREON: + } + case PATREON -> { webTextComponent = Component.text().content(LocaleLoader.getString("JSON.Hover.AtSymbolURL")); TextUtils.addChildWebComponent(webTextComponent, "Patreon"); webTextComponent.clickEvent(getUrlClickEvent(McMMOUrl.urlPatreon)); - break; - case WIKI: + } + case WIKI -> { webTextComponent = Component.text().content(LocaleLoader.getString("JSON.Hover.AtSymbolURL")); TextUtils.addChildWebComponent(webTextComponent, "Wiki"); webTextComponent.clickEvent(getUrlClickEvent(McMMOUrl.urlWiki)); - break; - case HELP_TRANSLATE: + } + case HELP_TRANSLATE -> { webTextComponent = Component.text().content(LocaleLoader.getString("JSON.Hover.AtSymbolURL")); TextUtils.addChildWebComponent(webTextComponent, "Lang"); webTextComponent.clickEvent(getUrlClickEvent(McMMOUrl.urlTranslate)); - break; - default: - webTextComponent = Component.text().content("NOT DEFINED"); + } + default -> webTextComponent = Component.text().content("NOT DEFINED"); } TextUtils.addNewHoverComponentToTextComponent(webTextComponent, getUrlHoverEvent(webLinks)); @@ -177,44 +182,45 @@ public class TextComponentFactory { TextComponent.Builder componentBuilder = Component.text().content(webLinks.getNiceTitle()); switch (webLinks) { - case WEBSITE: + case WEBSITE -> { addUrlHeaderHover(webLinks, componentBuilder); componentBuilder.append(Component.newline()).append(Component.newline()); componentBuilder.append(Component.text(webLinks.getLocaleDescription(), NamedTextColor.GREEN)); componentBuilder.append(Component.text("\nDev Blogs, and information related to mcMMO can be found here", NamedTextColor.GRAY)); - break; - case SPIGOT: + } + case SPIGOT -> { addUrlHeaderHover(webLinks, componentBuilder); componentBuilder.append(Component.newline()).append(Component.newline()); componentBuilder.append(Component.text(webLinks.getLocaleDescription(), NamedTextColor.GREEN)); componentBuilder.append(Component.text("\nI post regularly in the discussion thread here!", NamedTextColor.GRAY)); - break; - case PATREON: + } + case PATREON -> { addUrlHeaderHover(webLinks, componentBuilder); componentBuilder.append(Component.newline()).append(Component.newline()); componentBuilder.append(Component.text(webLinks.getLocaleDescription(), NamedTextColor.GREEN)); componentBuilder.append(Component.newline()); componentBuilder.append(Component.text("Show support by buying me a coffee :)", NamedTextColor.GRAY)); - break; - case WIKI: + } + case WIKI -> { addUrlHeaderHover(webLinks, componentBuilder); componentBuilder.append(Component.newline()).append(Component.newline()); componentBuilder.append(Component.text(webLinks.getLocaleDescription(), NamedTextColor.GREEN)); componentBuilder.append(Component.newline()); componentBuilder.append(Component.text("I'm looking for more wiki staff, contact me on our discord!", NamedTextColor.DARK_GRAY)); - break; - case DISCORD: + } + case DISCORD -> { addUrlHeaderHover(webLinks, componentBuilder); componentBuilder.append(Component.newline()).append(Component.newline()); componentBuilder.append(Component.text(webLinks.getLocaleDescription(), NamedTextColor.GREEN)); - break; - case HELP_TRANSLATE: + } + case HELP_TRANSLATE -> { addUrlHeaderHover(webLinks, componentBuilder); componentBuilder.append(Component.newline()).append(Component.newline()); componentBuilder.append(Component.text(webLinks.getLocaleDescription(), NamedTextColor.GREEN)); componentBuilder.append(Component.newline()); componentBuilder.append(Component.text("You can use this website to help translate mcMMO into your language!" + "\nIf you want to know more contact me in discord.", NamedTextColor.DARK_GRAY)); + } } return componentBuilder.build(); @@ -230,8 +236,8 @@ public class TextComponentFactory { } private static Component getSubSkillTextComponent(Player player, SubSkillType subSkillType) { - //Get skill name - String skillName = subSkillType.getLocaleName(); + //Get skill name and strip it of color + final String skillName = ChatColor.stripColor(subSkillType.getLocaleName()); boolean skillUnlocked = RankUtils.hasUnlockedSubskill(player, subSkillType); @@ -301,7 +307,7 @@ public class TextComponentFactory { * @return the hover basecomponent object for this subskill */ private static Component getSubSkillHoverEventJSON(AbstractSubSkill abstractSubSkill, Player player) { - String skillName = abstractSubSkill.getNiceName(); + String skillName = ChatColor.stripColor(abstractSubSkill.getNiceName()); /* * Hover Event BaseComponent color table @@ -394,7 +400,8 @@ public class TextComponentFactory { } private static Component getSubSkillHoverEventJSON(SubSkillType subSkillType, Player player) { - String skillName = subSkillType.getLocaleName(); + // Get skill name and strip it of color + String skillName = ChatColor.stripColor(subSkillType.getLocaleName()); /* * Hover Event BaseComponent color table @@ -428,11 +435,9 @@ public class TextComponentFactory { } componentBuilder.append(Component.newline()); - componentBuilder.append(Component.text(LocaleLoader.getString("JSON.DescriptionHeader"))); - componentBuilder.color(ccDescriptionHeader); + componentBuilder.append(Component.text(LocaleLoader.getString("JSON.DescriptionHeader")).color(ccDescriptionHeader)); componentBuilder.append(Component.newline()); - componentBuilder.append(Component.text(subSkillType.getLocaleDescription())); - componentBuilder.color(ccDescription); + componentBuilder.append(Component.text(ChatColor.stripColor(subSkillType.getLocaleDescription())).color(ccDescription)); } return componentBuilder.build(); @@ -474,7 +479,7 @@ public class TextComponentFactory { /* NEW SKILL SYSTEM */ for (AbstractSubSkill abstractSubSkill : InteractionManager.getSubSkillList()) { if (abstractSubSkill.getPrimarySkill() == parentSkill) { - if (Permissions.isSubSkillEnabled(player, abstractSubSkill)) + if (Permissions.isSubSkillEnabled(player, abstractSubSkill.getSubSkillType())) textComponents.add(TextComponentFactory.getSubSkillTextComponent(player, abstractSubSkill)); } } diff --git a/src/main/resources/advanced.yml b/src/main/resources/advanced.yml index 4e37db678..7d56c4e37 100644 --- a/src/main/resources/advanced.yml +++ b/src/main/resources/advanced.yml @@ -5,7 +5,7 @@ # For advanced users only! There is no need to change anything here. # # You can customize almost every aspect of every skill here. -# Its mainly here if you've customized the experience formula. +# It's mainly here if you've customized the experience formula. # Configure at what level you get better with certain abilities. # ##### @@ -20,7 +20,7 @@ Feedback: PlayerTips: true SkillCommand: BlankLinesAboveHeader: true - # If sendtitles is true messages will be sent using the title api (BIG TEXT ON SCREEN) + # If sendtitles is true, messages will be sent using the title api (BIG TEXT ON SCREEN) Events: XP: SendTitles: true @@ -264,6 +264,11 @@ Skills: MaxBonusLevel: Standard: 100 RetroMode: 1000 + VerdantBounty: + ChanceMax: 50.0 + MaxBonusLevel: + Standard: 1000 + RetroMode: 10000 HylianLuck: # ChanceMax: Maximum chance of Hylian Luck when on or higher @@ -284,6 +289,11 @@ Skills: # Settings for Mining ### Mining: + MotherLode: + MaxBonusLevel: + Standard: 1000 + RetroMode: 10000 + ChanceMax: 50.0 SuperBreaker: AllowTripleDrops: true DoubleDrops: @@ -608,9 +618,16 @@ Skills: Knock_On_Wood: Add_XP_Orbs_To_Drops: true + # Triple Drops + CleanCuts: + # ChanceMax: Maximum chance of receiving triple drops (100 = 100%) + # MaxBonusLevel: Level when the maximum chance of receiving triple drops is reached + ChanceMax: 50.0 + MaxBonusLevel: + Standard: 1000 + RetroMode: 10000 # Double Drops HarvestLumber: - # ChanceMax & MaxBonusLevel are only used for Classic, I'll make that more clear in the future. # ChanceMax: Maximum chance of receiving double drops (100 = 100%) # MaxBonusLevel: Level when the maximum chance of receiving double drops is reached ChanceMax: 100.0 diff --git a/src/main/resources/chat.yml b/src/main/resources/chat.yml index 34f812a07..febf9c9f6 100644 --- a/src/main/resources/chat.yml +++ b/src/main/resources/chat.yml @@ -8,10 +8,12 @@ Chat: Enable: true # Whether to use the current display name of a player Use_Display_Names: true + Send_To_Console: true Spies: # Whether players with the chat spy permission join the server with chat spying toggled on Automatically_Enable_Spying: false Admin: + Send_To_Console: true # Enable or disable admin chat Enable: true # Whether to use the current display name of a player diff --git a/src/main/resources/child.yml b/src/main/resources/child.yml deleted file mode 100644 index 685a6ce71..000000000 --- a/src/main/resources/child.yml +++ /dev/null @@ -1,16 +0,0 @@ -# -# mcMMO child skill configuration -# Last updated on ${project.version}-b${BUILD_NUMBER} -# -# You do not need to modify this file except to change parents of child skills -# -# If you wish a child skill to be the parent of another child skill, you must also make your changes to the child.yml within the jar -# WARNING: THIS IS NOT SUPPORTED, IF YOU DO SO YOU ARE RESPONSIBLE FOR THE ISSUES THAT MAY ARISE. That said, watch out for circular dependencies, those are bad. -# -##### -Salvage: - - Fishing - - Repair -Smelting: - - Mining - - Repair \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 789729cc6..4df9d2eb0 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -49,6 +49,10 @@ General: RetroMode: Enabled: true Locale: en_US + PowerLevel: + Skill_Mastery: + Enabled: true + AprilFoolsEvent: true MOTD_Enabled: true EventBroadcasts: true EventInfoOnPlayerJoin: true diff --git a/src/main/resources/custom_item_support.yml b/src/main/resources/custom_item_support.yml new file mode 100644 index 000000000..76285867e --- /dev/null +++ b/src/main/resources/custom_item_support.yml @@ -0,0 +1,11 @@ +# This is meant to be a general config for allowing mcMMO to allow interaction with custom items. +# In the future, I would like to add configs to be specific about certain custom items. +# For now, support is generalized to whether the custom item has a custom model. +# This is an easy solution to implement for now, but not the most ideal. +Custom_Item_Support: + Repair: + # Turn this off to disable repair on any items with custom model data + Allow_Repair_On_Items_With_Custom_Model_Data: true + Salvage: + # Turn this off to disable salvage on any items with custom model data + Allow_Salvage_On_Items_With_Custom_Model_Data: true diff --git a/src/main/resources/experience.yml b/src/main/resources/experience.yml index 00e72f01d..a41609521 100644 --- a/src/main/resources/experience.yml +++ b/src/main/resources/experience.yml @@ -39,6 +39,7 @@ ExploitFix: TreeFellerReducedXP: true PistonCheating: true SnowGolemExcavation: true + PreventArmorStandInteraction: true # This include NPCs from stuff like Citizens, this is not a setting for Vanilla Minecraft Villagers (Which can be considered NPCs) # mcMMO normally doesn't process attacks against an Entity if it is an NPC from another plugin # Of course, mcMMO doesn't know for sure whether something is an NPC, it checks a few known things, see our source code to see how @@ -77,6 +78,10 @@ Experience_Bars: Enable: true Color: BLUE BarStyle: SEGMENTED_6 + Crossbows: + Enable: true + Color: BLUE + BarStyle: SEGMENTED_6 Excavation: Enable: true Color: YELLOW @@ -113,6 +118,10 @@ Experience_Bars: Enable: true Color: RED BarStyle: SEGMENTED_6 + Tridents: + Enable: true + Color: BLUE + BarStyle: SEGMENTED_6 Unarmed: Enable: true Color: BLUE @@ -159,8 +168,10 @@ Experience_Formula: Breeding: Multiplier: 1.0 - # Experience gained will get divided by these values. 1.0 by default, 2.0 means two times less XP gained. - Modifier: + # Experience gained will get multiplied by these values. 1.0 by default, 0.5 means half XP gained. This happens right before multiplying the XP by the global multiplier. + Skill_Multiplier: + Crossbows: 1.0 + Tridents: 1.0 Swords: 1.0 Taming: 1.0 Acrobatics: 1.0 diff --git a/src/main/resources/locale/locale_en_US.properties b/src/main/resources/locale/locale_en_US.properties index 27c45d0d9..11e550b20 100644 --- a/src/main/resources/locale/locale_en_US.properties +++ b/src/main/resources/locale/locale_en_US.properties @@ -1,10 +1,5 @@ -#I'm going to try to normalize our locale file, forgive the mess for now. -# TODO: Update JSON to support hex - -#DO NOT USE COLOR CODES IN THE JSON KEYS -#COLORS ARE DEFINED IN advanced.yml IF YOU WISH TO CHANGE THEM JSON.Rank=Rank -JSON.DescriptionHeader=Description +JSON.DescriptionHeader=Description: JSON.JWrapper.Header=Details JSON.Type.Passive=Passive JSON.Type.Active=Active @@ -21,6 +16,7 @@ JSON.Acrobatics=Acrobatics JSON.Alchemy=Alchemy JSON.Archery=Archery JSON.Axes=Axes +JSON.Crossbows=Crossbows JSON.Excavation=Excavation JSON.Fishing=Fishing JSON.Herbalism=Herbalism @@ -29,6 +25,7 @@ JSON.Repair=Repair JSON.Salvage=Salvage JSON.Swords=Swords JSON.Taming=Taming +JSON.Tridents=Tridents JSON.Unarmed=Unarmed JSON.Woodcutting=Woodcutting JSON.URL.Website=The official mcMMO Website! @@ -88,6 +85,7 @@ Overhaul.Name.Acrobatics=Acrobatics Overhaul.Name.Alchemy=Alchemy Overhaul.Name.Archery=Archery Overhaul.Name.Axes=Axes +Overhaul.Name.Crossbows=Crossbows Overhaul.Name.Excavation=Excavation Overhaul.Name.Fishing=Fishing Overhaul.Name.Herbalism=Herbalism @@ -97,6 +95,7 @@ Overhaul.Name.Salvage=Salvage Overhaul.Name.Smelting=Smelting Overhaul.Name.Swords=Swords Overhaul.Name.Taming=Taming +Overhaul.Name.Tridents=Tridents Overhaul.Name.Unarmed=Unarmed Overhaul.Name.Woodcutting=Woodcutting # /mcMMO Command Style Stuff @@ -112,6 +111,7 @@ XPBar.Acrobatics=Acrobatics Lv.&6{0} XPBar.Alchemy=Alchemy Lv.&6{0} XPBar.Archery=Archery Lv.&6{0} XPBar.Axes=Axes Lv.&6{0} +XPBar.Crossbows=Crossbows Lv.&6{0} XPBar.Excavation=Excavation Lv.&6{0} XPBar.Fishing=Fishing Lv.&6{0} XPBar.Herbalism=Herbalism Lv.&6{0} @@ -121,6 +121,7 @@ XPBar.Salvage=Salvage Lv.&6{0} XPBar.Smelting=Smelting Lv.&6{0} XPBar.Swords=Swords Lv.&6{0} XPBar.Taming=Taming Lv.&6{0} +XPBar.Tridents=Tridents Lv.&6{0} XPBar.Unarmed=Unarmed Lv.&6{0} XPBar.Woodcutting=Woodcutting Lv.&6{0} #This is just a preset template that gets used if the 'ExtraDetails' setting is turned on in experience.yml (off by default), you can ignore this template and just edit the strings above @@ -176,6 +177,13 @@ Archery.SubSkill.ArcheryLimitBreak.Description=Breaking your limits. Increased d Archery.SubSkill.ArcheryLimitBreak.Stat=Limit Break Max DMG Archery.Listener=Archery: Archery.SkillName=ARCHERY +Archery.SubSkill.ExplosiveShot.Name=Explosive Shot +Archery.SubSkill.ExplosiveShot.Description=Fire an explosive arrow +Archery.Skills.ExplosiveShot.Off= +Archery.Skills.ExplosiveShot.On=&a**EXPLOSIVE SHOT ACTIVATED** +Archery.Skills.ExplosiveShot.Other.Off=Explosive Shot&a has worn off for &e{0} +Archery.Skills.ExplosiveShot.Other.On=&a{0}&2 has used &cExplosive Shot! +Archery.Skills.ExplosiveShot.Refresh=&aYour &Explosive Shot &ability is refreshed! #AXES Axes.Ability.Bonus.0=Axe Mastery Axes.Ability.Bonus.1=Bonus {0} damage @@ -285,8 +293,11 @@ Herbalism.SubSkill.FarmersDiet.Name=Farmer's Diet Herbalism.SubSkill.FarmersDiet.Description=Improves hunger restored from farmed foods Herbalism.SubSkill.FarmersDiet.Stat=Farmer's Diet: &aRank {0} Herbalism.SubSkill.DoubleDrops.Name=Double Drops -Herbalism.SubSkill.DoubleDrops.Description=Double the normal loot +Herbalism.SubSkill.DoubleDrops.Description=Skillfully harvest double the loot Herbalism.SubSkill.DoubleDrops.Stat=Double Drop Chance +Herbalism.SubSkill.VerdantBounty.Name=Verdant Bounty +Herbalism.SubSkill.VerdantBounty.Description=Masterfully harvest triple the loot +Herbalism.SubSkill.VerdantBounty.Stat=Triple Drop Chance Herbalism.SubSkill.HylianLuck.Name=Hylian Luck Herbalism.SubSkill.HylianLuck.Description=Gives a small chance of finding rare items Herbalism.SubSkill.HylianLuck.Stat=Hylian Luck Chance @@ -311,8 +322,11 @@ Mining.SubSkill.SuperBreaker.Name=Super Breaker Mining.SubSkill.SuperBreaker.Description=Speed+, Triple Drop Chance Mining.SubSkill.SuperBreaker.Stat=Super Breaker Length Mining.SubSkill.DoubleDrops.Name=Double Drops -Mining.SubSkill.DoubleDrops.Description=Double the normal loot +Mining.SubSkill.DoubleDrops.Description=Skillfully mine double the loot Mining.SubSkill.DoubleDrops.Stat=Double Drop Chance +Mining.SubSkill.MotherLode.Name=Mother Lode +Mining.SubSkill.MotherLode.Description=Masterfully mine triple the loot +Mining.SubSkill.MotherLode.Stat=Triple Drop Chance Mining.SubSkill.BlastMining.Name=Blast Mining Mining.SubSkill.BlastMining.Description=Bonuses to mining with TNT Mining.SubSkill.BlastMining.Stat=Blast Mining:&a Rank {0}/{1} &7({2}) @@ -395,7 +409,7 @@ Salvage.Skills.Adept.Level=You must be level &e{0}&c to salvage &e{1} Salvage.Skills.TooDamaged=&4This item is too damaged to be salvaged. Salvage.Skills.ArcaneFailed=&cYou were unable to extract the knowledge contained within this item. Salvage.Skills.ArcanePartial=&cYou were only able to extract some of the knowledge contained within this item. -Salvage.Skills.ArcaneSuccess=&aYou able to extract all of the knowledge contained within this item! +Salvage.Skills.ArcaneSuccess=&aYou were able to extract all the knowledge contained within this item! Salvage.Listener.Anvil=&4You have placed a Salvage anvil, use this to Salvage tools and armor. Salvage.Listener=Salvage: Salvage.SkillName=SALVAGE @@ -404,6 +418,53 @@ Salvage.Skills.Lottery.Perfect=&a&lPerfect!&r&6 You salvaged &3{1}&6 effortlessl Salvage.Skills.Lottery.Untrained=&7You aren't properly trained in salvaging. You were only able to recover &c{0}&7 materials from &a{1}&7. #Anvil (Shared between SALVAGE and REPAIR) Anvil.Unbreakable=This item is unbreakable! +Anvil.Repair.Reject.CustomModelData=A mysterious force prevents you from repairing this item... +Anvil.Salvage.Reject.CustomModelData=A mysterious force prevents you from salvaging this item... +#CROSSBOWS +Crossbows.SkillName=CROSSBOWS +Crossbows.Ability.Lower=&7You lower your crossbow. +Crossbows.Ability.Ready=&3You &6ready&3 your Crossbow. +Crossbows.Skills.SSG.Refresh=&aYour &eSuper Shotgun &aability is refreshed! +Crossbows.Skills.SSG.Other.On=&a{0}&2 used &Super Shotgun! +Crossbows.SubSkill.PoweredShot.Name=Powered Shot +Crossbows.SubSkill.PoweredShot.Description=Increases damage done with crossbows +Crossbows.SubSkill.PoweredShot.Stat=Powered Shot Bonus Damage +Crossbows.SubSkill.CrossbowsLimitBreak.Name=Crossbows Limit Break +Crossbows.SubSkill.CrossbowsLimitBreak.Description=Breaking your limits. Increased damage against tough opponents. Intended for PVP, up to server settings for whether it will boost damage in PVE. +Crossbows.SubSkill.CrossbowsLimitBreak.Stat=Limit Break Max DMG +Crossbows.SubSkill.TrickShot.Name=Trick Shot +Crossbows.SubSkill.TrickShot.Description=Richochet arrows with steep angles +Crossbows.SubSkill.TrickShot.Stat=Trick Shot Max Bounces +Crossbows.SubSkill.TrickShot.Stat.Extra=Trick Shot Max Bounces: &a{0} +Crossbows.SubSkill.TrickShot.Stat.Extra2=Trick Shot Reduced DMG per Bounce: &a{0} +Crossbows.SubSkill.SuperShotgun.Name=Super Shotgun +Crossbows.Listener=Crossbows: + +#TRIDENTS +Tridents.SkillName=TRIDENTS +Tridents.Ability.Lower=&7You lower your trident. +Tridents.Ability.Ready=&3You &6ready&3 your Trident. +Tridents.SubSkill.Impale.Name=Impale +Tridents.SubSkill.Impale.Description=Increases damage done with tridents +Tridents.SubSkill.Impale.Stat=Impale Bonus Damage +Tridents.SubSkill.TridentsLimitBreak.Name=Tridents Limit Break +Tridents.SubSkill.TridentsLimitBreak.Description=Breaking your limits. Increased damage against tough opponents. Intended for PVP, up to server settings for whether it will boost damage in PVE. +Tridents.SubSkill.TridentsLimitBreak.Stat=Limit Break Max DMG +Tridents.SubSkill.TridentAbility.Name=WIP +Tridents.Listener=Tridents: + +#MACES +Maces.SkillName=MACES +Maces.Ability.Lower=&7You lower your mace. +Maces.Ability.Ready=&3You &6ready&3 your Mace. +Maces.Skills.MaceSmash.Refresh=&aYour &eGiga Smash &aability is refreshed! +Maces.Skills.MaceSmash.Other.On=&a{0}&2 used &cGiga Smash! +Maces.SubSkill.GigaSmash.Name=Giga Smash +Maces.SubSkill.MacesLimitBreak.Name=Maces Limit Break +Maces.SubSkill.MacesLimitBreak.Description=Breaking your limits. Increased damage against tough opponents. Intended for PVP, up to server settings for whether it will boost damage in PVE. +Maces.SubSkill.MacesLimitBreak.Stat=Limit Break Max DMG +Maces.Listener=Maces: + #SWORDS Swords.Ability.Lower=&7You lower your sword. Swords.Ability.Ready=&3You &6ready&3 your Sword. @@ -542,8 +603,11 @@ Woodcutting.SubSkill.KnockOnWood.Stat=Knock on Wood Woodcutting.SubSkill.KnockOnWood.Loot.Normal=Standard loot from trees Woodcutting.SubSkill.KnockOnWood.Loot.Rank2=Standard loot from trees and experience orbs Woodcutting.SubSkill.HarvestLumber.Name=Harvest Lumber -Woodcutting.SubSkill.HarvestLumber.Description=Skillfully extract more Lumber +Woodcutting.SubSkill.HarvestLumber.Description=Skillfully extract up to double the Lumber Woodcutting.SubSkill.HarvestLumber.Stat=Double Drop Chance +Woodcutting.SubSkill.CleanCuts.Name=Clean Cuts +Woodcutting.SubSkill.CleanCuts.Description=Masterfully extract up to triple the Lumber +Woodcutting.SubSkill.CleanCuts.Stat=Triple Drop Chance Woodcutting.SubSkill.Splinter.Name=Splinter Woodcutting.SubSkill.Splinter.Description=Cut down trees more efficiently. Woodcutting.SubSkill.BarkSurgeon.Name=Bark Surgeon @@ -715,7 +779,7 @@ Commands.XPBar.Reset=&6XP Bar settings for mcMMO have been reset. Commands.XPBar.SettingChanged=&6XP Bar setting for &a{0}&6 is now set to &a{1} Commands.Skill.Invalid=That is not a valid skillname! Commands.Skill.ChildSkill=Child skills are not valid for this command! -Commands.Skill.Leaderboard=--mcMMO &9{0}&e Leaderboard-- +Commands.Skill.Leaderboard=-&e-mcMMO &9{0}&e Leaderboard-- Commands.SkillInfo=&a- View detailed information about a skill Commands.Stats=&a- View your mcMMO stats Commands.ToggleAbility=&a- Toggle ability activation with right click @@ -830,6 +894,7 @@ Commands.XPGain.Alchemy=Brewing Potions Commands.XPGain.Archery=Attacking Monsters Commands.XPGain.Axes=Attacking Monsters Commands.XPGain.Child=Gains levels from Parent Skills +Commands.XPGain.Crossbows=Attacking Monsters Commands.XPGain.Excavation=Digging and finding treasures Commands.XPGain.Fishing=Fishing (Go figure!) Commands.XPGain.Herbalism=Harvesting Herbs @@ -837,6 +902,7 @@ Commands.XPGain.Mining=Mining Stone & Ore Commands.XPGain.Repair=Repairing Commands.XPGain.Swords=Attacking Monsters Commands.XPGain.Taming=Animal Taming, or combat w/ your wolves +Commands.XPGain.Tridents=Attacking Monsters Commands.XPGain.Unarmed=Attacking Monsters Commands.XPGain.Woodcutting=Chopping down trees Commands.XPGain=&8XP GAIN: &f{0} @@ -970,6 +1036,13 @@ Guides.Woodcutting.Section.0=&3About Woodcutting:\n&eWoodcutting is all about ch Guides.Woodcutting.Section.1=&3How does Tree Feller work?\n&eTree Feller is an active ability, you can right-click\n&ewhile holding an ax to activate Tree Feller. This will\n&ecause the entire tree to break instantly, dropping all\n&eof its logs at once. Guides.Woodcutting.Section.2=&3How does Leaf Blower work?\n&eLeaf Blower is a passive ability that will cause leaf\n&eblocks to break instantly when hit with an axe. By default,\nðis ability unlocks at level 100. Guides.Woodcutting.Section.3=&3How do Double Drops work?\n&eThis passive ability gives you a chance to obtain an extra\n&eblock for every log you chop. +# Crossbows +Guides.Crossbows.Section.0=&3About Crossbows:\n&eCrossbows is all about shooting with your crossbow.\n\n&3XP GAIN:\n&eXP is gained whenever you shoot mobs with a crossbow. +Guides.Crossbows.Section.1=&3How does Trickshot work?\n&eTrickshot is an passive ability, you shoot your bolts at a shallow angle with a crossbow to attempt a Trickshot. This will cause the arrow to ricochet off of blocks and potentially hit a target. The number of potential bounces from a ricochet depend on the rank of Trickshot. +# Tridents +Guides.Tridents.Section.0=&3About Tridents:\n&eTridents skill involves impaling foes with your trident.\n\n&3XP GAIN:\n&eXP is gained whenever you hit mobs with a trident. + + #INSPECT Inspect.Offline= &cYou do not have permission to inspect offline players! Inspect.OfflineStats=mcMMO Stats for Offline Player &e{0} diff --git a/src/main/resources/locale/locale_ko.properties b/src/main/resources/locale/locale_ko.properties index 5eabe51e2..4f1533bd6 100644 --- a/src/main/resources/locale/locale_ko.properties +++ b/src/main/resources/locale/locale_ko.properties @@ -1,60 +1,49 @@ # korean locale file v2.0 # ----------------- -# Setting up a new standard for string names, for easier localization. -# SkillName.SubType.LocalEffect.Increment -# Skill name, such as ACROBATICS, MINING. -# Sub-type, such as EFFECT, ABILITY, COMBAT. -# Local Effect, local text for Skill Name; ACROBATICS_ROLL, IGNITIONTIME,GRACECHANCE. -# Increment, for multi-line or array-like lists. -# ###### -# -# EXAMPLES: -# -# Acrobatics.Ability.Proc -# Axes.Ability.Refresh.1 -# -# --wolfwork +# Work : MangChi__ / JJangGu +#로케일 파일을 정규화하려고 합니다. 지금은 혼란스러운 점을 용서해 주세요. +# TODO: 16진수를 지원하도록 JSON을 업데이트합니다. -#DO NOT USE COLOR CODES IN THE JSON KEYS -#COLORS ARE DEFINED IN advanced.yml IF YOU WISH TO CHANGE THEM -JSON.Rank=랭크 +#JSON 키에 색상 코드를 사용하지 마세요. +#색상을 변경하려는 경우에는 Advanced.yml에 정의되어 있습니다. +JSON.Rank=등급 JSON.DescriptionHeader=설명 JSON.JWrapper.Header=세부 정보 -JSON.Type.Passive=패시브 -JSON.Type.Active=액티브 +JSON.Type.Passive=수동적 +JSON.Type.Active=능동적 JSON.Type.SuperAbility=슈퍼 능력 -JSON.Locked=-=[해금되지 않음]=- +JSON.Locked=-=[잠김]=- JSON.LevelRequirement=레벨 요구 사항 JSON.JWrapper.Target.Type=대상 유형: JSON.JWrapper.Target.Block=블록 JSON.JWrapper.Target.Player=플레이어 -JSON.JWrapper.Perks.Header=&6행운 퍽 +JSON.JWrapper.Perks.Header=&6운이 좋은 특성 JSON.JWrapper.Perks.Lucky={0}% 더 좋은 확률 JSON.Hover.Tips=팁 JSON.Acrobatics=곡예 JSON.Alchemy=연금술 -JSON.Archery=궁술 -JSON.Axes=부술 -JSON.Excavation=발굴 +JSON.Archery=활쏘기 +JSON.Axes=참수 +JSON.Excavation=파괴 JSON.Fishing=낚시 -JSON.Herbalism=약초학 +JSON.Herbalism=약초 채집 JSON.Mining=채광 JSON.Repair=수리 -JSON.Salvage=회수 +JSON.Salvage=분해 JSON.Swords=검술 -JSON.Taming=조련 -JSON.Unarmed=비무장 -JSON.Woodcutting=벌목 -JSON.URL.Website=mcMMO 공식 웹사이트! -JSON.URL.Discord=mcMMO 공식 디스코드 서버! -JSON.URL.Patreon=nossr50와 mcMMO를 위한 Patreon에서 지원하세요! -JSON.URL.Spigot=mcMMO 공식 Spigot 리소스 페이지! +JSON.Taming=길들이기 +JSON.Unarmed=맨손전투 +JSON.Woodcutting=나무 베기 +JSON.URL.Website=공식 mcMMO 웹사이트! +JSON.URL.Discord=공식 mcMMO 디스코드 서버! +JSON.URL.Patreon=nossr50과 mcMMO에 대한 작업을 Patreon에서 지원하세요! +JSON.URL.Spigot=공식 mcMMO Spigot 리소스 페이지! JSON.URL.Translation=mcMMO를 다른 언어로 번역하세요! -JSON.URL.Wiki=mcMMO 공식 위키! -JSON.SkillUnlockMessage=&6[ mcMMO&e @&3{0} &6랭크 &3{1}&6이(가) 해제되었습니다! ] -JSON.Hover.Rank=&e&l랭크:&r &f{0} +JSON.URL.Wiki=공식 mcMMO 위키! +JSON.SkillUnlockMessage=&6[ mcMMO&e @&3{0} &6등급 &3{1}&6 해제되었습니다! ] +JSON.Hover.Rank=&e&l등급:&r &f{0} JSON.Hover.NextRank=&7&o다음 업그레이드: 레벨 {0} -# for JSON.Hover.Mystery you can add {0} to insert the level required into the name, I don't like how that looks so I'm not doing that atm +# JSON.Hover.Mystery에 {0}을 추가하여 필요한 레벨을 이름에 삽입할 수 있지만, 현재는 사용하지 않습니다. JSON.Hover.Mystery=&7??? JSON.Hover.Mystery2=&e[&8{0}&e]&8???&r JSON.Hover.SkillName=&3{0}&r @@ -63,29 +52,27 @@ JSON.Hover.MaxRankSkillName=&6{0}&r JSON.Hover.AtSymbolSkills=&e@ JSON.Hover.AtSymbolURL=&e@ -#This is the message sent to players when an ability is activated +# 능력이 활성화될 때 플레이어에게 전송되는 메시지입니다. JSON.Notification.SuperAbility={0} -#These are the JSON Strings used for SubSkills -JSON.Acrobatics.Roll.Interaction.Activated=테스트 &c구른 테스트 -JSON.Acrobatics.SubSkill.Roll.Details.Tips=낙하 중 웅크리기 키를 누르면 일반적으로 받을 데미지의 2배까지 방지할 수 있습니다! -Anvil.SingleItemStack=&c하나 이상의 아이템을 가진 아이템 스택은 회수하거나 수리할 수 없습니다. 먼저 스택을 분할하세요. +# 하위 스킬에 사용되는 JSON 문자열입니다. +JSON.Acrobatics.Roll.Interaction.Activated=테스트 &c굴러가기 테스트 +JSON.Acrobatics.SubSkill.Roll.Details.Tips=낙하 중에 스프린트 키를 누르면 일반적으로 받을 데미지의 최대 2배까지 예방할 수 있습니다! +Anvil.SingleItemStack=&c한 개 이상의 아이템이 있는 아이템 스택은 분해하거나 수리할 수 없습니다. 먼저 스택을 나누세요. -#DO NOT USE COLOR CODES IN THE JSON KEYS -#COLORS ARE DEFINED IN advanced.yml IF YOU WISH TO CHANGE THEM +#JSON 키에 색상 코드를 사용하지 마세요. +#색상을 변경하려는 경우에는 Advanced.yml에 정의되어 있습니다. mcMMO.Template.Prefix=&6(&amcMMO&6) &7{0} - -# BEGIN STYLING -Ability.Generic.Refresh=&a**능력이 갱신되었습니다!** +# 스타일링 시작 +Ability.Generic.Refresh=&a**능력 갱신됨!** Ability.Generic.Template.Lock=&7{0} - -# Skill Command Styling +# 스킬 명령어 스타일링 Ability.Generic.Template=&3{0}: &a{1} Ability.Generic.Template.Custom=&3{0} Skills.Overhaul.Header=&c[]=====[]&a {0} &c[]=====[] Effects.Effects=효과 -Effects.SubSkills.Overhaul=부가 스킬 +Effects.SubSkills.Overhaul=하위 스킬 Effects.Child.Overhaul=&3하위 레벨.&e {0}&3: {1} Effects.Child.ParentList=&a{0}&6(&3레벨.&e{1}&6) Effects.Level.Overhaul=&6레벨: &e{0} &3경험치&e(&6{1}&e/&6{2}&e) @@ -93,1197 +80,1077 @@ Effects.Parent=&6{0} - Effects.Template=&3{0}: &a{1} Commands.Stats.Self.Overhaul=스탯 Commands.XPGain.Overhaul=&6경험치 획득: &3{0} -MOTD.Version.Overhaul=&6[mcMMO] &3Overhaul 시대&6 - &3{0} -Overhaul.mcMMO.Header=&c[]=====[]&a mcMMO - Overhaul 시대 &c[]=====[] +MOTD.Version.Overhaul=&6[mcMMO] &3오버홀 시대&6 - &3{0} +Overhaul.mcMMO.Header=&c[]=====[]&a mcMMO - 오버홀 시대 &c[]=====[] Overhaul.mcMMO.Url.Wrap.Prefix=&c[| Overhaul.mcMMO.Url.Wrap.Suffix=&c|] -Overhaul.mcMMO.MmoInfo.Wiki=&e[&f이 스킬을 위키에서 보기!&e] - +Overhaul.mcMMO.MmoInfo.Wiki=&e[&f이 스킬을 위키에서 확인하세요!&e] +# Overhaul.Levelup can take {0} - Skill Name defined in Overhaul.Name {1} - Amount of levels gained {2} - Level in skill +Overhaul.Levelup=&l{0} 능력이 증가하여 &r&a&l{2}&r&f이 되었습니다. +Overhaul.Name.Acrobatics=곡예 +Overhaul.Name.Alchemy=연금술 +Overhaul.Name.Archery=활쏘기 +Overhaul.Name.Axes=참수 +Overhaul.Name.Excavation=파괴 +Overhaul.Name.Fishing=낚시 +Overhaul.Name.Herbalism=약초 채집 +Overhaul.Name.Mining=채광 +Overhaul.Name.Repair=수리 +Overhaul.Name.Salvage=분해 +Overhaul.Name.Smelting=제련 +Overhaul.Name.Swords=검술 +Overhaul.Name.Taming=길들이기 +Overhaul.Name.Unarmed=맨손 전투 +Overhaul.Name.Woodcutting=벌목 # /mcMMO Command Style Stuff Commands.mcc.Header=&c---[]&amcMMO 명령어&c[]--- Commands.Other=&c---[]&a특수 명령어&c[]--- Commands.Party.Header=&c-----[]&a파티&c[]----- Commands.Party.Features.Header=&c-----[]&a기능&c[]----- -# XP BAR Allows for the following variables -- {0} = Skill Level, {1} Current XP, {2} XP Needed for next level, {3} Power Level, {4} Percentage of Level -# Make sure you turn on Experience_Bars.ThisMayCauseLag.AlwaysUpdateTitlesWhenXPIsGained if you want the XP bar title to update every time a player gains XP! +# XP 바는 다음 변수를 사용할 수 있습니다. -- {0} = 스킬 레벨, {1} 현재 경험치, {2} 다음 레벨까지 필요한 경험치, {3} 파워 레벨, {4} 레벨의 백분율 +# XP 바 제목이 플레이어가 경험치를 획득할 때마다 업데이트되기를 원한다면 Experience_Bars.ThisMayCauseLag.AlwaysUpdateTitlesWhenXPIsGained를 켜주세요! XPBar.Template={0} -XPBar.Template.EarlyGameBoost=&6새로운 스킬을 습득하는 중... -XPBar.Acrobatics=곡예 Lv.&6{0} -XPBar.Alchemy=연금술 Lv.&6{0} -XPBar.Archery=궁술 Lv.&6{0} -XPBar.Axes=부술 Lv.&6{0} -XPBar.Excavation=발굴 Lv.&6{0} -XPBar.Fishing=낚시 Lv.&6{0} -XPBar.Herbalism=약초학 Lv.&6{0} -XPBar.Mining=채광 Lv.&6{0} -XPBar.Repair=수리 Lv.&6{0} -XPBar.Salvage=회수 Lv.&6{0} -XPBar.Smelting=제련 Lv.&6{0} -XPBar.Swords=검술 Lv.&6{0} -XPBar.Taming=조련 Lv.&6{0} -XPBar.Unarmed=비무장 Lv.&6{0} -XPBar.Woodcutting=벌목 Lv.&6{0} +XPBar.Template.EarlyGameBoost=&6새로운 스킬 배움 중... +XPBar.Acrobatics=곡예 레벨 &6{0} +XPBar.Alchemy=연금술 레벨 &6{0} +XPBar.Archery=활쏘기 레벨 &6{0} +XPBar.Axes=참수 레벨 &6{0} +XPBar.Excavation=파괴 레벨 &6{0} +XPBar.Fishing=낚시 레벨 &6{0} +XPBar.Herbalism=약초 채집 레벨 &6{0} +XPBar.Mining=채광 레벨 &6{0} +XPBar.Repair=수리 레벨 &6{0} +XPBar.Salvage=분해 레벨 &6{0} +XPBar.Smelting=제련 레벨 &6{0} +XPBar.Swords=검술 레벨 &6{0} +XPBar.Taming=길들이기 레벨 &6{0} +XPBar.Unarmed=맨손 전투 레벨 &6{0} +XPBar.Woodcutting=벌목 레벨 &6{0} +#이것은 experience.yml에서 'ExtraDetails' 설정이 켜져 있는 경우(기본적으로 꺼져 있음) 사용되는 미리 설정된 템플릿일 뿐입니다. 이 템플릿을 무시하고 위의 문자열만 편집하면 됩니다. XPBar.Complex.Template={0} &3 {4}&f% &3(&f{1}&3/&f{2}&3) -#This is just a preset template that gets used if the 'ExtraDetails' setting is turned on in experience.yml (off by default), you can ignore this template and just edit the strings above -XPBar.Complex.Template={0} &3 {4}&f% &3(&f{1}&3/&f{2}&3) -# XP BAR Allows for the following variables -- {0} = Skill Level, {1} Current XP, {2} XP Needed for next level, {3} Power Level, {4} Percentage of Level -# Make sure you turn on Experience_Bars.ThisMayCauseLag.AlwaysUpdateTitlesWhenXPIsGained if you want the XP bar title to update every time a player gains XP! -# END STYLING +# XP 바는 다음 변수를 사용할 수 있습니다. -- {0} = 스킬 레벨, {1} 현재 경험치, {2} 다음 레벨까지 필요한 경험치, {3} 파워 레벨, {4} 레벨의 백분율 +# XP 바 제목이 플레이어가 경험치를 획득할 때마다 업데이트되기를 원한다면 Experience_Bars.ThisMayCauseLag.AlwaysUpdateTitlesWhenXPIsGained를 켜주세요! +# 스타일링 끝 - -#ACROBATICS -Acrobatics.SubSkill.Roll.Stats=&6구르기 확률 &e{0}%&6 우아한 구르기 확률&e {1}% -Acrobatics.SubSkill.Roll.Stat=구르기 확률 -Acrobatics.SubSkill.Roll.Stat.Extra=우아한 구르기 확률 -Acrobatics.SubSkill.Roll.Chance=구르기 확률: &e{0} -Acrobatics.SubSkill.Roll.Mechanics=&7구르기는 수동적인 부분이 있는 능력입니다.\n낙하 데미지를 입을 때마다, 당신은 당신의 능력 레벨에 따라 데미지를 완전히 무효화할 수 있습니다. 능력 레벨이 &e{6}%&7일 때, 당신은 &e{0}%&7의 확률로 데미지를 무효화할 수 있으며, 우아한 구르기를 활성화하면 &e{1}%&7의 확률로 데미지를 무효화할 수 있습니다.\n성공 확률은 능력 레벨에 따라 선형적으로 조정되며, 능력 레벨 &e{2}&7에서 최대치에 도달합니다. 곡예 능력을 1 레벨 올릴 때마다 &e{3}%&7의 확률이 증가합니다.\n웅크리기 버튼을 누르면 구르기 확률이 2배가 되며, 데미지를 2배로 감소시킬 수 있습니다. 웅크리기를 누르면 일반적인 구르기가 우아한 구르기로 변합니다.\n구르기는 최대 &c{4}&7의 데미지를 무효화합니다. 우아한 구르기는 최대 &a{5}&7의 데미지를 무효화합니다. -Acrobatics.SubSkill.GracefulRoll.Name=우아한 구르기 -Acrobatics.SubSkill.Dodge.Stat=구르기 확률 -Acrobatics.Ability.Proc=&a**우아한 구르기** -Acrobatics.Combat.Proc=&a**회피** -Acrobatics.DodgeChance=회피 확률: &e{0} -Acrobatics.SubSkill.Roll.Name=구르기 -Acrobatics.SubSkill.Roll.Description=추락 데미지 감소 또는 무효 -Acrobatics.SubSkill.GracefulRoll.Name=우아한 구르기 -Acrobatics.SubSkill.GracefulRoll.Description=구르기 2배 효과 +#재주 넘기 +Acrobatics.Ability.Proc=&a**용아치** (Graceful Landing) +Acrobatics.Combat.Proc=&a**회피** (Dodged) +Acrobatics.SubSkill.Roll.Stats=&6Roll 확률 &e{0}%&6 우아한 Roll 확률 &e{1}% +Acrobatics.SubSkill.Roll.Stat=Roll 확률 +Acrobatics.SubSkill.Roll.Stat.Extra=우아한 Roll 확률 +Acrobatics.SubSkill.Roll.Name=Roll +Acrobatics.SubSkill.Roll.Description=전략적으로 착지하여 피해를 피합니다. +Acrobatics.SubSkill.Roll.Chance=Roll 확률: &e{0} +Acrobatics.SubSkill.Roll.GraceChance=우아한 Roll 확률: &e{0} +Acrobatics.SubSkill.Roll.Mechanics=&7Roll은 수동 구성 요소가 있는 능동적인 하위 기술입니다.\n낙하 피해를 입을 때마다 기술 레벨에 따라 피해를 완전히 무효화할 기회가 있습니다. 레벨 &e{6}%&7에서 피해를 방지할 확률은 &e{0}%&7이며, 우아한 Roll을 활성화하면 &e{1}%&7입니다.\n성공 확률은 능력 레벨에 따라 선형 곡선으로 조정되며, 능력이 있는 Acrobatics 레벨마다 &e{3}%&7의 성공 확률이 주어집니다.\n스니크 버튼을 누르면 낙하 피해를 피하는 기회가 두 배로 늘어나며, 최대 2배의 낙하 피해를 피할 수 있습니다! 스니크를 누르면 일반적인 Roll이 우아한 Roll로 변합니다.\nRoll은 최대 &c{4}&7의 피해를 방지할 수 있으며, 우아한 Roll은 최대 &a{5}&7의 피해를 방지할 수 있습니다. +Acrobatics.SubSkill.GracefulRoll.Name=우아한 Roll +Acrobatics.SubSkill.GracefulRoll.Description=일반 Roll의 2배 효과적인 기술 Acrobatics.SubSkill.Dodge.Name=회피 -Acrobatics.SubSkill.Dodge.Description=낙하 데미지 절반 감소 -Acrobatics.Listener=곡예(ACROBATICS): -Acrobatics.SubSkill.Roll.Chance=구르기 확률: &e{0} -Acrobatics.SubSkill.Roll.GraceChance=우아한 구르기 확률: &e{0} -Acrobatics.Roll.Text=**구르기** +Acrobatics.SubSkill.Dodge.Description=공격 피해를 절반으로 줄입니다. +Acrobatics.SubSkill.Dodge.Stat=회피 확률 +Acrobatics.Listener=곡예: +Acrobatics.Roll.Text=&o**Roll 사용** Acrobatics.SkillName=곡예 -Acrobatics.Skillup=낙법 기술이 {0} 올라 총 {1} 레벨이 되었습니다 -#ALCHEMY -Alchemy.SubSkill.Catalysis.Name=촉매 -Alchemy.SubSkill.Catalysis.Description=포션 양조 속도 증가 -Alchemy.SubSkill.Concoctions.Name=혼합 -Alchemy.SubSkill.Concoctions.Description=더 많은 성분의 포션 양조 -Alchemy.Listener=연금술(ALCHEMY): -Alchemy.Ability.Locked.0={0}레벨 때 기술해제 (촉매) -Alchemy.Catalysis.Speed=양조 속도: &e{0} -Alchemy.Concoctions.Rank=혼합 랭크: &e{0}/{1} -Alchemy.Concoctions.Ingredients=성분 [&e{0}&c]: &e{1} -Alchemy.SkillName=연금술 -Alchemy.Skillup=연금술 기술이 {0} 올라 총 {1} 레벨이 되었습니다 -Alchemy.SubSkill.Catalysis.Stat=양조 속도 -Alchemy.SubSkill.Concoctions.Stat=혼합 랭크: &a{0}&3/&a{1} -Alchemy.SubSkill.Concoctions.Stat.Extra=성분 [&a{0}&3]: &a{1} -Alchemy.Ability.Locked.0={0}+ 레벨에서 해금됩니다 (촉매) +#연금술 +Alchemy.SubSkill.Catalysis.Name=촉매작용 +Alchemy.SubSkill.Catalysis.Description=포션 제조 속도를 증가시킵니다. +Alchemy.SubSkill.Catalysis.Stat=포션 제조 속도 +Alchemy.SubSkill.Concoctions.Name=조합 +Alchemy.SubSkill.Concoctions.Description=더 많은 재료로 포션을 제조합니다. +Alchemy.SubSkill.Concoctions.Stat=조합 순위: &a{0}&3/&a{1} +Alchemy.SubSkill.Concoctions.Stat.Extra=재료 [&a{0}&3]: &a{1} +Alchemy.Listener=연금술: +Alchemy.Ability.Locked.0={0}+ SKILL (CATALYSIS)까지 잠금 Alchemy.SkillName=연금술 -#ARCHERY -Archery.Combat.DazeChance=현혹 확률: &e{0} -Archery.Combat.RetrieveChance=화살 회수 확률: &e{0} -Archery.Combat.SkillshotBonus=쏘기 솜씨 추가 피해 확률: &e{0} -Archery.SubSkill.SkillShot.Name=쏘기 솜씨 -Archery.SubSkill.SkillShot.Description=활 피해 영구 증가 -Archery.SubSkill.Daze.Name=현혹 (플레이어) -Archery.SubSkill.Daze.Description=적에게 혼란, {0} 피해 추가 +#양궁 +Archery.SubSkill.SkillShot.Name=스킬 사격 +Archery.SubSkill.SkillShot.Description=활로 입히는 피해량을 증가시킵니다. +Archery.SubSkill.SkillShot.Stat=스킬 사격 추가 피해 +Archery.SubSkill.Daze.Name=혼란시키기 +Archery.SubSkill.Daze.Description=상대를 혼란시키고 추가 피해를 입힙니다. +Archery.SubSkill.Daze.Stat=혼란 확률 Archery.SubSkill.ArrowRetrieval.Name=화살 회수 -Archery.SubSkill.ArrowRetrieval.Description=시체에서 화살 회수 확률 증가 -Archery.Listener=궁술(ARCHERY): -Archery.SkillName=궁술 -Archery.Skillup=궁술 기술이 {0} 올라 총 {1} 레벨이 되었습니다 -Archery.SubSkill.SkillShot.Stat=스킬샷 추가 피해 -Archery.SubSkill.Daze.Stat=현혹 확률 +Archery.SubSkill.ArrowRetrieval.Description=시체에서 화살을 회수할 확률이 있습니다. Archery.SubSkill.ArrowRetrieval.Stat=화살 회수 확률 -Archery.SubSkill.ArcheryLimitBreak.Name=궁술 한계 돌파 -Archery.SubSkill.ArcheryLimitBreak.Description=한계 돌파. 강한 적에게 추가 피해를 줍니다. PVP를 위해 만들어졌으며, PVE에서 추가 피해를 줄지는 서버 설정에 따라 다릅니다. -Archery.SubSkill.ArcheryLimitBreak.Stat=한계 돌파 최대 피해 +Archery.SubSkill.ArcheryLimitBreak.Name=양궁 한계 극복 +Archery.SubSkill.ArcheryLimitBreak.Description=한계를 극복합니다. 강력한 상대에 대한 피해량이 증가합니다. PVP용으로 적용되며, PVE에서 피해량을 증가시킬지는 서버 설정에 따릅니다. +Archery.SubSkill.ArcheryLimitBreak.Stat=한계 극복 최대 피해량 +Archery.Listener=양궁: +Archery.SkillName=양궁 - -#AXES -Axes.Ability.Bonus.0=도끼 마스터리 -Axes.Ability.Bonus.1={0} 추가 피해 -Axes.Ability.Bonus.2=갑옷 충격 -Axes.Ability.Bonus.3=방어구 추가 피해: {0} -Axes.Ability.Bonus.4=엄청난 충격 -Axes.Ability.Bonus.5=비무장 추가 피해: {0} -Axes.Ability.Lower=&7**도끼 준비 해제** -Axes.Ability.Ready=&a**도끼 준비 완료** -Axes.Combat.CritStruck=&4크리티컬 히트에 맞았습니다! -Axes.Combat.CritChance=크리티컬 히트 확률: &e{0} -Axes.Combat.CriticalHit=크리티컬 히트! -Axes.Combat.GI.Proc=&a**최고의 강타를 때렸습니다** -Axes.Combat.GI.Struck=**엄청난 충격을 받았습니다** -Axes.Combat.SS.Struck=&4뼈 쪼개기에 맞았습니다! -Axes.Combat.SS.Length=뼈 쪼개기 지속시간: &e{0}초 -Axes.SubSkill.SkullSplitter.Name=뼈 쪼개기 (능력) -Axes.SubSkill.SkullSplitter.Description=광역 추가 피해 -Axes.SubSkill.CriticalStrikes.Name=크리티컬 스트라이크 -Axes.SubSkill.CriticalStrikes.Description=피해 2배 -Axes.SubSkill.AxeMastery.Name=도끼 마스터리 -Axes.SubSkill.AxeMastery.Description=추가 특혜 피해 -Axes.SubSkill.ArmorImpact.Name=갑옷 충격 -Axes.SubSkill.ArmorImpact.Description=갑옷 파괴 공격 -Axes.SubSkill.GreaterImpact.Name=엄청난 충격 -Axes.SubSkill.GreaterImpact.Description=비무장 추가 피해 -Axes.Listener=부술(AXES): -Axes.SkillName=부술 -Axes.Skills.SS.Off=**뼈 쪼개기 발동 해제** -Axes.Skills.SS.On=&a**뼈 쪼개기 발동** -Axes.Skills.SS.Refresh=&a당신의 &e뼈 쪼개기 &a기술은 이제 사용 가능합니다! -Axes.Skills.SS.Other.Off={0}님이 &c뼈 쪼개기를&a 준비 해제했습니다 -Axes.Skills.SS.Other.On=&a{0}&2님이 &c뼈 쪼개기를 사용했습니다! -Axes.Skillup=부술 기술이 {0} 올라 총 ({1}) 레벨이 되었습니다 -Axes.Ability.Ready.Extra=도끼 준비 완료. &7({0}은 {1}초 동안 쿨타임 중입니다) -Axes.SubSkill.SkullSplitter.Stat=뼈 쪼개기 지속시간 -Axes.SubSkill.CriticalStrikes.Stat=크리티컬 스트라이크 확률 -Axes.SubSkill.AxesLimitBreak.Name=도끼 한계 돌파 -Axes.SubSkill.AxesLimitBreak.Description=한계를 돌파합니다. 강한 상대에 대한 추가 피해가 증가합니다. PVP를 위해 만들어졌으며, PVE에서 추가 피해를 줄지는 서버 설정에 따라 다릅니다. +#참수 +Axes.Ability.Bonus.0=도끼 숙련 +Axes.Ability.Bonus.1=추가 {0} 피해 +Axes.Ability.Bonus.2=방어구 타격 +Axes.Ability.Bonus.3={0} 추가 피해를 방어구에 입힙니다. +Axes.Ability.Bonus.4=강력한 타격 +Axes.Ability.Bonus.5={0} 추가 피해를 방어구가 없는 적에게 입힙니다. +Axes.Ability.Lower=&7도끼를 내려놓습니다. +Axes.Ability.Ready=&3도끼를 &6준비&3합니다. +Axes.Ability.Ready.Extra=&3참수를 &6준비&3합니다. &7({0}이(가) {1}초 동안 재사용 대기 중입니다) +Axes.Combat.CritStruck=&4치명적으로 맞았습니다! +Axes.Combat.CriticalHit=치명타! +Axes.Combat.GI.Proc=&a**강력한 힘으로 타격받았습니다** +Axes.Combat.GI.Struck=**강력한 타격으로 맞았습니다** +Axes.Combat.SS.Struck=&4해골 분쇄로 맞았습니다! +Axes.SubSkill.SkullSplitter.Name=해골 분쇄 +Axes.SubSkill.SkullSplitter.Description=광역 피해를 입힙니다. +Axes.SubSkill.SkullSplitter.Stat=해골 분쇄 지속 시간 +Axes.SubSkill.CriticalStrikes.Name=치명타 +Axes.SubSkill.CriticalStrikes.Description=두 배의 피해를 입힙니다. +Axes.SubSkill.CriticalStrikes.Stat=치명타 확률 +Axes.SubSkill.AxeMastery.Name=참수 숙련 +Axes.SubSkill.AxeMastery.Description=추가 피해를 입힙니다. +Axes.SubSkill.AxesLimitBreak.Name=참수 한계 돌파 +Axes.SubSkill.AxesLimitBreak.Description=한계를 돌파합니다. 강력한 적에 대한 추가 피해가 증가합니다. PVP를 위해 설계되었으며, PVE에서의 추가 피해 증가 여부는 서버 설정에 따릅니다. Axes.SubSkill.AxesLimitBreak.Stat=한계 돌파 최대 피해 - -#EXCAVATION -Excavation.Ability.Lower=&7**삽 준비 해제** -Excavation.Ability.Ready=&a**삽 준비 완료** -Excavation.SubSkill.GigaDrillBreaker.Name=기가 드릴 버서커 (능력) -Excavation.SubSkill.GigaDrillBreaker.Description=드롭 속도 3배, 경험치 3배, 속도 증가 -Excavation.SubSkill.TreasureHunter.Name=보물 사냥꾼 -Excavation.SubSkill.TreasureHunter.Description=보물 발굴 능력 -Excavation.Effect.Length=기가 드릴 버서커 지속시간: &e{0}초 -Excavation.Listener=발굴(EXCAVATION): -Excavation.SkillName=발굴 -Excavation.Skills.GigaDrillBreaker.Off=**기가 드릴 버서커 발동 해제** -Excavation.Skills.GigaDrillBreaker.On=&a**기가 드릴 버서커 발동** -Excavation.Skills.GigaDrillBreaker.Refresh=&a당신의 &e기가 드릴 버서커 &a기술은 이제 사용 가능합니다! -Excavation.Skills.GigaDrillBreaker.Other.Off={0}&2님은 &c기가 드릴 버서커를 사용했습니다! -Excavation.Skills.GigaDrillBreaker.Other.On=&a{0}&2님은 &c기가 드릴 버서커를 사용 했습니다! -Excavation.Skillup=발굴 기술이 {0} 올라 총 {1} 레벨이 되었습니다 -Excavation.SubSkill.GigaDrillBreaker.Stat=기가 드릴 버서커 지속시간 +Axes.SubSkill.ArmorImpact.Name=방어구 타격 +Axes.SubSkill.ArmorImpact.Description=충분한 힘으로 방어구를 깨뜨립니다. +Axes.SubSkill.GreaterImpact.Name=강력한 타격 +Axes.SubSkill.GreaterImpact.Description=방어구가 없는 적에게 추가 피해를 입힙니다. +Axes.Listener=참수: +Axes.SkillName=참수 +Axes.Skills.SS.Off=**해골 분쇄 효과가 사라졌습니다** +Axes.Skills.SS.On=&a**해골 분쇄 활성화됨** +Axes.Skills.SS.Refresh=&a해당 &e해골 분쇄 &a기술이 회복되었습니다! +Axes.Skills.SS.Other.Off=해골 분쇄&a 효과가 &e{0}에게서 사라졌습니다 +Axes.Skills.SS.Other.On=&a{0}&2이(가) &c해골 분쇄를 사용했습니다! +#발굴 +Excavation.Ability.Lower=&7삽을 내려놓습니다. +Excavation.Ability.Ready=&3삽을 &6준비&3합니다. +Excavation.SubSkill.GigaDrillBreaker.Name=기가 드릴 브레이커 +Excavation.SubSkill.GigaDrillBreaker.Description=드롭 확률 3배, 경험치 3배, 속도 증가 +Excavation.SubSkill.GigaDrillBreaker.Stat=기가 드릴 브레이커 지속 시간 Excavation.SubSkill.Archaeology.Name=고고학 -Excavation.SubSkill.Archaeology.Description=땅의 비밀을 발굴하세요! 높은 기술 레벨은 보물을 찾을 때 경험치 구슬을 찾을 확률을 높입니다! -Excavation.SubSkill.Archaeology.Stat=고고학 경험치 구슬 확률 -Excavation.SubSkill.Archaeology.Stat.Extra=고고학 경험치 구슬량 - -#FISHING -Fishing.Ability.Chance=입질 확률: &e{0} -Fishing.Ability.Info=매직 헌터: &7 **트레져 헌터 랭크 향상** -Fishing.Ability.Locked.0={0}레벨 때 기술 해제 (흔들기) -Fishing.Ability.Locked.1={0}레벨 때 기술 해제 (얼음 낚시) -Fishing.Ability.Locked.2={0}레벨 때 기술 해제 (낚시꾼 장인) -Fishing.Ability.Rank=트레져 헌터 랭크: &e{0}/5랭크 -Fishing.Ability.TH.DropRate= 드롭 비율: &4함정: &e{0} &7공통: &e{1} &a비공통: &e{2}\n&9레어: &e{3} &d에픽: &e{4} &6레전드리: &e{5} &b레코드: &e{6} -Fishing.Ability.TH.MagicRate=매직 헌터 확률: &e{0} -Fishing.Ability.Shake=흔들기 확률: &e{0} -Fishing.Ability.IceFishing=얼음 낚시: 얼음에서 낚시 -Fishing.Ability.FD=어부의 다이어트 랭크: &e{0}랭크 -Fishing.SubSkill.TreasureHunter.Name=트레져 헌터 (패시브) -Fishing.SubSkill.TreasureHunter.Description=물건(그외) 낚시 -Fishing.SubSkill.MagicHunter.Name=매직 헌터 -Fishing.SubSkill.MagicHunter.Description=인챈트된 아이템 발견 -Fishing.SubSkill.Shake.Name=흔들기 (vs. 독립체) -Fishing.SubSkill.Shake.Description=아이템을 몹이나 낚시에서 얻음 -Fishing.SubSkill.FishermansDiet.Name=어부의 다이어트 -Fishing.SubSkill.FishermansDiet.Description=어류 음식 허기 회복 증가 -Fishing.SubSkill.MasterAngler.Name=낚시꾼 장인 -Fishing.SubSkill.IceFishing.Name=얼음 낚시 -Fishing.SubSkill.IceFishing.Description=얼음이 덮혀있는 환경에서 낚시 가능 -Fishing.Chance.Raining=&9 비 특혜 -Fishing.Listener=낚시(FISHING): -Fishing.Ability.TH.MagicFound=&7이 입질에서 마법이 느껴집니다... -Fishing.Ability.TH.Boom=&7폭발 시간!!! -Fishing.Ability.TH.Poison=&7낌새가 좋지 않습니다... -Fishing.SkillName=낚시 -Fishing.Skillup=낚시 기술이 {0} 올라 총 {1} 레벨이 되었습니다 -Fishing.ScarcityTip=&e&o이 지역은 과잉 낚시로 인해 어필이 부족합니다. 더 많은 물고기를 잡으려면 다른 곳에 낚싯대를 던져보세요. 적어도 {0} 블록 이상 떨어진 곳에서 낚시하세요. -Fishing.Scared=&7&o물고기를 놀라게 하는 불규칙한 움직임입니다! -Fishing.Exhausting=&c&o낚싯대를 부적절하게 사용하면 피로가 쌓이고 낚싯대가 닳아버립니다! -Fishing.LowResourcesTip=&7이 지역에는 물고기가 많이 남아있지 않을 것 같습니다. 적어도 {0} 블록 이상 떨어진 곳에서 낚시해보세요. -Fishing.SubSkill.TreasureHunter.Stat=보물 사냥꾼 랭크: &a{0}&3/&a{1} -Fishing.SubSkill.TreasureHunter.Stat.Extra=드롭 비율: &7일반: &e{0} &a희귀: &e{1}\n&9레어: &e{2} &d에픽: &e{3} &6레전드리: &e{4} &b신화: &e{5} -Fishing.SubSkill.MagicHunter.Stat=매직 헌터 확률 +Excavation.SubSkill.Archaeology.Description=땅의 비밀을 밝혀냅니다! 높은 스킬 레벨은 보물을 찾을 때 경험치 오브를 발견할 확률을 높입니다! +Excavation.SubSkill.Archaeology.Stat=고고학 경험치 오브 확률 +Excavation.SubSkill.Archaeology.Stat.Extra=고고학 경험치 오브 양 +Excavation.Listener=발굴: +Excavation.SkillName=발굴 +Excavation.Skills.GigaDrillBreaker.Off=**기가 드릴 브레이커 효과가 사라졌습니다** +Excavation.Skills.GigaDrillBreaker.On=&a**기가 드릴 브레이커 활성화됨** +Excavation.Skills.GigaDrillBreaker.Refresh=&a해당 &e기가 드릴 브레이커 &a기술이 회복되었습니다! +Excavation.Skills.GigaDrillBreaker.Other.Off=기가 드릴 브레이커&a 효과가 &e{0}에게서 사라졌습니다 +Excavation.Skills.GigaDrillBreaker.Other.On=&a{0}&2이(가) &c기가 드릴 브레이커를 사용했습니다! +#낚시 +Fishing.ScarcityTip=&e&o이 지역은 낚시가 과잉되어 있습니다. 더 많은 물고기를 잡으려면 다른 곳에 낚싯대를 던지세요. 적어도 {0} 블록 떨어진 곳에서 낚시하세요. +Fishing.Scared=&7&o난잡한 움직임은 물고기를 놀라게 합니다! +Fishing.Exhausting=&c&o낚시대를 부적절하게 사용하면 피로가 쌓이고 낚시대가 닳습니다! +Fishing.LowResourcesTip=&7이 지역에는 물고기가 많지 않을 것 같습니다. 적어도 {0} 블록 떨어진 곳에서 낚시하세요. +Fishing.Ability.Info=마법사 사냥꾼: &7 **보물사냥꾼 랭크에 따라 향상됩니다** +Fishing.Ability.Locked.0={0}+ 스킬까지 잠금 (흔들기) +Fishing.Ability.Locked.1={0}+ 스킬까지 잠금 (얼음 낚시) +Fishing.Ability.Locked.2={0}+ 스킬까지 잠금 (마스터 낚시꾼) +Fishing.SubSkill.TreasureHunter.Name=보물사냥꾼 +Fishing.SubSkill.TreasureHunter.Description=다양한 물건을 낚아올립니다 +Fishing.SubSkill.TreasureHunter.Stat=보물사냥꾼 랭크: &a{0}&3/&a{1} +Fishing.SubSkill.TreasureHunter.Stat.Extra=드롭 확률: &7흔함: &e{0} &a보통: &e{1}\n&9희귀: &e{2} &d에픽: &e{3} &6전설: &e{4} &b신화: &e{5} +Fishing.SubSkill.MagicHunter.Name=마법사 사냥꾼 +Fishing.SubSkill.MagicHunter.Description=마법 부여된 아이템을 찾습니다 +Fishing.SubSkill.MagicHunter.Stat=마법사 사냥꾼 확률 +Fishing.SubSkill.Shake.Name=흔들기 +Fishing.SubSkill.Shake.Description=낚싯대로 몹이나 플레이어에서 아이템을 흔들어 떨구세요 Fishing.SubSkill.Shake.Stat=흔들기 확률 -Fishing.SubSkill.FishermansDiet.Stat=어부의 다이어트: &a랭크 {0} -Fishing.SubSkill.MasterAngler.Description=어부가 더 자주 물고기를 잡습니다. 보트에서 낚시할 때 더 잘 작동합니다. +Fishing.SubSkill.FishermansDiet.Name=어부의 식단 +Fishing.SubSkill.FishermansDiet.Description=낚은 음식의 포만감을 개선합니다 +Fishing.SubSkill.FishermansDiet.Stat=어부의 식단: &a랭크 {0} +Fishing.SubSkill.MasterAngler.Name=마스터 낚시꾼 +Fishing.SubSkill.MasterAngler.Description=물고기를 더 자주 잡을 수 있으며, 배에서 낚시할 때 더 잘 작동합니다. Fishing.SubSkill.MasterAngler.Stat=낚시 최소 대기 시간 감소: &a-{0} 초 Fishing.SubSkill.MasterAngler.Stat.Extra=낚시 최대 대기 시간 감소: &a-{0} 초 +Fishing.SubSkill.IceFishing.Name=얼음 낚시 +Fishing.SubSkill.IceFishing.Description=얼음 생물이 서식하는 생물꾼입니다 Fishing.SubSkill.IceFishing.Stat=얼음 낚시 - -#HERBALISM -Herbalism.Ability.DoubleDropChance=2배 드롭 확률: &e{0} -Herbalism.Ability.FD=농부의 다이어트: &e{0}랭크 -Herbalism.Ability.GTe.Length=재배의 대지 지속시간: &e{0}초 -Herbalism.Ability.GTe.NeedMore=재배의 대지에 뿌릴 씨가 좀더 필요합니다. -Herbalism.Ability.GTh.Chance=재배의 재능 확률: &e{0} -Herbalism.Ability.GTh.Fail=**재배의 재능 실패** -Herbalism.Ability.GTh.Stage=재배의 재능 단계: &e 작물 재배 {0}단계 -Herbalism.Ability.GTh=&a**재배의 재능** -Herbalism.Ability.HylianLuck=하이랄인의 행운 확률: &e{0} -Herbalism.Ability.Lower=&7**호미 준비 해제** -Herbalism.Ability.Ready=&a**호미 준비 완료** -Herbalism.Ability.ShroomThumb.Chance=버섯재배자의 숨결 확률: &e{0} -Herbalism.Ability.ShroomThumb.Fail=**버섯재배자의 숨결 실패** -Herbalism.SubSkill.GreenTerra.Name=재배의 대지 (능력) -Herbalism.SubSkill.GreenTerra.Description=대지 뿌리기, 드롭 3배, 재배의 재능 부스트 -Herbalism.SubSkill.GreenThumb.Name=재배의 재능 (밀) -Herbalism.SubSkill.GreenThumb.Description=수확시 자동 씨 심기 -Herbalism.Effect.4=재배의 재능 (블록들) -Herbalism.SubSkill.GreenThumb.Description.2=이끼 낀 블록 만들기, 잔디 자라게 하기 -Herbalism.SubSkill.FarmersDiet.Name=농부의 다이어트 -Herbalism.SubSkill.FarmersDiet.Description=농작물 배고품 회복 향상 -Herbalism.SubSkill.DoubleDrops.Name=2배 드롭 (모든 작물) -Herbalism.SubSkill.DoubleDrops.Description=항상 드롭 2배 -Herbalism.SubSkill.HylianLuck.Name=하이랄인의 행운 -Herbalism.SubSkill.HylianLuck.Description=적은 확률로 레어 아이템 얻음 -Herbalism.SubSkill.ShroomThumb.Name=버섯재배자의 숨결 -Herbalism.SubSkill.ShroomThumb.Description=흙 & 잔디에 균사체 살포 -Herbalism.HylianLuck=&a하이랄의 행운이 오늘 너에게 따르는구나! -Herbalism.Listener=약초학(HERBALISM): +Fishing.Chance.Raining=&9 비 보너스 +Fishing.Listener=낚시: +Fishing.Ability.TH.MagicFound=&7이 잡은 물고기에는 마법의 손길을 느낍니다... +Fishing.Ability.TH.Boom=&7폭발 시간!!! +Fishing.Ability.TH.Poison=&7뭔가 이상한 냄새가 납니다... +Fishing.SkillName=낚시 +#약초학 +Herbalism.Ability.GTe.NeedMore=더 많은 씨앗이 필요합니다. 녹색 에너지를 전파하기 위해서입니다. +Herbalism.Ability.GTh.Fail=**식물재배 실패** +Herbalism.Ability.GTh=&a**식물재배** +Herbalism.Ability.Lower=&7국지를 낮춥니다. +Herbalism.Ability.Ready=&3국지를 &6준비&3합니다. +Herbalism.Ability.ShroomThumb.Fail=**버섯썸 실패** +Herbalism.SubSkill.GreenTerra.Name=그린 테라 +Herbalism.SubSkill.GreenTerra.Description=테라를 전파하며, 드롭 3배 증가, 식물재배 강화 +Herbalism.SubSkill.GreenTerra.Stat=그린 테라 지속시간 +Herbalism.SubSkill.GreenThumb.Name=식물재배 +Herbalism.SubSkill.GreenThumb.Description=국지로 작물 수확 시 자동으로 심기 +Herbalism.SubSkill.GreenThumb.Stat=식물재배 확률 +Herbalism.SubSkill.GreenThumb.Stat.Extra=식물재배 단계: &a작물 단계 {0}에서 자랍니다 +Herbalism.Effect.4=식물재배 (블록) +Herbalism.SubSkill.GreenThumb.Description.2=벽돌에 이끼를 더하거나 풀을 자라게 합니다 +Herbalism.SubSkill.FarmersDiet.Name=농부의 식단 +Herbalism.SubSkill.FarmersDiet.Description=농산물로부터 회복되는 허기가 향상됩니다 +Herbalism.SubSkill.FarmersDiet.Stat=농부의 식단: &a등급 {0} +Herbalism.SubSkill.DoubleDrops.Name=더블 드롭 +Herbalism.SubSkill.DoubleDrops.Description=보통 드롭의 2배 획득 +Herbalism.SubSkill.DoubleDrops.Stat=더블 드롭 확률 +Herbalism.SubSkill.HylianLuck.Name=하이랄의 행운 +Herbalism.SubSkill.HylianLuck.Description=희귀 아이템을 찾을 작은 확률 제공 +Herbalism.SubSkill.HylianLuck.Stat=하이랄의 행운 확률 +Herbalism.SubSkill.ShroomThumb.Name=버섯썸 +Herbalism.SubSkill.ShroomThumb.Description=흙과 풀에 균사체를 전파합니다 +Herbalism.SubSkill.ShroomThumb.Stat=버섯썸 확률 +Herbalism.HylianLuck=&a하이라루의 행운이 오늘은 함께 합니다! +Herbalism.Listener=약초학: Herbalism.SkillName=약초학 -Herbalism.Skills.GTe.Off=**재배의 대지 비활성화됨** -Herbalism.Skills.GTe.On=&a**재배의 대지 활성화됨** -Herbalism.Skills.GTe.Refresh=&a당신의 &e재배의 대지 &a기술은 이제 사용 가능합니다! -Herbalism.Skills.GTe.Other.Off={0}&2님은 &c재배의 대지를 사용했습니다! -Herbalism.Skills.GTe.Other.On=&a{0}&2님은 &c재배의 대지를 사용했습니다! -Herbalism.Skillup=약초학 기술이 {0} 올라 총 {1} 레벨이 되었습니다 -Herbalism.SubSkill.GreenTerra.Stat=재배의 대지 지속 시간 -Herbalism.SubSkill.GreenThumb.Stat=재배의 재능 확률 -Herbalism.SubSkill.GreenThumb.Stat.Extra=재배의 재능 단계: &a작물이 {0} 단계로 자라납니다 -Herbalism.SubSkill.FarmersDiet.Stat=농부의 다이어트: &a랭크 {0} -Herbalism.SubSkill.DoubleDrops.Stat=2배 드롭 확률 -Herbalism.SubSkill.HylianLuck.Stat=하이랄인의 행운 확률 -Herbalism.SubSkill.ShroomThumb.Stat=버섯재배자의 숨결 확률 +Herbalism.Skills.GTe.Off=**그린 테라가 종료되었습니다** +Herbalism.Skills.GTe.On=&a**그린 테라 활성화됨** +Herbalism.Skills.GTe.Refresh=&a그린 테라 능력이 갱신되었습니다! +Herbalism.Skills.GTe.Other.Off=&e{0}&a님의 그린 테라가 종료되었습니다 +Herbalism.Skills.GTe.Other.On=&a{0}&2님이 &c그린 테라를 사용했습니다! +#채광 +Mining.Ability.Locked.0={0}+ 스킬이 필요합니다 (폭탄 채굴) +Mining.Ability.Locked.1={0}+ 스킬이 필요합니다 (더 큰 폭탄) +Mining.Ability.Locked.2={0}+ 스킬이 필요합니다 (폭파 전문가) +Mining.Ability.Lower=&7곡괭이를 낮춥니다. +Mining.Ability.Ready=&3곡괭이를 &6준비&3합니다. +Mining.SubSkill.SuperBreaker.Name=슈퍼 브레이커 +Mining.SubSkill.SuperBreaker.Description=속도+, 드롭 3배 확률 +Mining.SubSkill.SuperBreaker.Stat=슈퍼 브레이커 지속시간 +Mining.SubSkill.DoubleDrops.Name=더블 드롭 +Mining.SubSkill.DoubleDrops.Description=보통 드롭의 2배 획득 +Mining.SubSkill.DoubleDrops.Stat=더블 드롭 확률 +Mining.SubSkill.BlastMining.Name=폭탄 채굴 +Mining.SubSkill.BlastMining.Description=TNT로 채굴 시 보너스 효과 +Mining.SubSkill.BlastMining.Stat=폭탄 채굴: &a등급 {0}/{1} &7({2}) +Mining.SubSkill.BlastMining.Stat.Extra=폭발 반경 증가: &a+{0} +Mining.SubSkill.BiggerBombs.Name=더 큰 폭탄 +Mining.SubSkill.BiggerBombs.Description=TNT 폭발 반경 증가 +Mining.SubSkill.DemolitionsExpertise.Name=폭파 전문가 +Mining.SubSkill.DemolitionsExpertise.Description=TNT 폭발로 인한 피해 감소 +Mining.SubSkill.DemolitionsExpertise.Stat=폭파 전문가 피해 감소 -#MINING -Mining.Ability.Length=파괴자 지속시간: &e{0}s -Mining.Ability.Locked.0={0}레벨 때 기술 해제 (폭발 채굴) -Mining.Ability.Locked.1={0}레벨 때 기술 해제 (거대 폭발) -Mining.Ability.Locked.2={0}레벨 때 기술 해제 (전문 폭파) -Mining.Ability.Lower=&7**곡괭이 준비 해제** -Mining.Ability.Ready=&a**곡괭이 준비 완료** -Mining.SubSkill.SuperBreaker.Name=파괴자 (능력) -Mining.SubSkill.SuperBreaker.Description=속도 향상, 드롭 확률 3배 -Mining.SubSkill.DoubleDrops.Name=드롭 2배 -Mining.SubSkill.DoubleDrops.Description=항상 드롭 2배 -Mining.SubSkill.BlastMining.Name=폭발 채굴 -Mining.SubSkill.BlastMining.Description=TNT로 채굴시 추가 광물 -Mining.SubSkill.BiggerBombs.Name=거대 폭발 -Mining.SubSkill.BiggerBombs.Description=TNT 폭발거리 증가 -Mining.SubSkill.DemolitionsExpertise.Name=전문 폭파 -Mining.SubSkill.DemolitionsExpertise.Description=TNT 폭발 피해 감소 -Mining.Effect.Decrease=전문 폭파 피해 감소: &e{0} -Mining.Effect.DropChance=드롭 2배 확률: &e{0} -Mining.Listener=채광(MINING): +Mining.Listener=Mining: Mining.SkillName=채광 -Mining.Skills.SuperBreaker.Off=**파괴자 발동 해제** -Mining.Skills.SuperBreaker.On=&a**파괴자 발동** -Mining.Skills.SuperBreaker.Other.Off={0}&2님은 &c파괴자를 사용했습니다! -Mining.Skills.SuperBreaker.Other.On=&a{0}&2님은 &c파괴자를 사용했습니다! -Mining.Skills.SuperBreaker.Refresh=&a당신의 &e파괴자는 &a이제 사용 가능합니다! -Mining.Skillup=채광 기술이 {0} 올라 총 {1} 레벨이 되었습니다 -Mining.SubSkill.SuperBreaker.Stat=파괴자 지속 시간 -Mining.SubSkill.DoubleDrops.Stat=드롭 2배 확률 -Mining.SubSkill.BlastMining.Stat=폭발 채광: &a랭크 {0}/{1} &7({2}) -Mining.SubSkill.BlastMining.Stat.Extra=폭발 범위 증가: &a+{0} -Mining.SubSkill.DemolitionsExpertise.Stat=전문 폭파 피해 감소 - -#폭발 채굴 -Mining.Blast.Boom=&7**폭발** -Mining.Blast.Effect=+{0} 광물 이익, {1}x 드롭 -Mining.Blast.Radius.Increase=폭발 반경 증가: &e+{0} -Mining.Blast.Rank=폭발 채굴: &e{0}/8랭크 &7({1}) -Mining.Blast.Other.On=&a{0}&2님은 &c폭발 채굴을 사용하셨습니다! -Mining.Blast.Refresh=&a당신의 &e폭발 채굴 &a기술은 이제 사용 가능합니다! - +Mining.Skills.SuperBreaker.Off=**슈퍼 브레이커가 사라졌습니다** +Mining.Skills.SuperBreaker.On=&a**슈퍼 브레이커가 활성화되었습니다** +Mining.Skills.SuperBreaker.Other.Off=슈퍼 브레이커가 &e{0}님에게서 사라졌습니다 +Mining.Skills.SuperBreaker.Other.On=&a{0}&2님이 &c슈퍼 브레이커를 사용하였습니다! +Mining.Skills.SuperBreaker.Refresh=&a당신의 &e슈퍼 브레이커&a 능력이 갱신되었습니다! +Mining.Listener=Mining: +Mining.SkillName=채광 +Mining.Skills.SuperBreaker.Off=슈퍼 브레이커가 사라졌습니다 +Mining.Skills.SuperBreaker.On=&a슈퍼 브레이커가 활성화되었습니다 +Mining.Skills.SuperBreaker.Other.Off=슈퍼 브레이커가 &e{0}님에게서 사라졌습니다 +Mining.Skills.SuperBreaker.Other.On=&a{0}&2님이 &c슈퍼 브레이커를 사용하였습니다! +Mining.Skills.SuperBreaker.Refresh=&a당신의 &e슈퍼 브레이커&a 능력이 갱신되었습니다! +#Blast Mining +Mining.Blast.Boom=&7펑 +Mining.Blast.Cooldown= +Mining.Blast.Effect=채굴량 +{0}, 드롭 횟수 {1}배 증가 +Mining.Blast.Other.On=&a{0}&2님이 &c폭발 채굴을 사용하였습니다! +Mining.Blast.Refresh=&a폭발 채굴 능력이 갱신되었습니다! #REPAIR Repair.SubSkill.Repair.Name=수리 -Repair.SubSkill.Repair.Description=도구 & 방어구 수리 -Repair.SubSkill.GoldRepair.Name=금 수리 ({0}레벨 이상) -Repair.SubSkill.GoldRepair.Description=금 도구 & 방어구 수리 -Repair.SubSkill.IronRepair.Name=철 수리 ({0}레벨 이상) -Repair.SubSkill.IronRepair.Description=철 도구 & 방어구 수리 -Repair.SubSkill.StoneRepair.Name=돌 수리 ({0}레벨 이상) +Repair.SubSkill.Repair.Description=도구 및 갑옷 수리 +Repair.SubSkill.GoldRepair.Name=금 수리 ({0}+ 레벨) +Repair.SubSkill.GoldRepair.Description=금 도구 및 갑옷 수리 +Repair.SubSkill.IronRepair.Name=철 수리 ({0}+ 레벨) +Repair.SubSkill.IronRepair.Description=철 도구 및 갑옷 수리 +Repair.SubSkill.StoneRepair.Name=돌 수리 ({0}+ 레벨) Repair.SubSkill.StoneRepair.Description=돌 도구 수리 -Repair.SubSkill.RepairMastery.Name=수리 마스터리 -Repair.SubSkill.RepairMastery.Description=수리 양 증가 +Repair.SubSkill.RepairMastery.Name=수리 숙련 +Repair.SubSkill.RepairMastery.Description=수리량 증가 +Repair.SubSkill.RepairMastery.Stat=수리 숙련도: &a추가로 {0} 내구도 복구 Repair.SubSkill.SuperRepair.Name=슈퍼 수리 -Repair.SubSkill.SuperRepair.Description=효율 2배 -Repair.SubSkill.DiamondRepair.Name=다이아몬드 수리 ({0} 레벨) -Repair.SubSkill.DiamondRepair.Description=다이아몬드 도구 & 방어구 수리 -Repair.SubSkill.ArcaneForging.Name=인챈트 아이템 수리 -Repair.SubSkill.ArcaneForging.Description=마법 아이템 수리 -Repair.Error=&4mcMMO이 아이템을 수리하려고 시도하는 동안 오류가 발생했습니다! -Repair.Listener.Anvil=&4당신은 모루를 놓았습니다, 모루는 도구들과 방어구를 수리할 수 있습니다. -Repair.Listener=수리(REPAIR): -Repair.SkillName=수리 -Repair.Skills.AdeptDiamond=&4당신은 아직 다이아몬드를 수리할 수 있는 기술을 배우지 않았습니다. -Repair.Skills.AdeptGold=&4당신은 아직 금을 수리할 수 있는 기술을 배우지 않았습니다. -Repair.Skills.AdeptIron=&4당신은 아직 철을 수리할 수 있는 기술을 배우지 않았습니다. -Repair.Skills.AdeptStone=&4당신은 아직 돌을 수리할 수 있는 기술을 배우지 않았습니다. -Repair.Skills.Adept=당신은 &e{1}을/를 수리할려면 &e{0}&c레벨이 필요합니다 -Repair.Skills.FeltEasy=&7쉬운 느낌~ -Repair.Skills.FullDurability=&7내구도가 꽉 찼습니다. -Repair.Skills.Mastery=수리 마스터리: &e추가 내구성 복구: {0} -Repair.Skills.StackedItems=&4한번에 많은 아이템을 수리할 수 없습니다. -Repair.Skills.Super.Chance=슈퍼 수리 확률: &e{0} -Repair.Skillup=수리 기술이 {0} 올라 총 {1} 레벨이 되었습니다 -Repair.Pretty.Name=수리 -Repair.SubSkill.RepairMastery.Stat=수리 마스터리: &a추가 {0} 내구성 복구 +Repair.SubSkill.SuperRepair.Description=두 배로 효과 증가 Repair.SubSkill.SuperRepair.Stat=슈퍼 수리 확률 -Repair.SubSkill.ArcaneForging.Stat=인챈트 아이템 수리: &e랭크 {0}/{1} -Repair.SubSkill.ArcaneForging.Stat.Extra=&3인챈트 아이템 수리 확률:&7 성공 &a{0}&7%, 실패 &c{1}&7% - +Repair.SubSkill.DiamondRepair.Name=다이아몬드 수리 ({0}+ 레벨) +Repair.SubSkill.DiamondRepair.Description=다이아몬드 도구 및 갑옷 수리 +Repair.SubSkill.ArcaneForging.Name=신비한 대장간 +Repair.SubSkill.ArcaneForging.Description=마법 아이템 수리 +Repair.SubSkill.ArcaneForging.Stat=신비한 대장간: &e등급 {0}/{1} +Repair.SubSkill.ArcaneForging.Stat.Extra=&3신비한 대장간 확률:&7 성공 &a{0}&7%, 실패 &c{1}&7% +Repair.Error=&4mcMMO가 이 아이템을 수리하는 동안 오류가 발생했습니다! +Repair.Listener.Anvil=&4당신은 모루를 배치했습니다. 모루는 도구와 갑옷을 수리할 수 있습니다. +Repair.Listener=수리: +Repair.SkillName=수리 +Repair.Skills.AdeptDiamond=&4다이아몬드 수리에 능숙하지 않습니다. +Repair.Skills.AdeptGold=&4금 수리에 능숙하지 않습니다. +Repair.Skills.AdeptIron=&4철 수리에 능숙하지 않습니다. +Repair.Skills.AdeptStone=&4돌 수리에 능숙하지 않습니다. +Repair.Skills.Adept=&c레벨 &e{0}&c 이상이어야 &e{1}&c을(를) 수리할 수 있습니다. +Repair.Skills.FeltEasy=&7쉽게 느껴졌습니다. +Repair.Skills.FullDurability=&7이 아이템은 완전한 내구도입니다. +Repair.Skills.StackedItems=&4중첩된 아이템은 수리할 수 없습니다. +Repair.Pretty.Name=수리 #Arcane Forging -Repair.Arcane.Chance.Downgrade=&7인챈트 수리 격하 확률: &e{0}% -Repair.Arcane.Chance.Success=&7인챈트 수리 성공 확률: &e{0}% -Repair.Arcane.Downgrade=이 아이템의 인챈트는 감소했습니다. -Repair.Arcane.Fail=이 아이템의 인챈트는 영구적으로 소멸되었습니다. -Repair.Arcane.Lost=당신은 모든 인챈트를 유지할 기술이 충분치 않습니다. -Repair.Arcane.Perfect=&a이 아이템의 인챈트를 지속시켰습니다. -Repair.Arcane.Rank=인챈트 수리: &e{0}/{1}랭크 - +Repair.Arcane.Downgrade=이 아이템의 신비한 힘이 감소되었습니다. +Repair.Arcane.Fail=이 아이템의 신비한 힘이 영구히 사라졌습니다. +Repair.Arcane.Lost=너무 능력이 부족하여 마법을 추출할 수 없었습니다. +Repair.Arcane.Perfect=&a이 아이템의 신비한 힘을 유지하였습니다. #SALVAGE -Salvage.Pretty.Name=회수 -Salvage.SubSkill.AdvancedSalvage.Name=전문적인 회수 -Salvage.SubSkill.AdvancedSalvage.Description=손상된 아이템 회수 -Salvage.Ability.Locked.0={0} 레벨 때 기술해제 (전문적인 회수) -Salvage.Ability.Bonus.0=전문적인 회수 -Salvage.Ability.Bonus.1=부셔진 아이템의 최대 추출량 {0} -Salvage.Arcane.Rank=신비로운 회수: &eRank {0}/{1} -Salvage.Arcane.ExtractFull=&7최대-인챈트 기회 부과 -Salvage.Arcane.ExtractPartial=&7일부-인챈트 기회 부과 -Salvage.Skills.Success=&a아이템 회수됨! -Salvage.Skills.Adept.Damaged=&4손상된 아이템을 회수할 능력이 없습니다. -Salvage.Skills.Adept.Level={1}를 &c회수하려면 &e{0}&c 레벨이 되야합니다 -Salvage.Skills.TooDamaged=&4이 아이템은 심하게 손상되어 회수할 수 없습니다. -Salvage.Skills.ArcaneFailed=당신은 이 아이템 속의 지식을 추출할 수 없습니다. -Salvage.Skills.ArcanePartial=당신은 이 아이템 속의 지식의 일부만 추출할 수 있었습니다. -Salvage.Skills.ArcaneSuccess=&a당신은 이 아이템의 모든 지식을 추출할 수 있습니다! -Salvage.Listener.Anvil=&4당신은 회수 모루를 놓았습니다, 도구나 방어구 회수에 쓰입니다. -Salvage.Listener=회수(SALVAGE): -Salvage.SkillName=회수 -Salvage.Pretty.Name=Salvage +Salvage.Pretty.Name=분해 Salvage.SubSkill.UnderstandingTheArt.Name=예술의 이해 -Salvage.SubSkill.UnderstandingTheArt.Description=이웃의 쓰레기를 뒤지는 것뿐만 아니라 환경을 보호하는 것입니다.\n회수의 다양한 속성을 강화합니다. +Salvage.SubSkill.UnderstandingTheArt.Description=이웃의 쓰레기를 털기 위한 것이 아니라 환경을 보호하기 위한 것입니다.\n분해의 다양한 속성을 강화합니다. Salvage.SubSkill.ScrapCollector.Name=폐기물 수집가 -Salvage.SubSkill.ScrapCollector.Description=아이템에서 재료를 회수하며, 완벽한 회수는 기술과 운에 달려 있습니다. -Salvage.SubSkill.ScrapCollector.Stat=폐기물 수집가: &a최대 &e{0}&a개의 아이템을 회수합니다. 운이 조금 관여됩니다. -Salvage.SubSkill.ArcaneSalvage.Name=신비로운 회수 -Salvage.SubSkill.ArcaneSalvage.Description=아이템에서 인챈트 추출 -Salvage.SubSkill.ArcaneSalvage.Stat=신비로운 회수: &e랭크 {0}/{1} -Salvage.Skills.Lottery.Normal=&6{1}&6에서 &3{0}&6개의 재료를 회수했습니다. -Salvage.Skills.Lottery.Perfect=&a&l완벽합니다!&r&6{1}&6에서 수고 없이 &3{0}&6개의 재료를 회수했습니다. -Salvage.Skills.Lottery.Untrained=&7회수에 적절하게 훈련되지 않았습니다. {1}&7에서는 &c{0}&7개의 재료만 회수할 수 있었습니다. - -#Anvil (Shared between SALVAGE and REPAIR) -Anvil.Unbreakable=이 아이템은 Unbreakable입니다! - -#SWORDS -Swords.Ability.Lower=&7**검 준비 해제** -Swords.Ability.Ready=&a**검 준비 완료** -Swords.Combat.Bleed.Chance=출혈 확률: &e{0} -Swords.Combat.Bleed.Length=출혈 지속시간: &e{0} 틱 -Swords.Combat.Bleed.Note=&7알림: &e1 틱은 2초입니다 -Swords.Combat.Bleeding.Started=&4 당신은 피를 흘리고 있습니다! -Swords.Combat.Bleeding.Stopped=&7출혈이 &a멈췄습니다&7! -Swords.Combat.Bleeding=&a**출혈** -Swords.Combat.Counter.Chance=카운터 어택 확률: &e{0} -Swords.Combat.Counter.Hit=&4카운터 어택에 맞았습니다! -Swords.Combat.Countered=&a**카운터-어택** -Swords.Combat.SS.Struck=&4톱날 공격에 맞았습니다! -Swords.SubSkill.CounterAttack.Name=카운터 어택 -Swords.SubSkill.CounterAttack.Description={0} 피해 반사 -Swords.SubSkill.SerratedStrikes.Name=톱날 공격 (능력) -Swords.SubSkill.SerratedStrikes.Description=피해 {0} 증가, 출혈 증가 -Swords.Effect.4=톱날 공격 출혈 증가 -Swords.Effect.5={0} 틱 출혈 -Swords.SubSkill.Bleed.Name=출혈 -Swords.SubSkill.Bleed.Description=과다 출혈 -Swords.Listener=검술(SWORDS): -Swords.SkillName=검술 -Swords.Skills.SS.Off=**톱날 공격 발동 해제** -Swords.Skills.SS.On=&a**톱날 공격 발동** -Swords.Skills.SS.Refresh=&a당신의 &e톱날 공격 &a스킬은 이제 사용 가능합니다! -Swords.Skills.SS.Other.Off={0}&2님은 &c톱날 공격 스킬을 사용 해제했습니다! -Swords.Skills.SS.Other.On=&a{0}&2님은 &c톱날 공격 스킬을 사용했습니다! -Swords.Skillup=검술 스킬이 {0} 올라 총 {1} 레벨이 되었습니다 -Swords.SS.Length=톱날 공격 지속시간: &e{0}초 -Swords.Combat.Rupture.Note.Update.One=&7(파열 노트): 주기적인 피해는 치명적이지 않으며 초당 두 번 발생하며 방어구를 무시합니다. +Salvage.SubSkill.ScrapCollector.Description=아이템에서 재료를 분해합니다. 완벽한 분해는 기술과 운에 달려있습니다. +Salvage.SubSkill.ScrapCollector.Stat=폐기물 수집가: &a최대 &e{0}&a개의 아이템 분해. 일부 운이 필요합니다. +Salvage.SubSkill.ArcaneSalvage.Name=신비한 분해 +Salvage.SubSkill.ArcaneSalvage.Description=아이템에서 마법을 추출합니다. +Salvage.SubSkill.ArcaneSalvage.Stat=신비한 분해: &e등급 {0}/{1} +Salvage.Ability.Bonus.0=폐기물 수집가 +Salvage.Ability.Bonus.1=최대 &e{0}&a개의 아이템 분해. 일부 운이 필요합니다. +Salvage.Arcane.ExtractFull=&7신비한 분해 전체 마법 추출 확률 +Salvage.Arcane.ExtractPartial=&7신비한 분해 부분적 마법 추출 확률 +Salvage.Skills.Success=&a아이템을 분해하였습니다! +Salvage.Skills.Adept.Damaged=&4손상된 아이템은 분해할 수 없습니다. +Salvage.Skills.Adept.Level=레벨 &e{0}&c 이상이어야 &e{1}&c을(를) 분해할 수 있습니다. +Salvage.Skills.TooDamaged=&4이 아이템은 너무 손상되었어 분해할 수 없습니다. +Salvage.Skills.ArcaneFailed=&c이 아이템에 담긴 지식을 추출하지 못했습니다. +Salvage.Skills.ArcanePartial=&c이 아이템에 담긴 일부 지식만 추출할 수 있었습니다. +Salvage.Skills.ArcaneSuccess=&a이 아이템에 담긴 모든 지식을 추출할 수 있었습니다! +Salvage.Listener.Anvil=&4당신은 분해 모루를 배치했습니다. 도구와 갑옷을 분해하려면 이를 사용하세요. +Salvage.Listener=분해: +Salvage.SkillName=분해 +Salvage.Skills.Lottery.Normal=&6{1}&6에서 &3{0}&6개의 재료를 분해하였습니다. +Salvage.Skills.Lottery.Perfect=&a&l완벽해요!&r&6 &3{1}&6을 쉽게 회수하여 &3{0}&6 자료를 검색했습니다. +Salvage.Skills.Lottery.Untrained=&7인양에 대한 교육을 제대로 받지 못했습니다. &a{1}&7에서 &c{0}&7 자료만 복구할 수 있었습니다. +#모루(SALVAGE와 REPAIR 간에 공유) +Anvil.Unbreakable=이 아이템은 깨지지 않습니다! +#검 +Swords.Ability.Lower=&7검을 내려놓습니다. +Swords.Ability.Ready=&3당신은 &6검을 준비합니다&3. +Swords.Combat.Rupture.Note.Update.One=&7(파열 노트): 주기적인 피해는 매초 두 번 발생하며 방어구를 뚫고 발생합니다. +Swords.Combat.Bleeding.Started=&4출혈 중입니다! +Swords.Combat.Bleeding.Stopped=&7출혈이 &a중단&7되었습니다! +Swords.Combat.Bleeding=&a**적이 출혈 중** +Swords.Combat.Counter.Hit=&4반격으로 공격당했습니다! +Swords.Combat.Countered=&a**반격당함** +Swords.Combat.SS.Struck=&4톱니 모양의 타격을 받았습니다! +Swords.SubSkill.CounterAttack.Name=반격 +Swords.SubSkill.CounterAttack.Description=공격당했을 때 일부 피해를 반사합니다! Swords.SubSkill.CounterAttack.Stat=반격 확률 -Swords.SubSkill.SerratedStrikes.Stat=톱날 공격 지속시간 +Swords.SubSkill.SerratedStrikes.Name=톱니 모양 타격 +Swords.SubSkill.SerratedStrikes.Description=일정 범위 내에서 부분적인 피해를 입히며 파열을 적용할 확률이 있습니다! +Swords.SubSkill.SerratedStrikes.Stat=톱니 모양 타격 길이 Swords.SubSkill.Rupture.Name=파열 Swords.SubSkill.Rupture.Description=폭발적으로 끝나는 지속 피해 효과 Swords.SubSkill.Stab.Name=찌르기 Swords.SubSkill.Stab.Description=공격에 추가 피해를 줍니다. Swords.SubSkill.Stab.Stat=찌르기 피해 Swords.SubSkill.SwordsLimitBreak.Name=검 한계 돌파 -Swords.SubSkill.SwordsLimitBreak.Description=한계를 깨는 것. 강력한 상대에 대한 추가 피해. PVP를 위해 설계되었으며, PVE에서 피해 증가 여부는 서버 설정에 따릅니다. +Swords.SubSkill.SwordsLimitBreak.Description=한계 돌파. 강한 상대에 대한 피해 증가. PVP용으로 의도되었으며 PVE에서 피해를 증가시킬 것인지는 서버 설정에 따라 다릅니다. Swords.SubSkill.SwordsLimitBreak.Stat=한계 돌파 최대 피해 Swords.SubSkill.Rupture.Stat=파열 확률 -Swords.SubSkill.Rupture.Stat.Extra=[[DARK_AQUA]]파열 지속 시간: &e{0}초&a (플레이어), &e{1}초&a (몹). -Swords.SubSkill.Rupture.Stat.TickDamage=[[DARK_AQUA]]파열 순수 틱 피해: &e{0}&a (플레이어), &e{1}&a (몹). -Swords.SubSkill.Rupture.Stat.ExplosionDamage=[[DARK_AQUA]]파열 폭발 피해: &e{0}&a (플레이어), &e{1}&a (몹). - -#TAMING +Swords.SubSkill.Rupture.Stat.Extra=[[DARK_AQUA]]파열 지속 시간: &e{0}초&a 플레이어 대상, &e{1}초&a 몹 대상. +Swords.SubSkill.Rupture.Stat.TickDamage=[[DARK_AQUA]]파열 순수 틱 피해: &e{0}&a 플레이어 대상, &e{1}&a 몹 대상. +Swords.SubSkill.Rupture.Stat.ExplosionDamage=[[DARK_AQUA]]파열 폭발 피해: &e{0}&a 플레이어 대상, &e{1}&a 몹 대상 +Swords.Effect.4=톱니 모양 타격 파열+ +Swords.Effect.5={0} 틱 파열 +Swords.Listener=검: +Swords.SkillName=검술 +Swords.Skills.SS.Off=**톱니 모양 타격 효과가 사라졌습니다** +Swords.Skills.SS.On=&a**톱니 모양 타격 활성화됨** +Swords.Skills.SS.Refresh=&a당신의 &e톱니 모양 타격 &a능력이 갱신되었습니다! +Swords.Skills.SS.Other.Off=톱니 모양 타격&a가 &e{0}에게 종료되었습니다 +Swords.Skills.SS.Other.On=&a{0}&2님이 &c톱니 모양 타격을 사용했습니다! +#길들이기 Taming.Ability.Bonus.0=환경 인식 -Taming.Ability.Bonus.1=늑대 위험 회피 +Taming.Ability.Bonus.1=늑대가 위험을 피합니다 Taming.Ability.Bonus.2=두꺼운 털 -Taming.Ability.Bonus.3=1/{0} 피해, 내화성(불저항력) -Taming.Ability.Bonus.4=충격 증명 -Taming.Ability.Bonus.5=항상 1/{0} 폭발 피해 -Taming.Ability.Bonus.6=날카로운 발톱 +Taming.Ability.Bonus.3=1/{0} 피해, 화염 저항 +Taming.Ability.Bonus.4=내구성이 좋음 +Taming.Ability.Bonus.5=폭발물의 일반 피해를 1/{0} 받습니다 +Taming.Ability.Bonus.6=올바르게 갈겨진 발톱 Taming.Ability.Bonus.7=+{0} 피해 -Taming.Ability.Bonus.8=빠른 음식 제공 -Taming.Ability.Bonus.9={0} 확률로 공격시 회복 -Taming.Ability.Bonus.10=신성한 사냥개 -Taming.Ability.Bonus.11=마법이나 독으로 인한 손상 회복 -Taming.Ability.Locked.0={0}레벨 때 스킬해제 (환경 인식) -Taming.Ability.Locked.1={0}레벨 때 스킬해제 (두꺼운 털) -Taming.Ability.Locked.2={0}레벨 때 스킬해제 (충격 증명) -Taming.Ability.Locked.3={0}레벨 때 스킬해제 (날카로운 발톱) -Taming.Ability.Locked.4={0}레벨 때 스킬해제 (빠른 음식 제공) -Taming.Ability.Locked.5={0}레벨 때 스킬해제 (신성한 사냥개) -Taming.Combat.Chance.Gore=돌진 확률: &e{0} -Taming.SubSkill.BeastLore.Name=짐승의 포효 -Taming.SubSkill.BeastLore.Description=뼈로 늑대/오셀롯 검사 -Taming.SubSkill.ShockProof.Name=충격 방지 -Taming.SubSkill.ShockProof.Description=폭발 피해 절감 -Taming.SubSkill.CallOfTheWild.Name=야생의 포효 -Taming.SubSkill.CallOfTheWild.Description=옆에 동물 소환 -Taming.SubSkill.CallOfTheWild.Description.2=&7COTW (오셀롯): 쭈그리면서 물고기를 들고 {0}번 좌 클릭 -Taming.Effect.15=&7COTW (늑대): 쭈그리면서 뼈를 들고 {0}번 좌 클릭 -Taming.SubSkill.Gore.Name0=&7COTW (말): 쭈그리면서 사과를 들고 {0}번 좌 클릭 -Taming.SubSkill.FastFoodService.Name=빠른 음식 제공 -Taming.SubSkill.FastFoodService.Description=공격시 치료 기회 -Taming.SubSkill.HolyHound.Name=신성한 사냥개 -Taming.SubSkill.HolyHound.Description=마법 & 독 피해 치료 -Taming.SubSkill.Gore.Name=돌진 -Taming.SubSkill.Gore.Description=크리티컬 스크라이크 출혈 적용 -Taming.SubSkill.SharpenedClaws.Name=날카로운 발톱 -Taming.SubSkill.SharpenedClaws.Description=추가 피해 +Taming.Ability.Bonus.8=패스트 푸드 서비스 +Taming.Ability.Bonus.9=공격 시 회복 확률 {0} +Taming.Ability.Bonus.10=성스러운 사냥개 +Taming.Ability.Bonus.11=마법 또는 독 피해를 받을 때 체력 회복 +Taming.Ability.Locked.0={0}+ 능력(환경 인식)까지 잠금 해제됨 +Taming.Ability.Locked.1={0}+ 능력(두꺼운 털)까지 잠금 해제됨 +Taming.Ability.Locked.2={0}+ 능력(내구성이 좋음)까지 잠금 해제됨 +Taming.Ability.Locked.3={0}+ 능력(올바르게 갈겨진 발톱)까지 잠금 해제됨 +Taming.Ability.Locked.4={0}+ 능력(패스트 푸드 서비스)까지 잠금 해제됨 +Taming.Ability.Locked.5={0}+ 능력(성스러운 사냥개)까지 잠금 해제됨 +Taming.Combat.Chance.Gore=피 피하기 확률 +Taming.SubSkill.BeastLore.Name=야수 지식 +Taming.SubSkill.BeastLore.Description=늑대와 오셀롯을 검토하면서 골격을 칩니다 +Taming.SubSkill.ShockProof.Name=내구성이 좋음 +Taming.SubSkill.ShockProof.Description=폭발 데미지 감소 +Taming.SubSkill.CallOfTheWild.Name=야생의 부름 +Taming.SubSkill.CallOfTheWild.Description=동물을 당신 곁으로 소환합니다 +Taming.SubSkill.CallOfTheWild.Description.2=&7COTW: 앉아서 왼쪽 버튼을 클릭하면\n {0} {1} (오셀롯), {2} {3} (늑대), {4} {5} (말)이 소환됩니다 +Taming.SubSkill.FastFoodService.Name=패스트 푸드 서비스 +Taming.SubSkill.FastFoodService.Description=늑대가 공격할 때 회복할 확률 +Taming.SubSkill.HolyHound.Name=성스러운 사냥개 +Taming.SubSkill.HolyHound.Description=마법 및 독으로 회복됩니다 +Taming.SubSkill.Gore.Name=피 피하기 +Taming.SubSkill.Gore.Description=파열을 적용하는 크리티컬 스트라이크 +Taming.SubSkill.SharpenedClaws.Name=올바르게 갈겨진 발톱 +Taming.SubSkill.SharpenedClaws.Description=피해 보너스 Taming.SubSkill.EnvironmentallyAware.Name=환경 인식 -Taming.SubSkill.EnvironmentallyAware.Description=선인장/용암 공포증, 낙사 피해 감소 +Taming.SubSkill.EnvironmentallyAware.Description=선인장/용암 공포증, 낙하 피해 면역 Taming.SubSkill.ThickFur.Name=두꺼운 털 -Taming.SubSkill.ThickFur.Description=피해 감소, 내화성(불저항력) -Taming.Listener.Wolf=&8늑대가 당신에게 되돌아감... -Taming.Listener=조련(TAMING): -Taming.SkillName=조련 -Taming.Skillup=조련 스킬이 {0} 올라 총 {1} 레벨이 되었습니다 -Taming.Summon.Complete=&a소환 완료 -Taming.Summon.Fail.Ocelot=당신 근처에 이미 많은 오셀롯이 있어 더는 소환시킬 수 없습니다. -Taming.Summon.Fail.Wolf=당신 근처에 이미 많은 늑대가 있어 더는 소환시킬 수 없습니다. -Taming.Summon.Fail.Horse=당신 근처에 이미 많은 말이 있어 더는 소환시킬 수 없습니다. -Taming.SubSkill.Pummel.Name=퍼멜 -Taming.SubSkill.Pummel.Description=당신의 늑대는 상대방을 넉백시킬 확률이 있습니다. -Taming.SubSkill.Pummel.TargetMessage=늑대에 의해 넉백당했습니다! -Taming.Summon.COTW.Success.WithoutLifespan=&a(Call Of The Wild) &7당신은 &6{0}&7을 소환했습니다. -Taming.Summon.COTW.Success.WithLifespan=&a(Call Of The Wild) &7당신은 &6{0}&7을 소환했으며, 지속 시간은 &6{1}&7초입니다. -Taming.Summon.COTW.Limit=&a(Call Of The Wild) &7동시에 소환할 수 있는 애완동물은 최대 &c{0}마리&7입니다. -Taming.Summon.COTW.TimeExpired=&a(Call Of The Wild) &7시간이 지났습니다. 당신의 &6{0}&7이(가) 떠납니다. -Taming.Summon.COTW.Removed=&a(Call Of The Wild) &7소환된 &6{0}&7이(가) 이 세상에서 사라졌습니다. -Taming.Summon.COTW.BreedingDisallowed=&a(Call Of The Wild) &c소환된 동물은 번식할 수 없습니다. -Taming.Summon.COTW.NeedMoreItems=&a(Call Of The Wild) &7더 필요한 아이템은 &e{0}&7개의 &3{1}&7입니다. -Taming.Summon.Name.Format=&6(COTW) &f{0}의 {1} - -#UNARMED -Unarmed.Ability.Berserk.Length=버서커 지속시간: &e{0}초 -Unarmed.Ability.Bonus.0=아이언 암 스타일 +Taming.SubSkill.ThickFur.Description=피해 감소, 화염 저항 +Taming.SubSkill.Pummel.Name=때리기 +Taming.SubSkill.Pummel.Description=늑대가 적을 밀치는 확률이 있습니다 +Taming.SubSkill.Pummel.TargetMessage=늑대에 의해 밀렸습니다! +Taming.Listener.Wolf=&8당신의 늑대가 당신에게로 달려옵니다... +Taming.Listener=길들이기: +Taming.SkillName=길들이기 +Taming.Summon.COTW.Success.WithoutLifespan=&a(야생의 부름) &7당신은 &6{0}&7을(를) 소환했습니다 +Taming.Summon.COTW.Success.WithLifespan=&a(야생의 부름) &7당신은 &6{0}&7을(를) 소환했고 지속 시간은 &6{1}&7초입니다. +Taming.Summon.COTW.Limit=&a(야생의 부름) &7동시에 &c{0} &7마리의 &7{1}을(를) 소환할 수 있습니다. +Taming.Summon.COTW.TimeExpired=&a(야생의 부름) &7시간이 지나갔습니다, 당신의 &6{0}&7이(가) 떠납니다. +Taming.Summon.COTW.Removed=&a(야생의 부름) &7당신이 소환한 &6{0}&7이(가) 이 세계에서 사라졌습니다. +Taming.Summon.COTW.BreedingDisallowed=&a(야생의 부름) &c소환된 동물을 번식할 수 없습니다. +Taming.Summon.COTW.NeedMoreItems=&a(야생의 부름) &7더 많은 &e{0}&7개의 &3{1}&7이(가) 필요합니다 +Taming.Summon.Name.Format=&6(야생의 부름) &f{0}의 {1} +#비무장 +Unarmed.Ability.Bonus.0=강철 팔 스타일 Unarmed.Ability.Bonus.1=+{0} 피해 업그레이드 -Unarmed.Ability.Chance.ArrowDeflect=화살 회피 확률: &e{0} -Unarmed.Ability.Chance.Disarm=비무장 확률: &e{0} -Unarmed.Ability.Chance.IronGrip=강철 주먹 확률: &e{0} -Unarmed.Ability.IronGrip.Attacker=상대는 강철 주먹을 가지고 있습니다! -Unarmed.Ability.IronGrip.Defender=&a강철 주먹의 비무장을 일시적으로 방어했습니다! -Unarmed.Ability.Lower=&7**손 준비 해제** -Unarmed.Ability.Ready=&a**손 준비 완료** -Unarmed.SubSkill.Berserk.Name=버서커 (능력) -Unarmed.SubSkill.Berserk.Description=+50% 피해, 약한 광물들을 부숨 -Unarmed.SubSkill.Disarm.Name=비무장 (플레이어) -Unarmed.SubSkill.Disarm.Description=적이 들고있는 아이템 드롭 -Unarmed.SubSkill.IronArmStyle.Name=강철 팔 형태 -Unarmed.SubSkill.IronArmStyle.Description=견고해지는 팔 -Unarmed.SubSkill.ArrowDeflect.Name=화살 회피 -Unarmed.SubSkill.ArrowDeflect.Description=회피 화살 -Unarmed.SubSkill.IronGrip.Name=아이언 그립 -Unarmed.SubSkill.IronGrip.Description=비무장 상태 방지 -Unarmed.Listener=비무장(UNARMED): -Unarmed.SkillName=비무장 -Unarmed.Skills.Berserk.Off=**버서커 발동 해제** -Unarmed.Skills.Berserk.On=&a**버서커 발동** -Unarmed.Skills.Berserk.Other.Off={0}&2님은 &c버서커를 사용했습니다! -Unarmed.Skills.Berserk.Other.On=&a{0}&2님은 &c버서커를 사용합니다! -Unarmed.Skills.Berserk.Refresh=&a당신의 &e버서커 &a스킬은 이제 사용 가능합니다! -Unarmed.Skillup=비무장 스킬이 {0} 올라 총 {1} 레벨이 되었습니다 -Unarmed.SubSkill.Berserk.Stat=버서커 지속시간 -Unarmed.SubSkill.Disarm.Stat=비무장 확률 +Unarmed.Ability.IronGrip.Attacker=상대방이 철제 손잡이를 가지고 있습니다! +Unarmed.Ability.IronGrip.Defender=&a철제 손잡이 덕분에 무장 해제를 막았습니다! +Unarmed.Ability.Lower=&7주먹을 내립니다. +Unarmed.Ability.Ready=&3주먹을 &6준비합니다&3. +Unarmed.SubSkill.Berserk.Name=광폭화 +Unarmed.SubSkill.Berserk.Description=+50% 피해, 약한 물체를 파괴합니다 +Unarmed.SubSkill.Berserk.Stat=광폭화 지속 시간 +Unarmed.SubSkill.Disarm.Name=무장 해제 +Unarmed.SubSkill.Disarm.Description=상대방이 손에 든 아이템을 떨어뜨립니다 +Unarmed.SubSkill.Disarm.Stat=무장 해제 확률 Unarmed.SubSkill.UnarmedLimitBreak.Name=비무장 한계 돌파 -Unarmed.SubSkill.UnarmedLimitBreak.Description=한계를 깨는 것. 강한 상대에 대한 향상된 피해. PVP를 위해 고안되었으며, PVE에서 피해 증가 여부는 서버 설정에 따라 다릅니다. +Unarmed.SubSkill.UnarmedLimitBreak.Description=한계를 돌파합니다. 강력한 상대에 대한 피해가 증가합니다. PVP를 위해 의도되었으며 PVE에서 피해를 증가시킬 것인지는 서버 설정에 따릅니다. Unarmed.SubSkill.UnarmedLimitBreak.Stat=한계 돌파 최대 피해 -Unarmed.SubSkill.SteelArmStyle.Name=강철 팔 형태 -Unarmed.SubSkill.SteelArmStyle.Description=견고해지는 팔 -Unarmed.SubSkill.ArrowDeflect.Stat=화살 회피 확률 -Unarmed.SubSkill.IronGrip.Stat=강철 주먹 확률 +Unarmed.SubSkill.SteelArmStyle.Name=강철 팔 스타일 +Unarmed.SubSkill.SteelArmStyle.Description=시간이 지남에 따라 팔을 강화합니다 +Unarmed.SubSkill.ArrowDeflect.Name=화살 방어 +Unarmed.SubSkill.ArrowDeflect.Description=화살을 튕깁니다 +Unarmed.SubSkill.ArrowDeflect.Stat=화살 방어 확률 +Unarmed.SubSkill.IronGrip.Name=철제 손잡이 +Unarmed.SubSkill.IronGrip.Description=무장 해제되지 않도록 합니다 +Unarmed.SubSkill.IronGrip.Stat=철제 손잡이 확률 Unarmed.SubSkill.BlockCracker.Name=블록 크래커 -Unarmed.SubSkill.BlockCracker.Description=주먹으로 바위 부수기 - -#WOODCUTTING -Woodcutting.Ability.0=나뭇잎 떨어트리기 -Woodcutting.Ability.1=나뭇잎 청소 -Woodcutting.Ability.Chance.DDrop=드롭 2배 확률: &e{0} -Woodcutting.Ability.Length=나무꾼 지속시간: &e{0}초 -Woodcutting.Ability.Locked.0={0}레벨 때 스킬이 해제됩니다 (나뭇잎 떨어트리기) -Woodcutting.SubSkill.TreeFeller.Name=나무꾼 (능력) -Woodcutting.SubSkill.TreeFeller.Description=나무 폭발시키기 -Woodcutting.SubSkill.LeafBlower.Name=나뭇잎 떨어트리기 -Woodcutting.SubSkill.LeafBlower.Description=나뭇잎 청소 -Woodcutting.SubSkill.HarvestLumber.Name=드롭 2배 -Woodcutting.SubSkill.HarvestLumber.Description=항상 드롭 2배 -Woodcutting.Listener=벌목(WOODCUTTING): -Woodcutting.SkillName=벌목 -Woodcutting.Skills.TreeFeller.Off=**나무꾼 발동 해제** -Woodcutting.Skills.TreeFeller.On=&a**나무꾼 발동** -Woodcutting.Skills.TreeFeller.Refresh=&a당신의 &e나무꾼 &a스킬은 이제 사용 가능합니다! -Woodcutting.Skills.TreeFeller.Other.Off={0}&2님은 &c나무꾼 스킬을 사용 해제했습니다! -Woodcutting.Skills.TreeFeller.Other.On=&a{0}&2님은 &c나무꾼 스킬을 사용했습니다! -Woodcutting.Skills.TreeFeller.Splinter=도끼 파편 조각 수집! -Woodcutting.Skills.TreeFeller.Threshold=그 나무는 너무 큽니다! -Woodcutting.Skillup=벌목 스킬이 {0} 올라 총 {1} 레벨이 되었습니다 -Woodcutting.SubSkill.TreeFeller.Stat=나무꾼 지속시간 -Woodcutting.SubSkill.KnockOnWood.Name=나무 두드리기 -Woodcutting.SubSkill.KnockOnWood.Description=나무꾼을 사용할 때 추가 아이템을 찾을 수 있습니다. -Woodcutting.SubSkill.KnockOnWood.Stat=나무 두드리기 -Woodcutting.SubSkill.KnockOnWood.Loot.Normal=나무에서 일반적인 아이템 획득 -Woodcutting.SubSkill.KnockOnWood.Loot.Rank2=나무에서 일반적인 아이템과 경험치 오브 획득 -Woodcutting.SubSkill.HarvestLumber.Stat=드롭 2배 확률 +Unarmed.SubSkill.BlockCracker.Description=주먹으로 바위를 부수세요 +Unarmed.Listener=비무장 전투: +Unarmed.SkillName=비무장 전투 +Unarmed.Skills.Berserk.Off=**광폭화가 사라졌습니다** +Unarmed.Skills.Berserk.On=&a**광폭화가 활성화되었습니다** +Unarmed.Skills.Berserk.Other.Off=광폭화&a가 &e{0}에게 종료되었습니다 +Unarmed.Skills.Berserk.Other.On=&a{0}&2님이 &c광폭화를 사용했습니다! +Unarmed.Skills.Berserk.Refresh=&a당신의 &e광폭화 &a능력이 갱신되었습니다! +#나무꾼 +Woodcutting.Ability.0=잎 블로워 +Woodcutting.Ability.1=잎을 날려 버립니다 +Woodcutting.Ability.Locked.0={0}+ 기술(잎 블로워)까지 잠금 해제됨 +Woodcutting.SubSkill.TreeFeller.Name=나무 패러 +Woodcutting.SubSkill.TreeFeller.Description=나무를 폭발시킵니다 +Woodcutting.SubSkill.TreeFeller.Stat=나무 패러 지속 시간 +Woodcutting.SubSkill.LeafBlower.Name=잎 블로워 +Woodcutting.SubSkill.LeafBlower.Description=잎을 날려 버립니다 +Woodcutting.SubSkill.KnockOnWood.Name=나무를 두드립니다 +Woodcutting.SubSkill.KnockOnWood.Description=나무 패러를 사용할 때 추가 아이템을 찾을 수 있습니다 +Woodcutting.SubSkill.KnockOnWood.Stat=나무를 두드림 +Woodcutting.SubSkill.KnockOnWood.Loot.Normal=나무에서 표준 전리품 +Woodcutting.SubSkill.KnockOnWood.Loot.Rank2=나무에서 표준 전리품과 경험의 오브 +Woodcutting.SubSkill.HarvestLumber.Name=목재 수확 +Woodcutting.SubSkill.HarvestLumber.Description=숙련된 기술로 더 많은 목재를 추출합니다 +Woodcutting.SubSkill.HarvestLumber.Stat=더블 드롭 확률 Woodcutting.SubSkill.Splinter.Name=파편 -Woodcutting.SubSkill.Splinter.Description=나무를 더 효율적으로 베어내세요. +Woodcutting.SubSkill.Splinter.Description=나무를 효율적으로 베어 내세요. Woodcutting.SubSkill.BarkSurgeon.Name=나무 껍질 수술 Woodcutting.SubSkill.BarkSurgeon.Description=나무 껍질을 벗길 때 유용한 재료를 추출하세요. -Woodcutting.SubSkill.NaturesBounty.Name=자연의 풍요 -Woodcutting.SubSkill.NaturesBounty.Description=자연에서 경험치를 모으세요. - -#ABILITIY - -#COMBAT -Combat.ArrowDeflect=&f**화살 회피** -Combat.BeastLore=&a**짐승의 포효** -Combat.BeastLoreHealth=&3체력: (&a{0}&3/{1}) -Combat.BeastLoreOwner=&3주인: (&c{0}&3) -Combat.Gore=&a**돌진** -Combat.StruckByGore=**돌진에 맞았습니다** -Combat.TargetDazed=목표가 &4혼란스러워합니다 -Combat.TouchedFuzzy=&4혼란이 일어났습니다. 아~ 어지러워. +Woodcutting.SubSkill.NaturesBounty.Name=자연의 축복 +Woodcutting.SubSkill.NaturesBounty.Description=자연으로부터 경험치를 얻으세요. +Woodcutting.Listener=나무꾼: +Woodcutting.SkillName=나무꾼 +Woodcutting.Skills.TreeFeller.Off=**나무 패러가 사라졌습니다** +Woodcutting.Skills.TreeFeller.On=&a**나무 패러가 활성화되었습니다** +Woodcutting.Skills.TreeFeller.Refresh=&a당신의 &e나무 패러 &a능력이 갱신되었습니다! +Woodcutting.Skills.TreeFeller.Other.Off=나무 패러&a가 &e{0}에게 종료되었습니다 +Woodcutting.Skills.TreeFeller.Other.On=&a{0}&2님이 &c나무 패러를 사용했습니다! +Woodcutting.Skills.TreeFeller.Splinter=당신의 도끼가 수십 개의 조각으로 깨졌습니다! +Woodcutting.Skills.TreeFeller.Threshold=그 나무는 너무 큽니다! +#전투 +Combat.ArrowDeflect=&f**화살 방어** +Combat.BeastLore=&a**야수 지식** +Combat.BeastLoreHealth=&3체력 (&a{0}&3/{1}) +Combat.BeastLoreOwner=&3주인 (&c{0}&3) Combat.BeastLoreHorseSpeed=&3말 이동 속도 (&a{0} 블록/초&3) Combat.BeastLoreHorseJumpStrength=&3말 점프 강도 (&a최대 {0} 블록&3) +Combat.Gore=&a**찔림** +Combat.StruckByGore=**당신은 찔렸습니다** +Combat.TargetDazed=대상이 &4현기를 잃었습니다 +Combat.TouchedFuzzy=&4털에 닿았습니다. 어지러웠습니다. -#COMMANDS -##generic -mcMMO.Description=mcMMO&3 프로젝트에 대해서:,&6mcMMO는 한 &c오픈 소스&6 RPG 모드로 2011년 2월에 &9nossr50&6님이 만들었습니다. 목표는 질좋은 RPG 경험을 제공하는 것 입니다.,&3팁:,&6 - &c/mcmmo help&a 명령어들을 봅니다,&6 - &a타입 &c/스킬이름&a 자세한 스킬 정보를 봅니다,&3개발자들:,&6 - &anossr50 &9(제작자),&6 - &aGJ &9(프로젝트 주장),&6 - &aNuclearW &9(개발자),&6 - &abm01 &9(개발자),&6 - &aTfT_02 &9(개발자),&6 - &aGlitchfinder &9(개발자),&6 - &at00thpick1 &9(개발자),&3유용한 링크:,&6 - &ahttps://github.com/mcMMO-Dev/mcMMO/issues&6 버그 보고,&6 - &a#mcmmo @ irc.esper.net&6 IRC 채팅, +#커멘드 +##일반적인 +mcMMO.Description=&3&emcMMO&3 프로젝트에 대해:,&6mcMMO는 2011년 2월에 &c오픈 소스&6 RPG 모드로 생성되었습니다,&6nossr50&6에 의해. 목표는 품질 높은 RPG 경험을 제공하는 것입니다.,&3팁:,&6 - &a/mcmmo help&a 명령어를 사용하여 명령어를 확인하세요,&6 - &a/SKILLNAME&a 을(를) 입력하여 자세한 스킬 정보를 확인하세요,&3개발자:,&6 - &anossr50 &9(창조자 및 프로젝트 리드),&6 - &aelectronicboy &9(개발자),&6 - &akashike &9(개발자),&6 - &at00thpick1 &9(클래식 관리자) mcMMO.Description.FormerDevs=&3이전 개발자: &aGJ, NuclearW, bm01, TfT_02, Glitchfinder -Commands.addlevels.AwardAll.1=&a당신은 모든 스킬에 {0} 레벨을 지급했습니다! -Commands.addlevels.AwardAll.2=모든 스킬이 {0}로 변경되었습니다 -Commands.addlevels.AwardSkill.1=&a당신은 {0} 레벨을 {1}에 지급하였습니다! -Commands.addlevels.AwardSkill.2={1} 님은 {0}을/를 수정하였습니다 -Commands.addxp.AwardAll=&a당신은 모든 스킬에 {0} 경험치를 지급했습니다! -Commands.addxp.AwardSkill=&a당신은 {0} 경험치를 {1}에 지급하였습니다! -Commands.Ability.Off=능력 사용이 &c꺼졌습니다 -Commands.Ability.On=능력 사용이 &a켜졌습니다 -Commands.Ability.Toggle=능력 사용은 &e{0}(으)로 전환되었습니다 -Commands.AdminChat.Off=관리자 채팅이 &c꺼졌습니다 -Commands.AdminChat.On=관리자 채팅이 &a켜졌습니다 -Commands.AdminToggle=&a- 관리자 채팅을 켜기/끄기합니다 -Commands.Chat.Console=*시스템* -Commands.Cooldowns.Header=&6--= &amcMMO 능력 재 사용 대기시간&6 =-- -Commands.Cooldowns.Row.N=\ &c{0}&f - &6{1}초 남음 -Commands.Cooldowns.Row.Y=\ &b{0}&f - &2준비! -Commands.Database.Cooldown=이 명령어를 다시 치기전에 1초를 기달려야만 합니다. -Commands.Database.Processing=당신의 이전 명령어가 여전히 실행 중입니다. 기다려주세요. -Commands.Database.CooldownMS=이 명령어를 다시 사용하기 전에 {0} 밀리초를 기다려야 합니다. -Commands.Disabled=이 명령어는 비활성화 되있습니다. -Commands.DoesNotExist= &c플레이어가 데이터베이스에 존재하지 않습니다! -Commands.AdminChatSpy.Enabled=mcMMO 파티 채팅 감시가 활성화되었습니다. -Commands.AdminChatSpy.Disabled=mcMMO 파티 채팅 감시가 비활성화되었습니다. -Commands.AdminChatSpy.Toggle=mcMMO 파티 채팅 감시가 &e{0}&6님에 의해 토글되었습니다. -Commands.AdminChatSpy.Chat=&6[감시: &a{0}&6] &f{1} -Commands.GodMode.Disabled=mcMMO 불사신 모드 비활성화 -Commands.GodMode.Enabled=mcMMO 불사신 모드 활성화 -Commands.GodMode.Forbidden=[mcMMO] 이 월드에서 불사신 모드는 허용 금지입니다 (펄미션을 확인하세요) -Commands.GodMode.Toggle=불사신 모드는 &e{0}&f(으)로 전환되었습니다 -Commands.Healthbars.Changed.HEARTS=[mcMMO] 당신의 체력바 보기 방식은 &c하트&f로 변경되었습니다. -Commands.Healthbars.Changed.BAR=[mcMMO] 당신의 체력바 보기 방식은 &e박스&f로 변경되었습니다. -Commands.Healthbars.Changed.DISABLED=[mcMMO] 당신의 몹 체력바는 &7비활성화&f 되었습니다. -Commands.Healthbars.Invalid=잘못된 체력바 타입! -Commands.Inspect=<플레이어> &a- 상세한 플레이어 정보를 봅니다 -Commands.Invite.Success=&a초대를 성공적으로 보냈습니다. -Commands.Leaderboards=<스킬> <페이지> &a- mcMMO 스킬 정보 -Commands.mcc.Header=---[]&amcMMO 명령어&c[]--- -Commands.mcgod=&a- 불사신 모드 켜기/끄기 -Commands.mchud.Invalid=HUD 타입이 올바르지 않습니다. -Commands.mcpurge.Success=&a데이터베이스가 성공적으로 초기화됬습니다! +Commands.addlevels.AwardAll.1=&a모든 스킬에 {0} 레벨이 수여되었습니다! +Commands.addlevels.AwardAll.2=모든 스킬에 {0}이(가) 수정되었습니다. +Commands.addlevels.AwardSkill.1=&a{1} 스킬에서 {0} 레벨이 수여되었습니다! +Commands.addlevels.AwardSkill.2={1}에서 {0}이(가) 수정되었습니다. +Commands.addxp.AwardAll=&a모든 스킬에 {0} 경험이 주어졌습니다! +Commands.addxp.AwardSkill=&a{1} 스킬에 {0} 경험이 주어졌습니다! +Commands.Ability.Off=능력 사용이 &c비활성화&f되었습니다 +Commands.Ability.On=능력 사용이 &a활성화&f되었습니다 +Commands.Ability.Toggle=능력 사용이 &e{0}&f에 대해 전환되었습니다 +Commands.AdminChat.Off=어드민 채팅 전용 &c비활성화&f됨 +Commands.AdminChat.On=어드민 채팅 전용 &a활성화&f됨 +Commands.AdminToggle=&a- 어드민 채팅 전환 +Commands.Chat.Console=*콘솔* +Commands.Cooldowns.Header=&6--= &amcMMO 능력 쿨다운&6 =-- +Commands.Cooldowns.Row.N=\ &c{0}&f - &6남은 시간: {1} 초 +Commands.Cooldowns.Row.Y=\ &b{0}&f - &2준비 완료! +Commands.Database.CooldownMS=이 명령을 다시 사용하기 전에 {0} 밀리초를 기다려야 합니다. +Commands.Database.Cooldown=이 명령을 다시 사용하기 전에 {0} 초를 기다려야 합니다. +Commands.Database.Processing=이전 명령이 아직 처리 중입니다. 기다려 주십시오. +Commands.Disabled=이 명령은 비활성화되었습니다. +Commands.DoesNotExist= &c플레이어가 데이터베이스에 없습니다! +Commands.GodMode.Disabled=mcMMO 갓 모드 비활성화됨 +Commands.GodMode.Enabled=mcMMO 갓 모드 활성화됨 +Commands.AdminChatSpy.Enabled=mcMMO 파티 채팅 감시 활성화됨 +Commands.AdminChatSpy.Disabled=mcMMO 파티 채팅 감시 비활성화됨 +Commands.AdminChatSpy.Toggle=mcMMO 파티 채팅이 &e{0}&f에 대해 전환되었습니다 +Commands.AdminChatSpy.Chat=&6[SPY: &a{0}&6] &f{1} +Commands.GodMode.Forbidden=[mcMMO] 이 월드에서 갓 모드가 허용되지 않습니다 (권한 참조) +Commands.GodMode.Toggle=갓 모드가 &e{0}&f에 대해 전환되었습니다 +Commands.Healthbars.Changed.HEARTS=[mcMMO] 건강 막대 표시 유형이 &c하트&f로 변경되었습니다. +Commands.Healthbars.Changed.BAR=[mcMMO] 건강 막대 표시 유형이 &e상자&f로 변경되었습니다. +Commands.Healthbars.Changed.DISABLED=[mcMMO] 몹의 건강 막대가 &7비활성화&f되었습니다. +Commands.Healthbars.Invalid=유효하지 않은 건강 막대 유형입니다! +Commands.Inspect=<플레이어> &a- 자세한 플레이어 정보 보기 +Commands.Invite.Success=&a초대가 성공적으로 전송되었습니다. +Commands.Leaderboards=<스킬> <페이지> &a- 리더보드 +Commands.mcgod=&a- 갓 모드 전환 +Commands.mchud.Invalid=유효하지 않은 HUD 유형입니다. +Commands.mcpurge.Success=&a데이터베이스가 성공적으로 정리되었습니다! Commands.mcrank.Heading=&6-=개인 순위=- -Commands.mcrank.Overall=종합&a - &6랭크 &f#&a{0} -Commands.mcrank.Player=타겟: &f{0} -Commands.mcrank.Skill={0}&a - &6랭크 &f#&a{1} -Commands.mcrank.Unranked=&f랭크없음 -Commands.mcrefresh.Success={0}의 쿨다운이 초기화되었습니다. -Commands.mcremove.Success=&a{0}님의 데이터베이스가 성공적으로 삭제되었습니다! -Commands.mctop.Tip=&6팁: &c/mcrank&6 명령어를 사용하면 모든 개인 순위를 볼수 있습니다! -Commands.mmoedit=[플레이어] <스킬> <새값> &a - 대상을 수정합니다 -Commands.mmoedit.AllSkills.1=&a당신의 모든 스킬 레벨이 {0}로 설정되었습니다! -Commands.mmoedit.Modified.1=&a당신의 {0} 레벨이 {1}로 설정되었습니다! -Commands.mmoedit.Modified.2={0}님은 {1}를 수정했습니다. -Commands.mcconvert.Database.Same=당신은 이미 {0} 데이터베이스를 사용중입니다! -Commands.mcconvert.Database.InvalidType={0} 은/는 잘못된 데이터베이스 타입입니다. -Commands.mcconvert.Database.Start=&7{0}에서 {1}(으)로 전환 시작중... -Commands.mcconvert.Database.Finish=&7데이터베이스 이동 완료; {1} 데이터베이스는 이제 {0} 데이터베이스로부터 모든 자료를 가집니다. -Commands.mmoshowdb=현재 사용하는 데이터베이스: &a{0} -Commands.mcconvert.Experience.Invalid=잘못된 공식 타입! 올바른 타입: &aLINEAR &c그리고 &aEXPONENTIAL. -Commands.mcconvert.Experience.Same=이미 {0} 공식을 사용중입니다 -Commands.mcconvert.Experience.Start=&7{0} 에서 {1} 곡선으로 변환 시작 -Commands.mcconvert.Experience.Finish=&7공식 변환 완료; 이제 {0} XP 곡선입니다. -Commands.ModDescription=&a- 플러그인에 대한 정보를 봅니다 -Commands.NoConsole=이 명령어는 콘솔에서의 사용을 지원하지 않습니다. -Commands.Notifications.Off=능력 알림이 &c켜졌습니다 -Commands.Notifications.On=능력 알림이 &a꺼졌습니다 -Commands.Offline=이 명령어는 오프라인 플레이어에게 동작하지 않습니다. -Commands.NotLoaded=플레이어 프로파일을 아직 불러오지 못했습니다. -Commands.Other=---[]&a기타 명령어&c[]--- -Commands.Party.Header=-----[]&a파티&c[]----- -Commands.Party.Features.Header=-----[]&a특징&c[]----- +Commands.mcrank.Overall=종합&a - &6순위 &f#&a{0} +Commands.mcrank.Player=&e{0}에 대한 순위 +Commands.mcrank.Skill=&e{0}&a - &6순위 &f#&a{1} +Commands.mcrank.Unranked=&f순위 없음 +Commands.mcrefresh.Success={0}의 쿨다운이 새로 고쳐졌습니다. +Commands.mcremove.Success=&a{0}가 데이터베이스에서 성공적으로 제거되었습니다! +Commands.mctop.Tip=&6팁: 모든 개인 순위를 보려면 &c/mcrank&6을(를) 사용하세요! +Commands.mmoedit=[플레이어] <스킬> <새값> &a - 대상 수정 +Commands.mmoedit.AllSkills.1=모든 스킬의 레벨이 {0}(으)로 설정되었습니다! +Commands.mmoedit.Modified.1={0}의 레벨이 {1}(으)로 설정되었습니다! +Commands.mmoedit.Modified.2={0}이(가) {1}로 수정되었습니다. +Commands.mcconvert.Database.Same=이미 {0} 데이터베이스를 사용 중입니다! +Commands.mcconvert.Database.InvalidType={0}은(는) 유효하지 않은 데이터베이스 유형입니다. +Commands.mcconvert.Database.Start=&7{0}에서 {1}(으)로 변환 시작 중... +Commands.mcconvert.Database.Finish=&7데이터베이스 마이그레이션이 완료되었습니다; {1} 데이터베이스에는 이제 {0} 데이터베이스의 모든 데이터가 포함되어 있습니다. +Commands.mmoshowdb=현재 사용 중인 데이터베이스는 &a{0}&f입니다 +Commands.mcconvert.Experience.Invalid=알 수 없는 공식 유형! 유효한 유형은 다음과 같습니다: &aLINEAR 및 &aEXPONENTIAL. +Commands.mcconvert.Experience.Same=이미 {0} 공식 유형을 사용 중입니다 +Commands.mcconvert.Experience.Start=&7{0}에서 {1} 곡선으로 변환 시작 중 +Commands.mcconvert.Experience.Finish=&7공식 변환 완료; 이제 {0} 경험 곡선을 사용합니다. +Commands.ModDescription=&a- 간단한 모드 설명 읽기 +Commands.NoConsole=이 명령은 콘솔 사용을 지원하지 않습니다. +Commands.Notifications.Off=능력 알림이 &c비활성화&f되었습니다 +Commands.Notifications.On=능력 알림이 &a활성화&f되었습니다 +Commands.Offline=이 명령은 오프라인 플레이어에게 작동하지 않습니다. +Commands.NotLoaded=플레이어 프로필이 아직 로드되지 않았습니다. Commands.Party.Status=&8이름: &f{0} {1} &8레벨: &3{2} Commands.Party.Status.Alliance=&8동맹: &f{0} -Commands.Party.UnlockedFeatures=&8해제된 특징: &7&o{0} +Commands.Party.UnlockedFeatures=&8잠금 해제된 기능: &7&o{0} Commands.Party.ShareMode=&8공유 모드: Commands.Party.ItemShare=&7아이템 &3({0}) -Commands.Party.ExpShare=&7EXP &3({0}) -Commands.Party.ItemShareCategories=&8공유중인 아이템: &7&o{0} -Commands.Party.MembersNear=&8당신의 근처 &3{0}&8/&3{1} -Commands.Party.Accept=&a- 파티 초대 허용 -Commands.Party.Chat.Off=파티 채팅을 &c끕니다 -Commands.Party.Chat.On=파티 채팅을 &a켭니다 -Commands.Party.Commands=---[]&a파티 명령어&c[]--- -Commands.Party.Invite.0=알림: &a당신은 {1} 님으로부터 {0} 파티 초대에 권유받았습니다 -Commands.Party.Invite.1=타입 &a/party accept&e 명령어를 치면 파티 초대에 승낙됩니다 -Commands.Party.Invite=<플레이어> &a- 파티 초대를 보냅니다 -Commands.Party.Invite.Accepted=&a초대 수락됨. 당신은 {0} 파티에 가입되었습니다 -Commands.Party.Join=참여된 파티: {0} -Commands.Party.Create=&7만들어진 파티: {0} -Commands.Party.Rename=&7변경된 파티 이름: &f{0} +Commands.Party.ExpShare=&7경험치 &3({0}) +Commands.Party.ItemShareCategories=&8아이템 공유: &7&o{0} +Commands.Party.MembersNear=&8근처 플레이어 &3{0}&8/&3{1} +Commands.Party.Accept=&a- 파티 초대 수락 +Commands.Party.Chat.Off=파티 채팅 전용 &c비활성화 +Commands.Party.Chat.On=파티 채팅 전용 &a활성화 +Commands.Party.Commands=&c---[]&a파티 명령어&c[]--- +Commands.Party.Invite.0=&c알림: &a{1}님이 {0}으로부터 파티 초대를 받았습니다. +Commands.Party.Invite.1=&e파티 초대를 수락하려면 &a/party accept&e를 입력하세요. +Commands.Party.Invite=&a- 파티 초대 전송 +Commands.Party.Invite.Accepted=&a초대가 수락되었습니다. {0} 파티에 가입했습니다. +Commands.Party.Join=&7파티에 가입: {0} +Commands.Party.PartyFull=&6{0}&c 파티가 가득 찼습니다! +Commands.Party.PartyFull.Invite=이미 &a{1}&c에 &e{0}&c을(를) 초대했습니다. 이미 &3{2}&c명이 이 파티에 있습니다! +Commands.Party.PartyFull.InviteAccept=&a{0}&c 파티에 가입할 수 없습니다. 이미 &3{1}&c명이 이 파티에 있습니다! +Commands.Party.Create=&7파티 생성: {0} +Commands.Party.Rename=&7파티 이름이 다음으로 변경되었습니다: &f{0} Commands.Party.SetSharing=&7파티 {0} 공유 설정: &3{1} -Commands.Party.ToggleShareCategory=&7파티 아이템 공유 &6{0} &7가 &3{1}되었습니다 -Commands.Party.AlreadyExists=&4{0} 파티은/는 이미 존재합니다! -Commands.Party.Kick=당신은 {0} 파티에서 추방 당하였습니다. -Commands.Party.Leave=파티를 떠났습니다 -Commands.Party.Members.Header=-----[]&a맴버들&c[]----- -Commands.Party.None=당신은 파티에 참여되어 있지 않습니다. -Commands.Party.Quit=&a- 현재 참여 되어있는 파티를 나갑니다 -Commands.Party.Teleport=&a- 파티 맴버한테 텔레포트합니다 -Commands.Party.Toggle=&a- 파티 채팅을 켜기/끄기 합니다 -Commands.Party1=&a- 새 파티를 만듭니다 -Commands.Party2=&a- 플레이어가 파티에 가입합니다 -Commands.Party.Alliance.Header=-----[]&a파티 동맹&c[]----- -Commands.Party.Alliance.Ally=&f{0} &8파티의 동맹: &f{1} -Commands.Party.Alliance.Members.Header=-----[]&a동맹 구성원&c[]----- -Commands.Party.Alliance.Invite.0=알림: &a{1} 파티로부터 {0} 파티와의 동맹 초대를 받았습니다 -Commands.Party.Alliance.Invite.1=타입 &a/party alliance accept&e 초대에 수락합니다 -Commands.Party.Alliance.Invite.Accepted=&a동맹 초대 수락됨. -Commands.Party.Alliance.None=당신은 동맹을 가지고 있지 않습니다. -Commands.Party.Alliance.AlreadyAllies=당신의 파티는 이미 동맹을 가지고 있습니다. 관계를 해지하려면 &3/party alliance disband -Commands.Party.Alliance.Help.0=이 파티는 동맹 형태를 가지고 있지 않습니다. 파티장을 초대하세요 -Commands.Party.Alliance.Help.1= 동맹을 하려면 &3/party alliance invite &c. -Commands.ptp.Enabled=파티 텔레포트 &a활성화됨 -Commands.ptp.Disabled=파티 텔레포트 &c비활성화됨 -Commands.ptp.NoRequests=당신은 이 시간에 텔레포트 요청을 하실 수 없습니다 -Commands.ptp.NoWorldPermissions=[mcMMO] 당신은 월드 {0}(으)로 텔레포트할 권한이 없습니다. -Commands.ptp.Request1={0} &a님이 당신에게 텔레포트를 신청했습니다. -Commands.ptp.Request2=&a텔레포트하려면, 타입 &e/ptp accept&a. &c{0}&a초에 요청이 만기됩니다. +Commands.Party.ToggleShareCategory=&7파티 &6{0} &7의 아이템 공유가 &3{1}&7로 변경되었습니다. +Commands.Party.AlreadyExists=&4파티 {0}이(가) 이미 존재합니다! +Commands.Party.Kick=&c{0}&c님이 파티에서 추방되었습니다: &a{1}&c! +Commands.Party.Leave=&e현재 파티를 나갔습니다 +Commands.Party.Members.Header=&c-----[]&a파티 멤버&c[]----- +Commands.Party.None=&c파티에 속해 있지 않습니다. +Commands.Party.Quit=&a- 현재 파티 나가기 +Commands.Party.Teleport=&a- 파티 멤버로 이동 +Commands.Party.Toggle=&a- 파티 채팅 전환 +Commands.Party1=&a- 새로운 파티 생성 +Commands.Party2=&a- 플레이어의 파티 가입 +Commands.Party.Alliance.Header=&c-----[]&a파티 동맹&c[]----- +Commands.Party.Alliance.Ally=&f{0} &8동맹: &f{1} +Commands.Party.Alliance.Members.Header=&c-----[]&a동맹 멤버&c[]----- +Commands.Party.Alliance.Invite.0=알림: &a{1}님이 {0}으로부터 파티 동맹 초대를 받았습니다. +Commands.Party.Alliance.Invite.1=파티 동맹 초대를 수락하려면 &a/party alliance accept&e를 입력하세요. +Commands.Party.Alliance.Invite.Accepted=&a파티 동맹 초대가 수락되었습니다. +Commands.Party.Alliance.None=&c파티에 동맹이 없습니다. +Commands.Party.Alliance.AlreadyAllies=&c파티에 이미 동맹이 있습니다. &3/party alliance disband&c로 해체하세요. +Commands.Party.Alliance.Help.0=&c이 파티는 아직 동맹을 맺지 않았습니다. 파티 리더를 초대하여 +Commands.Party.Alliance.Help.1=&c 동맹을 맺으세요. &3/party alliance invite <플레이어>&c. +Commands.ptp.Enabled=파티 텔레포팅 &a활성화됨 +Commands.ptp.Disabled=파티 텔레포팅 &c비활성화됨 +Commands.ptp.NoRequests=&c현재 텔레포트 요청이 없습니다. +Commands.ptp.NoWorldPermissions=&c[mcMMO] 월드 {0}로 텔레포트할 권한이 없습니다. +Commands.ptp.Request1=&e{0} &a님이 당신에게 텔레포트를 요청했습니다. +Commands.ptp.Request2=&a텔레포트하려면 &e/ptp accept&a를 입력하세요. 요청은 &c{0} &a초 후 만료됩니다. Commands.ptp.AcceptAny.Enabled=파티 텔레포트 요청 확인 &a활성화됨 Commands.ptp.AcceptAny.Disabled=파티 텔레포트 요청 확인 &c비활성화됨 -Commands.ptp.RequestExpired=파티 텔레포트 요청이 만기됨! -Commands.PowerLevel.Leaderboard=--mcMMO&9 총 레벨 &e점수표-- -Commands.PowerLevel.Capped=&4총 레벨: &a{0} &4최대 레벨: &e{1} -Commands.PowerLevel=&4총 레벨: &a{0} -Commands.Reset.All=&a당신의 모든 스킬이 성공적으로 초기화되었습니다. -Commands.Reset.Single=&a당신의 {0} 스킬이 성공적으로 초기화되었습니다. -Commands.Reset=&a스킬 레벨을 0으로 초기화 시킵니다 -Commands.Scoreboard.Clear=&3mcMMO 점수판 청소됨. -Commands.Scoreboard.NoBoard=mcMMO 점수판이 활성화 되어있지 않음. -Commands.Scoreboard.Keep=&3mcMMO 점수판은 당신이 &a/mcscoreboard clear&3를 사용할 때까지 유지될 것임. -Commands.Scoreboard.Timer=&3mcMMO 점수판은 지금으로부터 &6{0}&3초 내에 청소될 예정임. -Commands.Scoreboard.Help.0=&6 == &c/mcscoreboard &a도움말&6 == -Commands.Scoreboard.Help.1=&3/mcscoreboard&b clear &f - McMMO 점수판을 청소함 -Commands.Scoreboard.Help.2=&3/mcscoreboard&b keep &f - McMMO 점수판을 유지함 -Commands.Scoreboard.Help.3=&3/mcscoreboard&b time [n] &f - McMMO 점수판을 &dn&f초 후에 청소함 -Commands.Scoreboard.Tip.Keep=&6팁: &c/mcscoreboard keep&6 점수판을 보이게 항상 유지. -Commands.Scoreboard.Tip.Clear=&6팁: &c/mcscoreboard clear&6 점수판 감춤. -Commands.Skill.Invalid=잘못된 스킬 이름 입니다! -Commands.Skill.ChildSkill=이 명령어에는 부가 스킬을 사용할 수 없습니다! -Commands.Skill.Leaderboard=--mcMMO &9{0}&e 점수표-- -Commands.SkillInfo=&a- 스킬에 대한 자세한 정보를 봅니다 -Commands.Stats.Self=당신의 통계 -Commands.Stats=&a- 당신의 mcMMO 통계 보기 -Commands.ToggleAbility=&a- 우클릭시 사용되는 스킬들을 켜기/끄기 합니다 -Commands.Usage.0=올바른 사용법 /{0} -Commands.Usage.1=올바른 사용법 /{0} {1} -Commands.Usage.2=올바른 사용법 /{0} {1} {2} -Commands.Usage.3=올바른 사용법 /{0} {1} {2} {3} -Commands.Usage.3.XP=&c올바른 사용법은 /{0} {1} {2} {3}&7입니다 (명령어를 실행한 플레이어에게 알리지 않고 실행하려면 끝에 -s를 포함시킬 수 있습니다) -Commands.Usage.FullClassName=클레스이름 +Commands.ptp.RequestExpired=&c파티 텔레포트 요청이 만료되었습니다! +Commands.PowerLevel.Leaderboard=&e--mcMMO&9 파워 레벨 &e리더보드-- +Commands.PowerLevel.Capped=&4파워 레벨: &a{0} &4최대 레벨: &e{1} +Commands.PowerLevel=&4파워 레벨: &a{0} +Commands.Reset.All=&a모든 스킬 레벨이 성공적으로 재설정되었습니다. +Commands.Reset.Single=&a{0} 스킬 레벨이 성공적으로 재설정되었습니다. +Commands.Reset=&a- 스킬 레벨을 0으로 재설정 +Commands.Scoreboard.Clear=&3mcMMO 스코어보드가 지워졌습니다. +Commands.Scoreboard.NoBoard=&cmcMMO 스코어보드가 활성화되지 않았습니다. +Commands.Scoreboard.Keep=&3mcMMO 스코어보드가 사용 중입니다. &a/mcscoreboard clear&3를 사용하여 제거하세요. +Commands.Scoreboard.Timer=&3mcMMO 스코어보드가 &6{0}&3초 후에 사라집니다. +Commands.Scoreboard.Help.0=&6 == &a/mcscoreboard&6 도움말 == +Commands.Scoreboard.Help.1=&3/mcscoreboard&b clear &f - mcMMO 스코어보드를 지웁니다. +Commands.Scoreboard.Help.2=&3/mcscoreboard&b keep &f - mcMMO 스코어보드를 유지합니다. +Commands.Scoreboard.Help.3=&3/mcscoreboard&b time [n] &f - mcMMO 스코어보드를 &d[n]&f초 후에 지웁니다. +Commands.Scoreboard.Tip.Keep=&6팁: 스코어보드가 표시된 상태에서 &c/mcscoreboard keep&6을 사용하여 보여지도록 유지하세요. +Commands.Scoreboard.Tip.Clear=&6팁: 스코어보드를 제거하려면 &c/mcscoreboard clear&6를 사용하세요. +Commands.XPBar.Reset=&6mcMMO의 XP 바 설정이 재설정되었습니다. +Commands.XPBar.SettingChanged=&6{0}&a의 XP 바 설정이 &a{1}&a(으)로 변경되었습니다. +Commands.Skill.Invalid=유효하지 않은 스킬명입니다! +Commands.Skill.ChildSkill=이 명령어에 대한 하위 스킬은 유효하지 않습니다! +Commands.Skill.Leaderboard=--mcMMO &9{0}&e 리더보드-- +Commands.SkillInfo=&a- 스킬 또는 기능에 대한 자세한 정보 보기 +Commands.Stats=&a- 당신의 mcMMO 스탯 보기 +Commands.ToggleAbility=&a- 우클릭으로 능력 활성화/비활성화 토글 +Commands.Usage.0=&c올바른 사용법은 /{0}입니다. +Commands.Usage.1=&c올바른 사용법은 /{0} {1}입니다. +Commands.Usage.2=&c올바른 사용법은 /{0} {1} {2}입니다. +Commands.Usage.3=&c올바른 사용법은 /{0} {1} {2} {3}입니다. +Commands.Usage.3.XP=&c올바른 사용법은 /{0} {1} {2} {3}&7입니다 (플레이어에게 알리지 않고 명령어를 실행하려면 마지막에 -s를 포함시킵니다). +Commands.Usage.FullClassName=클래스명 Commands.Usage.Level=레벨 -Commands.Usage.Message=메세지 +Commands.Usage.Message=메시지 Commands.Usage.Page=페이지 Commands.Usage.PartyName=이름 Commands.Usage.Password=비밀번호 Commands.Usage.Player=플레이어 -Commands.Usage.Rate=배율 +Commands.Usage.Rate=비율 Commands.Usage.Skill=스킬 -Commands.Usage.SubSkill=부가 스킬 -Commands.Usage.XP=xp -Commands.XPBar.Reset=&6mcMMO의 XP 바 설정이 초기화되었습니다. -Commands.XPBar.SettingChanged=&6{0}&6의 XP 바 설정이 &a{1}&6로 변경되었습니다. -Commands.Description.mmoinfo=스킬 또는 메커니즘에 대한 자세한 정보를 읽습니다. -Commands.MmoInfo.Mystery=&7아직 이 스킬을 해금하지 않았지만, 해금하면 여기에서 자세한 정보를 읽을 수 있습니다! -Commands.MmoInfo.NoMatch=해당 부가 스킬이 존재하지 않습니다! +Commands.Usage.SubSkill=하위 스킬 +Commands.Usage.XP=경험치 +Commands.Description.mmoinfo=스킬 또는 기능에 대한 자세한 정보를 읽으세요. +Commands.MmoInfo.Mystery=&7아직이 스킬을 잠금 해제하지 않았지만, 잠금 해제하면 여기에서 자세한 정보를 읽을 수 있습니다! +Commands.MmoInfo.NoMatch=그런 하위 스킬은 존재하지 않습니다! Commands.MmoInfo.Header=&3-=[]=====[]&6 MMO 정보 &3[]=====[]=- Commands.MmoInfo.SubSkillHeader=&6이름:&e {0} -Commands.MmoInfo.DetailsHeader=&3-=[]=====[]&a 자세한 내용 &3[]=====[]=- -Commands.MmoInfo.OldSkill=&7mcMMO 스킬은 개선된 모듈식 스킬 시스템으로 변환되고 있습니다. 불행히도 이 스킬은 아직 변환되지 않았으며 자세한 통계가 없습니다. 새로운 시스템은 새로운 mcMMO 스킬을 더 빠르게 출시하고 기존 스킬에 대한 유연성을 높일 수 있게 해줍니다. -Commands.MmoInfo.Mechanics=&3-=[]=====[]&6 메커니즘 &3[]=====[]=- +Commands.MmoInfo.DetailsHeader=&3-=[]=====[]&a 상세 정보 &3[]=====[]=- +Commands.MmoInfo.OldSkill=&7mcMMO 스킬은 개선된 모듈식 스킬 시스템으로 변환되고 있으며, 이 스킬은 아직 변환되지 않아 상세한 통계가 없습니다. 새로운 시스템에서는 새로운 mcMMO 스킬의 빠른 출시와 기존 스킬의 유연성이 향상될 것입니다. +Commands.MmoInfo.Mechanics=&3-=[]=====[]&6 기계 &3[]=====[]=- Commands.MmoInfo.Stats=통계: {0} -Commands.Mmodebug.Toggle=mcMMO 디버그 모드가 &6{0}&7되었습니다. 디버그 모드를 활성화하면 지원을 위해 유용한 정보를 얻기 위해 블록을 클릭할 수 있습니다. -mcMMO.NoInvites=이 시간에 당신은 초대하지 못합니다 -mcMMO.NoPermission=&4권한이 부족합니다. -mcMMO.NoSkillNote=&8만약 당신이 스킬을 사용할 수 없다면 여기에 표시되지 않습니다. - - - -##party -Party.Forbidden=[mcMMO] 이 월드에서 파티를 하실 수 없습니다 (펄미션을 확인하세요) -Party.Help.0=올바른 사용법 &3{0} <플레이어> [비밀번호]. -Party.Help.1=파티를 만들려면, &3{0} <이름> [비밀번호]. -Party.Help.2=파티 정보를 볼려면 &3{0} -Party.Help.3=파티에 가입할려면 &3{0} <플레이어> [비밀번호] &c나갈려면 &3{1} -Party.Help.4=파티를 잠금/잠금해제 할려면, &3{0} -Party.Help.5=비밀번호로 파티를 보호할려면, &3{0} <비밀번호> -Party.Help.6=파티에서 플레이어를 추방시킬려면, &3{0} <플레이어> -Party.Help.7=파티장을 교체할려면, &3{0} <플레이어> -Party.Help.8=파티를 해체할려면, &3{0} -Party.Help.9=파티 맴버들과 아이템을 공유하려면 &3{0} -Party.Help.10=파티 맴버들과 경험치 공유를 활성화화려면 &3{0} -Party.InformedOnJoin={0} &a님이 당신의 파티에 참여했습니다 -Party.InformedOnQuit={0} &a님이 당신의 파티에서 떠났습니다 -Party.InformedOnNameChange=&6{0} &a님이 파티 이름을 &f{1}로 설정했습니다 -Party.InvalidName=&4잘못된 파티 이름입니다. -Party.Invite.Self=자기자신을 초대할 수는 없습니다! -Party.IsLocked=이 파티는 이미 잠겨져 있습니다! -Party.IsntLocked=이 파티는 잠겨져 있지 않습니다! -Party.Locked=파티가 잠겼습니다, 오직 파티장만이 초대를 할 수 있습니다. -Party.NotInYourParty=&4{0}님은 당신의 파티에 없습니다 -Party.NotOwner=&4당신은 파티장이 아닙니다. -Party.Target.NotOwner=&4{0}님은 파티장이 아닙니다. -Party.Owner.New=&a{0}님이 새 파티장이 되었습니다. -Party.Owner.NotLeader=&4당신은 이제 파티장이 아닙니다. -Party.Owner.Player =&a당신은 이제 파티장입니다. -Party.Password.None=이 파티는 비밀번호로 보호되고 있습니다. 가입할때 비밀번호를 제공해주세요. -Party.Password.Incorrect=파티 비밀번호가 올바르지 않습니다. -Party.Password.Set=&a설정한 파티 비밀번호는 {0} 입니다 -Party.Password.Removed=&a파티 비밀번호가 청소되었습니다. -Party.Player.Invalid=그 플레이어는 올바르지 않습니다. -Party.NotOnline=&4{0}님은 접속중이 아닙니다! -Party.Player.InSameParty={0}님은 이미 당신의 파티에 있습니다! -Party.PlayerNotInParty=&4{0}님은 파티에 없습니다 -Party.Specify=당신은 파티를 명기해야합니다. -Party.Teleport.Dead=당신은 죽은 플레이어에게로 텔레포트 할 수 없습니다. -Party.Teleport.Hurt=당신은 마지막으로 {0}초에 다쳐 텔레포트 할 수 없습니다. -Party.Teleport.Player=&a당신은 {0}로 텔레포트했습니다. -Party.Teleport.Self=자기자신한테 텔레포트 할 수 없습니다! -Party.Teleport.Target=&a{0}님이 당신에게로 텔레포트했습니다. -Party.Teleport.Disabled={0}님은 파티 텔레포트를 허용하고 있지 않습니다. -Party.Rename.Same=이미 당신의 파티 이름입니다! -Party.Join.Self=자기자신을 가입시킬수 없습니다! -Party.Unlocked=&7파티가 잠금해제 되었습니다 -Party.Disband=&7그 파티가 해체되었습니다 -Party.Alliance.Formed=&7당신의 파티는 이제 &a{0} 파티와 동맹입니다 -Party.Alliance.Disband=&7당신의 파티는 더 이상 &c{0} 파티와 동맹이 아닙니다 -Party.Status.Locked=&4(초대만-허용) -Party.Status.Unlocked=&2(개방) -Party.LevelUp=파티 레벨이 {0} 올라 총 {1} 레벨이 되었습니다 +Commands.Mmodebug.Toggle=mcMMO 디버그 모드가 이제 &6{0}&7입니다. 디버그 모드가 true이면 지원에 사용되는 유용한 정보를 얻기 위해 블록을 두드릴 수 있습니다. +mcMMO.NoInvites=&c현재 초대가 없습니다. +mcMMO.NoPermission=&4권한이 충분하지 않습니다. +mcMMO.NoSkillNote=&8해당 스킬에 액세스 권한이 없으면 여기에 표시되지 않습니다. +##파티 +Party.Forbidden=[mcMMO] 이 월드에서는 파티가 허용되지 않습니다 (권한 확인). +Party.Help.0=&c올바른 사용법은 &3{0} <플레이어> [비밀번호]입니다. +Party.Help.1=&c파티를 생성하려면 &3{0} <이름> [비밀번호]를 사용하세요. +Party.Help.2=&c자세한 정보는 &3{0} &c를 참조하세요. +Party.Help.3=&c가입하려면 &3{0} <플레이어> [비밀번호]&c를 사용하고 나가려면 &3{1} &c를 사용하세요. +Party.Help.4=&c파티를 잠그거나 잠금 해제하려면 &3{0} &c를 사용하세요. +Party.Help.5=&c파티에 비밀번호를 설정하려면 &3{0} <비밀번호>&c를 사용하세요. +Party.Help.6=&c플레이어를 파티에서 추방하려면 &3{0} <플레이어>&c를 사용하세요. +Party.Help.7=&c파티 소유권을 이전하려면 &3{0} <플레이어>&c를 사용하세요. +Party.Help.8=&c파티를 해체하려면 &3{0} &c를 사용하세요. +Party.Help.9=&c파티 멤버와 아이템을 공유하려면 &3{0} &c를 사용하세요. +Party.Help.10=&c파티 멤버와 경험치를 공유하려면 &3{0} &c를 사용하세요. +Party.InformedOnJoin={0} &a님이 당신의 파티에 가입했습니다 +Party.InformedOnQuit={0} &a님이 당신의 파티를 나갔습니다 +Party.InformedOnNameChange=&6{0} &a님이 파티 이름을 &f{1}&a(으)로 설정했습니다 +Party.InvalidName=&4유효하지 않은 파티 이름입니다. +Party.Invite.Self=&c자신을 초대할 수 없습니다! +Party.IsLocked=&c이 파티는 이미 잠겨 있습니다! +Party.IsntLocked=&c이 파티는 잠겨 있지 않습니다! +Party.Locked=&c파티가 잠겨 있어서, 파티 리더만 초대할 수 있습니다. +Party.NotInYourParty=&4{0}님이 당신의 파티에 속해 있지 않습니다. +Party.NotOwner=&4파티 리더가 아닙니다. +Party.Target.NotOwner=&4{0}님이 파티 리더가 아닙니다. +Party.Owner.New=&a{0}님이 새로운 파티 리더가 되었습니다. +Party.Owner.NotLeader=&4더 이상 파티 리더가 아닙니다. +Party.Owner.Player =&a파티 리더가 되었습니다. +Party.Password.None=&c파티에 비밀번호가 설정되어 있습니다. 가입하려면 비밀번호를 제공하세요. +Party.Password.Incorrect=&c파티 비밀번호가 잘못되었습니다. +Party.Password.Set=&a파티 비밀번호가 &a{0}&a(으)로 설정되었습니다. +Party.Password.Removed=&a파티 비밀번호가 지워졌습니다. +Party.Player.Invalid=&c유효하지 않은 플레이어입니다. +Party.NotOnline=&4{0}님이 오프라인입니다! +Party.Player.InSameParty=&c{0}님은 이미 당신의 파티에 속해 있습니다! +Party.PlayerNotInParty=&4{0}님이 파티에 속해 있지 않습니다. +Party.Specify=&c파티를 지정해야 합니다. +Party.Teleport.Dead=&c죽은 플레이어에게 텔레포트할 수 없습니다. +Party.Teleport.Hurt=&c최근 {0}초 동안 다치셨기 때문에 텔레포트할 수 없습니다. +Party.Teleport.Player=&a당신이 {0}님에게 텔레포트하였습니다. +Party.Teleport.Self=&c자신에게 텔레포트할 수 없습니다! +Party.Teleport.Target=&a{0}님이 당신에게 텔레포트하였습니다. +Party.Teleport.Disabled=&c{0}님이 파티 텔레포트를 허용하지 않습니다. +Party.Rename.Same=&c이미 해당 파티의 이름입니다! +Party.Join.Self=&c자신에게 가입할 수 없습니다! +Party.Unlocked=&7파티가 잠금 해제되었습니다 +Party.Disband=&7파티가 해체되었습니다 +Party.Alliance.Formed=&7당신의 파티가 이제 &a{0}&7님과 동맹 관계입니다 +Party.Alliance.Disband=&7당신의 파티는 더 이상 &c{0}&7님과 동맹 관계가 아닙니다 +Party.Status.Locked=&4(초대 전용) +Party.Status.Unlocked=&2(공개) +Party.LevelUp=&e파티 레벨이 {0}만큼 증가하였습니다. 전체 ({1}) Party.Feature.Chat=파티 채팅 Party.Feature.Teleport=파티 텔레포트 Party.Feature.Alliance=동맹 Party.Feature.ItemShare=아이템 공유 -Party.Feature.XpShare=경험치 공유 -Party.Feature.Locked.Chat={0}레벨 때 스킬해제 (파티 채팅) -Party.Feature.Locked.Teleport={0}레벨 때 스킬해제 (파티 텔레포트) -Party.Feature.Locked.Alliance={0}레벨 때 스킬해제 (동맹) -Party.Feature.Locked.ItemShare={0}레벨 때 스킬해제 (아이템 공유) -Party.Feature.Locked.XpShare={0}레벨 때 스킬해제 (경험치 공유) -Party.Feature.Disabled.1=파티 채팅은 아직 해제되지 않았습니다. -Party.Feature.Disabled.2=파티 텔레포트는 아직 해제되지 않았습니다. -Party.Feature.Disabled.3=파티 동맹은 아직 해제되지 않았습니다. -Party.Feature.Disabled.4=아이템 공유는 아직 해제되지 않았습니다. -Party.Feature.Disabled.5=경험치 공유는 아직 해제되지 않았습니다. -Party.ShareType.Xp=경험치 +Party.Feature.XpShare=XP 공유 +Party.Feature.Locked.Chat=잠금 해제까지 {0}+ (파티 채팅) +Party.Feature.Locked.Teleport=잠금 해제까지 {0}+ (파티 텔레포트) +Party.Feature.Locked.Alliance=잠금 해제까지 {0}+ (동맹) +Party.Feature.Locked.ItemShare=잠금 해제까지 {0}+ (아이템 공유) +Party.Feature.Locked.XpShare=잠금 해제까지 {0}+ (XP 공유) +Party.Feature.Disabled.1=&c파티 채팅이 아직 잠금 해제되지 않았습니다. +Party.Feature.Disabled.2=&c파티 텔레포트가 아직 잠금 해제되지 않았습니다. +Party.Feature.Disabled.3=&c파티 동맹이 아직 잠금 해제되지 않았습니다. +Party.Feature.Disabled.4=&c파티 아이템 공유가 아직 잠금 해제되지 않았습니다. +Party.Feature.Disabled.5=&c파티 XP 공유가 아직 잠금 해제되지 않았습니다. +Party.ShareType.Xp=XP Party.ShareType.Item=아이템 Party.ShareMode.None=없음 -Party.ShareMode.Equal=균등 -Party.ShareMode.Random=무작위 -Party.ItemShare.Category.Loot=강탈 -Party.ItemShare.Category.Mining=채광 -Party.ItemShare.Category.Herbalism=약초학 -Party.ItemShare.Category.Woodcutting=벌목 +Party.ShareMode.Equal=동등 +Party.ShareMode.Random=랜덤 +Party.ItemShare.Category.Loot=전리품 +Party.ItemShare.Category.Mining=채굴 +Party.ItemShare.Category.Herbalism=허브 수확 +Party.ItemShare.Category.Woodcutting=나무 벌채 Party.ItemShare.Category.Misc=기타 - ##xp -Commands.XPGain.Acrobatics=떨어지기 -Commands.XPGain.Alchemy=포션 양조하기 -Commands.XPGain.Archery=몬스터 공격하기 -Commands.XPGain.Axes=몬스터 공격하기 -Commands.XPGain.Child=상위 스킬들로 부터 레벨들을 얻습니다 -Commands.XPGain.Excavation=땅 파거나 보물 발견하기 -Commands.XPGain.Fishing=낚시하기 -Commands.XPGain.Herbalism=식물 수집하기 -Commands.XPGain.Mining=돌이나 광석 캐기 -Commands.XPGain.Repair=수리하기 -Commands.XPGain.Swords=몬스터 공격하기 -Commands.XPGain.Taming=동물을 조련하거나, 조련된 동물로 사냥하기 -Commands.XPGain.Unarmed=몬스터 공격하기 -Commands.XPGain.Woodcutting=나무 자르기 -Commands.XPGain=&8경험치 얻는 방법: &f{0} -Commands.xplock.locked=&6당신의 경험치 바는 {0}로 잠겼습니다! -Commands.xplock.unlocked=&6당신의 경험치 바는 &a잠금 해제되었습니다&6! -Commands.xprate.modified=경험치 배율이 {0}배로 수정되었습니다 -Commands.xprate.over=mcMMO 경험치 이벤트가 종료되었습니다!! -Commands.xprate.proper.0=경험치 배율 이벤트를 사용법: &f/xprate <배율> -Commands.xprate.proper.1=경험치 배율을 초기화 방법: &f/xprate reset -Commands.xprate.proper.2=이것은 XP 이벤트인지 아닌지 true 또는 false로 나타내기 위해 지정하십시오 -Commands.xprate.started.0=&6mcMMO 경험치 이벤트가 시작되었습니다! -Commands.xprate.started.1=&6mcMMO 경험치 배율은 {0}배 입니다! -XPRate.Event= &6mcMMO 는 현재 경험치 이벤트 중입니다! 경험치는 {0}배 입니다! -Commands.NegativeNumberWarn=마이너스 숫자는 허용되지 않습니다! -Commands.Event.Start=&amcMMO&6 이벤트 시작! +Commands.XPGain.Acrobatics=낙하 +Commands.XPGain.Alchemy=포션 제조 +Commands.XPGain.Archery=몬스터 공격 +Commands.XPGain.Axes=몬스터 공격 +Commands.XPGain.Child=상위 스킬로부터 레벨을 얻음 +Commands.XPGain.Excavation=파헤치기 및 보물 찾기 +Commands.XPGain.Fishing=낚시 (당연한 얘기지요!) +Commands.XPGain.Herbalism=약초 수확 +Commands.XPGain.Mining=돌 및 광석 채굴 +Commands.XPGain.Repair=수리 +Commands.XPGain.Swords=몬스터 공격 +Commands.XPGain.Taming=동물 조련 또는 늑대와의 전투 +Commands.XPGain.Unarmed=몬스터 공격 +Commands.XPGain.Woodcutting=나무 베기 +Commands.XPGain=&8XP 획득: &f{0} +Commands.xplock.locked=&6XP 바가 이제 {0}으로 잠겨 있습니다! +Commands.xplock.unlocked=&6XP 바가 이제 &a잠금 해제&6되었습니다! +Commands.xprate.modified=&cXP 비율이 {0}(으)로 변경되었습니다 +Commands.xprate.over=&cmcMMO XP 비율 이벤트가 종료되었습니다!! +Commands.xprate.proper.0=&cXP 비율을 변경하려면 올바른 사용법은 /xprate <정수> 입니다 +Commands.xprate.proper.1=&cXP 비율을 기본값으로 복원하려면 올바른 사용법은 /xprate reset입니다 +Commands.xprate.proper.2=&cXP 이벤트 여부를 지정하려면 true 또는 false를 명시하십시오 +Commands.NegativeNumberWarn=음수를 사용하지 마세요! +Commands.Event.Start=&amcMMO&6 이벤트! Commands.Event.Stop=&amcMMO&3 이벤트 종료! -Commands.Event.Stop.Subtitle=&a즐거운 시간이었기를 바랍니다! -Commands.Event.XP=&3XP 배율이 이제 &6{0}&3배입니다 +Commands.Event.Stop.Subtitle=&a즐거웠길 바랍니다! +Commands.Event.XP=&3XP 비율은 이제 &6{0}&3배입니다 Commands.xprate.started.0=&6mcMMO XP 이벤트가 시작되었습니다! -Commands.xprate.started.1=&6mcMMO XP 배율은 이제 {0}배입니다! +Commands.xprate.started.1=&6mcMMO XP 비율이 {0}배로 설정되었습니다! -# Admin Notifications +# 관리자 알림 Server.ConsoleName=&e[서버] -Notifications.Admin.XPRate.Start.Self=&7전체 XP 배율을 &6{0}배로 설정했습니다. -Notifications.Admin.XPRate.End.Self=&7XP 배율 이벤트를 종료했습니다. -Notifications.Admin.XPRate.End.Others={0} &7님이 XP 배율 이벤트를 종료했습니다. -Notifications.Admin.XPRate.Start.Others={0} &7님이 전체 XP 배율 {1}배로 이벤트를 시작 또는 수정했습니다. +Notifications.Admin.XPRate.Start.Self=&7전역 XP 비율 배수를 &6{0}배&7로 설정했습니다 +Notifications.Admin.XPRate.End.Self=&7XP 비율 이벤트를 종료했습니다. +Notifications.Admin.XPRate.End.Others={0}&7님이 XP 비율 이벤트를 종료했습니다 +Notifications.Admin.XPRate.Start.Others={0}&7님이 전역 배수가 {1}배인 XP 비율 이벤트를 시작하거나 수정했습니다 Notifications.Admin.Format.Others=&6(&amcMMO &3관리자&6) &7{0} Notifications.Admin.Format.Self=&6(&amcMMO&6) &7{0} -#GUIDES -Guides.Available=&7{0} 가이드가 있습니다 - 타입 /{1} ? [페이지] +# 이벤트 +XPRate.Event=&6mcMMO가 현재 XP 비율 이벤트 중입니다! XP 비율은 {0}배입니다! + +#가이드 +Guides.Available=&7{0}에 대한 가이드 사용 가능 - /{1} ? [페이지] Guides.Header=&6-=&a{0} 가이드&6=- -Guides.Page.Invalid=올바른 페이지 번호가 아닙니다! -Guides.Page.OutOfRange=그 페이지는 존재하지 않습니다, 오직 총 {0} 페이지가 있습니다. -Guides.Usage= 사용법 /{0} ? [페이지] - -##Acrobatics -Guides.Acrobatics.Section.0=&3곡예에 대하여:\n&e곡예는 mcMMO의 우아하게 움직이는 예술입니다.\n&e전투 특혜와 환경 손상 특혜를 증가시킵니다.\n\n&3XP 얻기:\n&e이 스킬의 XP를 얻을려면 전투나 생존에서 피해를 \n&e입는 낙하에서 착지 행동이 요구됩니다. -Guides.Acrobatics.Section.1=&3어떻게 구르기를 하나요?\n&e당신이 낙하 피해를 받을 때 피해를 무효화할\n&e지속적인 기회를 가지게 됩니다. 웅크리기 키를 누르고 있으면\n&e떨어지는 동안 두 배의 기회를 가지게 됩니다.\n&e이는 일반 구르기 대신 우아한 구르기를 발동시킵니다.\n&e우아한 구르기는 일반 구르기보다 두 배 더 자주 발동되며\n&e일반 구르기보다 더 많은 피해 방어를 제공합니다.\n&e구르기 확률은 스킬 레벨에 연결됩니다. -Guides.Acrobatics.Section.2=&3어떻게 회피를 하나요?\n&e회피는 당신이 전투에서 상처를 입을 때 입는\n&e피해를 반감시키는 지속적인 기회입니다.\n&e이것은 당신의 스킬 레벨과 연결됩니다. - -##Alchemy -Guides.Alchemy.Section.0=[[DARK_AQUA]]연금술에 대하여:\n[[YELLOW]]연금술은 물약을 양조하는 것입니다.\n[[YELLOW]]물약 양조 시간을 빠르게 하고, 이전에 얻을 수 없었던\n[[YELLOW]]새로운 물약을 추가합니다.\n\n\n[[DARK_AQUA]]XP 획득:\n[[YELLOW]]이 스킬에서 XP를 얻으려면 물약을 양조해야 합니다. -Guides.Alchemy.Section.1=[[DARK_AQUA]]Catalysis는 어떻게 작동하나요?\n[[YELLOW]]Catalysis는 양조 과정을 가속화시키며, 최대\n[[YELLOW]]속도는 기본 설정에서 레벨 1000에서 4배입니다.\n[[YELLOW]]이 능력은 기본 설정에서 레벨 100에서 잠금 해제됩니다. -Guides.Alchemy.Section.2=[[DARK_AQUA]]Concoctions는 어떻게 작동하나요?\n[[YELLOW]]Concoctions는 사용자 정의 재료로 더 많은 물약을 양조할 수 있게 합니다.\n[[YELLOW]]잠금 해제되는 특별한 재료는 등급에 따라 결정됩니다.\n[[YELLOW]]잠금 해제할 수 있는 등급은 총 8개입니다. -Guides.Alchemy.Section.3=[[DARK_AQUA]]Concoctions 1단계 재료:\n[[YELLOW]]블레이즈 가루, 발효된 거미 눈, 가스트 눈물, 레드스톤,\n[[YELLOW]]발광석 가루, 설탕, 반짝이는 수박 조각, 황금 당근,\n[[YELLOW]]마그마 크림, 네더 사마귀, 거미 눈, 수플후르, 워터 릴리,\n[[YELLOW]]복어\n[[YELLOW]](바닐라 물약) -Guides.Alchemy.Section.4=[[DARK_AQUA]]Concoctions 2단계 재료:\n[[YELLOW]]당근 (신속의 물약)\n[[YELLOW]]슬라임볼 (채굴 피로의 물약)\n\n[[DARK_AQUA]]Concoctions 3단계 재료:\n[[YELLOW]]석영 (흡수의 물약)\n[[YELLOW]]토끼 발 (도약의 물약) -Guides.Alchemy.Section.5=[[DARK_AQUA]]Concoctions 4단계 재료:\n[[YELLOW]]사과 (생명력 강화의 물약)\n[[YELLOW]]썩은 고기 (허기의 물약)\n\n[[DARK_AQUA]]Concoctions 5단계 재료:\n[[YELLOW]]갈색 버섯 (멀미의 물약)\n[[YELLOW]]잉크 주머니 (실명의 물약) -Guides.Alchemy.Section.6=[[DARK_AQUA]]Concoctions 6단계 재료:\n[[YELLOW]]고사리 (포화의 물약)\n\n[[DARK_AQUA]]Concoctions 7단계 재료:\n[[YELLOW]]독이 있는 감자 (부패의 물약)\n\n[[DARK_AQUA]]Concoctions 8단계 재료:\n[[YELLOW]]일반 황금 사과 (저항의 물약) - -##Archery -Guides.Archery.Section.0=&3궁술에 대하여:\n&e궁술은 활과 화살로 사격하는 것입니다.\n&e레벨에 따라 증가하는 데미지 보너스와 PvP에서 상대를\n&e혼란시키는 능력과 같은 다양한 전투 보너스를 제공합니다.\n&e또한, 상대의 시체에서 사용한 화살을 일부 회수할 수 있습니다.\n\n\n&3XP 획득:\n&e이 스킬에서 XP를 얻으려면 몹이나 다른 플레이어를\n&e쏴서 맞춰야 합니다. -Guides.Archery.Section.1=&3Skill Shot은 어떻게 작동하나요?\n&eSkill Shot은 사격에 추가 데미지를 제공합니다.\n&eSkill Shot의 보너스 데미지는 궁술 레벨에 따라\n&e증가합니다.\n&e기본 설정에서 Archery 레벨당 활 데미지가 50 레벨마다\n&e10% 증가하여 최대 200%의 보너스 데미지를 얻을 수 있습니다. -Guides.Archery.Section.2=&3Daze는 어떻게 작동하나요?\n&e상대를 사격할 때 상대를 혼란시키는 확률이 있습니다.\n&eDaze가 발동되면 상대는 잠시 동안 위를 바라보게 됩니다.\n&eDaze 사격은 추가로 4의 데미지(2 하트)를 입힙니다. -Guides.Archery.Section.3=&3Arrow Retrieval은 어떻게 작동하나요?\n&e활로 몹을 처치할 때 일부 화살을 회수할 수 있는\n&e확률이 있습니다.\n&e이 확률은 궁술 레벨에 따라 증가합니다.\n&e기본 설정에서 이 능력은 레벨당 0.1%씩 증가하여\n&e레벨 1000에서 100%까지 증가합니다. - -##Axes -Guides.Axes.Section.0=&3부술에 대하여:\n&e부술 스킬을 사용하여 도끼로 나무를 베는 것 이상의 다양한 기능을 사용할 수 있습니다.\n&e부술 스킬을 사용하여 몹과 플레이어를 공격하고 경험치를 얻을 수 있으며,\n&e넉백 효과로 몹에게 치명적인 일격을 가할 수 있습니다.\n&e또한, 레벨이 올라감에 따라 적의 갑옷을 쉽게 파괴할 수 있는\n&e손에 들고 사용하는 나무 굴삭기가 됩니다.\n&3XP 획득:\n&e이 스킬에서 XP를 얻으려면 도끼로 다른 몹이나 플레이어를 공격해야 합니다. -Guides.Axes.Section.1=&3뼈 쪼개기는 어떻게 작동하나요?\n&e이 능력을 사용하면 AoE(영역 효과) 공격을 할 수 있습니다.\n&e이 AoE 공격은 주요 대상에 가한 데미지의 절반만큼의 데미지를 입힙니다.\n&e따라서 대량의 몹을 제거하는 데에 효과적입니다. -Guides.Axes.Section.2=&3크리티컬 히트는 어떻게 작동하나요?\n&e크리티컬 히트는 플레이어가 추가 데미지를 입힐 수 있는\n&e확률적인 능력입니다.\n&e기본 설정에서, 부술 스킬 레벨 2마다 0.1%의 확률로\n&e크리티컬 히트를 가할 수 있으며, 이로 인해 몹에게는\n&e2배의 데미지를, 다른 플레이어에게는 1.5배의 데미지를 입힙니다. -Guides.Axes.Section.3=&3도끼 마스터리는 어떻게 작동하나요?\n&e도끼 마스터리는 액스를 사용할 때 공격에 추가 데미지를\n&e줍니다.\n&e기본 설정에서, 보너스 데미지는 레벨당 50마다 1씩 증가하며,\n&e레벨 200에서 최대 4의 추가 데미지를 얻을 수 있습니다. -Guides.Axes.Section.4=&3갑옷 충격는 어떻게 작동하나요?\n&e강력한 힘으로 갑옷을 파괴하세요!\n&e갑옷 충격는 상대의 갑옷을 손상시킬 확률이 있습니다.\n&e이 확률은 부술 스킬 레벨이 올라감에 따라 증가합니다. -Guides.Axes.Section.5=&3엄청난 충격는 어떻게 작동하나요?\n&e부술 스킬로 몹이나 플레이어를 공격할 때\n&e더 큰 영향을 줄 확률이 있습니다.\n&e기본 설정에서 이 확률은 25%입니다.\n&e이 패시브 능력은 넉백 II 마법과 유사한\n&e인챈트 효과를 가지며, 대상에게 추가 데미지를 입힙니다. - -##Excavation -Guides.Excavation.Section.0=&3발굴에 대하여:\n&e발굴은 보물을 찾기 위해 흙을 파내는 행위입니다.\n&e땅을 파내면 보물을 찾을 수 있습니다.\n&e이를 계속하면 더 많은 보물을 찾을 수 있습니다.\n\n&3XP 획득:\n&e이 스킬에서 XP를 얻으려면 삽을 들고 파야 합니다.\n&e일부 재료만 보물과 XP를 얻을 수 있습니다. -Guides.Excavation.Section.1=&3호환 가능한 재료:\n&e풀, 흙, 모래, 점토, 자갈, 버섯, 영혼 모래, 눈 -Guides.Excavation.Section.2=&3기가 드릴 버서커 사용 방법:\n&e삽을 들고 우클릭하여 도구를 준비합니다.\n&e이 상태에서 약 4초 안에 발굴 호환 가능한 재료에\n&e접촉하면 기가 드릴 버서커가 활성화됩니다. -Guides.Excavation.Section.3=&3기가 드릴 버서커란 무엇인가요?\n&e기가 드릴 버서커는 발굴 스킬과 연결된 재사용 대기시간이 있는\n&e능력입니다. 이는 보물을 찾을 확률을 세 배로 늘리고\n&e발굴 재료를 즉시 부술 수 있게 합니다. -Guides.Excavation.Section.4=&3고고학은 어떻게 작동하나요?\n&e발굴을 위한 가능한 모든 보물은 떨어지기 위한\n&e스킬 레벨 요구 사항이 있으므로, 얼마나 도움이 되는지\n&정확히 말하기는 어렵습니다.\n&e단지 기억해 두세요. 발굴 스킬이 높을수록\n&더 많은 보물을 찾을 수 있습니다.\n&또한, 각각의 발굴 호환 가능한 재료에는 고유한 보물 목록이 있습니다.\n&다시 말해, 흙에서 찾는 보물과 자갈에서 찾는 보물은 다릅니다. -Guides.Excavation.Section.5=&3발굴에 대한 참고 사항:\n&e발굴 보상은 완전히 사용자 정의할 수 있으므로\n&결과는 서버마다 다릅니다. - -##Fishing -Guides.Fishing.Section.0=&3낚시에 대하여:\n&e낚시 스킬이 있다면 낚시가 다시 즐거워집니다!\n&e숨겨진 보물을 찾고 몹에서 아이템을 떨어뜨립니다.\n\n&3XP 획득:\n&e물고기를 낚아서 경험치를 얻습니다. -Guides.Fishing.Section.1=&3보물 사냥꾼은 어떻게 작동하나요?\n&e이 능력을 사용하면 낚시로 보물을 찾을 수 있습니다.\n&e아이템이 인챈트된 상태로 드롭될 수 있는 작은 확률이 있습니다.\n&e낚시로 얻을 수 있는 모든 보물은 어떤 레벨에서든 드롭될 수 있습니다.\n&e그러나 아이템의 희귀도에 따라 얼마나 자주 드롭되는지가 달라집니다.\n&e낚시 스킬이 높을수록 더 좋은 보물을 찾을 확률이 높아집니다. -Guides.Fishing.Section.2=&3얼음 낚시는 어떻게 작동하나요?\n&e이 패시브 스킬을 사용하면 얼음 호수에서 낚시를 할 수 있습니다!\n&e낚싯대를 얼음 호수에 던지면 물고기를 낚을 수 있는 작은 구멍이 생성됩니다. -Guides.Fishing.Section.3=&3낚시꾼 장인은 어떻게 작동하나요?\n&e이 패시브 스킬은 낚시할 때 물고기가 물에 물릴 확률을 증가시킵니다.\n&e이 능력을 잠금 해제하면 보트에서 낚시할 때\n&e물고기를 잡을 확률이 높아집니다. -Guides.Fishing.Section.4=&3흔들기는 어떻게 작동하나요?\n&e이 액티브 능력을 사용하면 낚싯대로 몹에게서 아이템을 흔들어 떨어뜨릴 수 있습니다.\n&e몹은 일반적으로 죽을 때 떨어뜨리는 아이템을 드롭합니다.\n&e또한, 서바이벌 모드에서는 얻을 수 없는 몹 머리를 획득할 수도 있습니다. -Guides.Fishing.Section.5=&3어부의 다이어트는 어떻게 작동하나요?\n&e이 패시브 스킬은 물고기를 먹을 때 회복되는 포만감을 증가시킵니다. -Guides.Fishing.Section.6=&3낚시에 대한 참고 사항:\n&e낚시 아이템은 완전히 사용자 정의할 수 있으므로\n&결과는 서버마다 다릅니다. - -##Herbalism -Guides.Herbalism.Section.0=&3약초학에 대하여:\n&e약초학은 허브와 식물을 수집하는 것에 관한 스킬입니다.\n\n\n&3XP 획득:\n&e식물과 허브를 수집하세요. -Guides.Herbalism.Section.1=&3호환 가능한 블록:\n&e밀, 감자, 당근, 수박, \n&e호박, 사탕수수, 코코아 콩, 꽃, 선인장, 버섯,\n&e네더 사마귀, 원반, 덩굴. -Guides.Herbalism.Section.2=&3재배의 대지는 어떻게 작동하나요?\n&e재배의 대지는 액티브 능력으로, 괭이를 들고 우클릭하여\n&e재배의 대지를 활성화할 수 있습니다.\n&e재배의 대지는 식물을 수확할 때 3배의 드롭을 얻을 확률을\n&제공합니다. 또한 인벤토리의 씨앗을 사용하여 블록에\n&생명을 불어넣고 변형시킬 수 있는 능력을 제공합니다. -Guides.Herbalism.Section.3=&3재배의 재능 (작물)은 어떻게 작동하나요?\n&e이 패시브 능력은 작물을 수확할 때 자동으로 재심을\n&합니다. 성공 확률은 약초학 스킬 레벨에 따라 달라집니다. -Guides.Herbalism.Section.4=&3재배의 재능 (석재/돌/흙)은 어떻게 작동하나요?\n&e이 액티브 능력은 블록을 해당하는 "식물 관련" 블록으로\n&변환할 수 있게 합니다. 씨앗을 들고 블록을 우클릭하여\n&사용할 수 있습니다. 이 과정에서 1개의 씨앗이 소모됩니다. -Guides.Herbalism.Section.5=&3농부의 다이어트는 어떻게 작동하나요?\n&e이 패시브 스킬은 빵, 쿠키, 수박, 버섯 스튜, 당근,\n&감자를 섭취할 때 회복되는 포만감을 증가시킵니다. -Guides.Herbalism.Section.6=&3하이랄인의 행운은 어떻게 작동하나요?\n&e이 패시브 능력은 검으로 특정 블록을 부술 때\n&희귀 아이템을 얻을 확률을 제공합니다. -Guides.Herbalism.Section.7=&32배 드롭은 어떻게 작동하나요?\n&e이 패시브 능력은 수확 시 더 많은 수확량을 제공합니다. - -##Mining -Guides.Mining.Section.0=&3채광에 대하여:\n&e채광은 돌과 광석을 캐는 것으로, 채굴 시 드롭되는 자원의 양에 보너스를 제공합니다.\n\n&3XP 획득:\n&e이 스킬에서 XP를 얻으려면 손에 곡괭이를 들고 채굴해야 합니다.\n&e일부 블록만 XP를 제공합니다. -Guides.Mining.Section.1=&3호환 가능한 자료:\n&e돌, 석탄 광석, 철 광석, 금 광석, 다이아몬드 광석, 레드스톤 광석,\n&e청금석 광석, 흑요석, 이끼 낀 석재, 엔더 돌,\n&e발광석, 네더랙입니다. -Guides.Mining.Section.2=&3파괴자는 어떻게 작동하나요?\n&e곡괭이를 손에 들고, 우클릭을 하면 도구가 준비 상태가 됩니다.\n&e이 상태에서, 4초 안에 채광 가능 블록을 좌클릭하면, 이것은 파괴자를 발동할 것입니다. -Guides.Mining.Section.3=&3파괴자가 무엇인가요?\n&e파괴자는 채광 스킬과 연결된 재사용 대기시간이 있는 능력입니다. 이는 추가 아이템이 떨어질 확률을 세 배로 늘리고 채광 재료를 즉시 부술 수 있게 합니다. -Guides.Mining.Section.4=&3폭발 채굴은 어떻게 작동하나요?\n&e곡괭이를 손에 들고 웅크리기를 한 후 TNT를 멀리서 우클릭하세요. 이것은 TNT를 즉시 폭발시킬 것입니다. -Guides.Mining.Section.5=&3폭발 채굴은 어떻게 작동합니까?\n&e 폭발 채굴은 채광 스킬과 연결된 쿨타임이 있는 기능입니다. TNT로 채굴할 때 보너스를 제공하고 TNT를 원격으로 폭발시킬 수 있습니다. 폭발 채굴에는 세 가지 부분이 있습니다.\n&e 첫 번째 부분은 폭발 반경을 증가시키는 더 큰 폭탄입니다.\n&e 두 번째 부분은 TNT 폭발로 인한 피해를 감소시키는 해체 전문가입니다. 세 번째 부분은 단순히 TNT에서 떨어지는 광석의 양을 증가시키고 떨어지는 파편을 감소시킵니다. - -##Repair -Guides.Repair.Section.0=&3수리에 대하여:\n&e수리는 철 블록을 사용하여 갑옷과 도구를 수리할 수 있게 합니다.\n\n&3XP 획득:\n&emcMMO 모루를 사용하여 도구나 갑옷을 수리하세요. 이는\n&e기본적으로 철 블록이며, Minecraft의 일반 모루와 혼동되지 않아야 합니다. -Guides.Repair.Section.1=&3어떻게 수리를 사용할 수 있나요?\n&emcMMO 모루를 설치하고 현재 들고 있는 아이템을 우클릭하여 수리하세요. 이는 사용할 때마다 1개의 아이템을 소모합니다. -Guides.Repair.Section.2=&3수리 마스터리은 어떻게 작동하나요?\n&e수리 마스터리은 수리량을 증가시킵니다. 추가로 수리되는 양은 수리 스킬 레벨에 영향을 받습니다. -Guides.Repair.Section.3=&3슈퍼 수리는 어떻게 작동하나요?\n&e슈퍼 수리는 패시브 능력입니다. 아이템을 수리할 때\n&e더욱 효과적으로 아이템을 수리할 수 있는 기회를 제공합니다. -Guides.Repair.Section.4=&3인챈트 아이템 수리는 어떻게 작동하나요?\n&e이 패시브 능력은 일정 확률로 아이템을 수리할 때\n&e인챈트를 유지할 수 있게 합니다. 인챈트는\n&e기존 레벨로 유지되거나 낮은 레벨로 강등되거나\n&완전히 사라질 수 있습니다. - -##Salvage -Guides.Salvage.Section.0=&3회수에 대하여:\n&e회수는 금 블록을 사용하여 갑옷과 도구를 회수할 수 있게 합니다.\n\n&3XP 획득:\n&e회수는 수리 및 낚시의 부가 스킬로, 회수 스킬 레벨은 낚시 및 수리 스킬 레벨에 기반합니다. -Guides.Salvage.Section.1=&3회수를 어떻게 사용할 수 있나요?\n&emcMMO 회수 모루를 설치하고 현재 들고 있는 아이템을 우클릭하여 회수하세요. 이렇게 하면 아이템이 분해되고 아이템을 제작하는 데 사용된 재료가 반환됩니다.\n\n&e예를 들어, 철 곡괭이를 회수하면 철 주괴를 얻을 수 있습니다. -Guides.Salvage.Section.2=&3전문적인 회수는 어떻게 작동하나요?\n&e전문적인 회수를 잠금 해제하면 손상된 아이템을 회수할 수 있습니다. 레벨이 올라감에 따라 수확률이 증가합니다. 높은 수확률은 더 많은 재료를 얻을 수 있음을 의미합니다.\n&e전문적인 회수를 사용하면 항상 1개의 재료를 얻게 되며, 아이템이 너무 손상된 경우를 제외하고는 아이템을 파괴하고 아무것도 얻지 못하는 일은 없습니다. -Guides.Salvage.Section.3=&3작동 방식을 설명하기 위해 예를 들어보겠습니다:\n&e손상된 금 곡괭이를 회수한다고 가정해 봅시다. 이 경우 최대로 얻을 수 있는 양은 2개입니다(곡괭이는 3개의 주괴로 제작되며 각각의 주괴는 33.33%의 내구성을 가지므로 66%에 해당하는 2개입니다). 수확률이 66%보다 낮으면 2개의 주괴를 얻을 수 없습니다. 수확률이 이 값보다 높으면 "전체 양"을 얻을 수 있으며, 즉 2개의 주괴를 얻게 됩니다. -Guides.Salvage.Section.4=&3신비로운 회수는 어떻게 작동하나요?\n&e이 능력을 사용하면 마법이 부여된 아이템을 회수할 때 마법이 부여된 책을 얻을 수 있습니다. 레벨에 따라 완전한 부분 또는 부분적인 부여를 성공적으로 추출할 확률이 다릅니다.\n\n&e부분적으로 추출된 경우, 부여된 책은 아이템에 있던 부여보다 낮은 레벨의 부여를 가지게 됩니다. - -##Smelting -Guides.Smelting.Section.0=준비 중... - -##Swords -Guides.Swords.Section.0=&3검술에 대해:\n&e이 스킬은 검을 사용하는 사람에게 전투 보너스를 제공합니다.\n\n&3XP 획득:\n&e검을 사용하여 몹이나 다른 플레이어에게 입힌 데미지에 따라 경험치를 획득합니다. -Guides.Swords.Section.1=&3톱날 공격은 어떻게 작동하나요?\n&e톱날 공격은 액티브 능력으로, 검으로 우클릭하여 활성화할 수 있습니다. 이 능력을 사용하면 AoE(영역 효과) 공격을 할 수 있습니다. 이 AoE는 추가 25%의 데미지를 입히며, Rupture를 적용할 수도 있습니다. -Guides.Swords.Section.2=&3카운터 어택은 활성 능력입니다. 몹으로부터 공격을 막으면서 피해를 입을 때, 받은 피해의 50%를 반사할 확률이 있습니다. -Guides.Swords.Section.3=&3파열은 적에게 2초마다 피해를 입힙니다. 이 효과는 피해를 입은 대상이 죽거나 효과가 사라질 때까지 계속됩니다. 검 기술이 높을수록 피 효과의 지속 시간이 증가합니다. - -##Taming -Guides.Taming.Section.0=&3조련에 대하여:\n&e조련은 길들인 늑대를 사용할 때 다양한 전투 보너스를 제공합니다.\n\n&3XP 획득:\n&e이 스킬에서 경험치를 얻으려면 늑대/오셀롯을 길들이거나\n&e늑대와 전투해야 합니다. -Guides.Taming.Section.1=&3야생의 포효는 어떻게 작동하나요?\n&e야생의 포효는 액티브 능력으로, 뼈다귀나 생선을 들고\n&e웅크리고 좌클릭하여 늑대나 오셀롯을 소환할 수 있습니다. -Guides.Taming.Section.2=&3짐승의 포효는 어떻게 작동하나요?\n&e짐승의 포효는 펫을 검사하고 늑대와 오셀롯의\n&estats를 확인할 수 있게 합니다. 늑대나 오셀롯을 좌클릭하여\n&e짐승의 포효를 사용하세요. -Guides.Taming.Section.3=&3돌진은 어떻게 작동하나요?\n&e돌진은 패시브 능력으로, 늑대의 대상에게 출혈 효과를\n&적용할 수 있는 확률이 있습니다. -Guides.Taming.Section.4=&3날카로운 발톱은 어떻게 작동하나요?\n&e날카로운 발톱은 늑대가 입히는 피해에 추가적인 데미지 보너스를\n&제공합니다. 이 데미지 보너스는 테이밍 레벨에 따라 달라집니다. -Guides.Taming.Section.5=&3환경 인식은 어떻게 작동하나요?\n&e이 패시브 능력은 늑대가 선인장/용암과 같은 위험 요소에\n&가까이 다가갈 때 당신에게 순간이동할 수 있게 합니다.\n&또한, 늑대에게 낙하 피해 면역성을 제공합니다. -Guides.Taming.Section.6=&3두꺼운 털은 어떻게 작동하나요?\n&e이 패시브 능력은 늑대의 피해를 감소시키고\n&불에 대한 내성을 제공합니다. -Guides.Taming.Section.7=&3충격 방지는 어떻게 작동하나요?\n&e이 패시브 능력은 늑대가 폭발로 인한 피해를\n&감소시킵니다. -Guides.Taming.Section.8=&3빠른 음식 제공은 어떻게 작동하나요?\n&e이 패시브 능력은 늑대가 공격할 때마다 회복할\n&수 있는 기회를 제공합니다. - -##Unarmed -Guides.Unarmed.Section.0=&3비무장에 대해:\n&e비무장은 주먹을 무기로 사용할 때 플레이어에게 다양한 전투 보너스를 제공합니다. \n\n&3XP 획득:\n&e맨손으로 몹이나 다른 플레이어에게 입힌 피해량에 따라 경험치를 획득합니다. -Guides.Unarmed.Section.1=&3버서커는 어떻게 작동하나요?\n&e버서커는 우클릭으로 활성화되는 액티브 능력입니다. 버서커 모드에서는 추가로 50%의 피해를 입히며, 흙과 풀과 같은 약한 재료를 즉시 부술 수 있습니다. -Guides.Unarmed.Section.2=&3강철 팔 형태는 어떻게 작동하나요?\n&e강철 팔 형태는 주먹으로 몹이나 플레이어를 때릴 때 입히는 피해량을 증가시킵니다. -Guides.Unarmed.Section.3=&3화살 회피는 어떻게 작동하나요?\n&e화살 튕김은 스켈레톤이나 다른 플레이어가 발사한 화살을 튕길 확률을 제공하는 패시브 능력입니다. 화살은 피해를 입히지 않고 땅에 떨어집니다. -Guides.Unarmed.Section.4=&3강철 주먹은 어떻게 작동하나요?\n&e강철 주먹은 다른 스킬의 강제 무장 해제 효과를 방지하는 패시브 능력입니다. 비무장 레벨이 올라갈수록 강제 무장 해제를 방지하는 확률이 증가합니다. -Guides.Unarmed.Section.5=&3비무장은 어떻게 작동하나요?\n&e이 패시브 능력은 플레이어가 다른 플레이어의 무기를 해제할 수 있게 해줍니다. 대상의 장착된 아이템은 땅에 떨어집니다. - -##Woodcutting -Guides.Woodcutting.Section.0=&3벌목에 대하여:\n&e벌목은 나무를 베는 것에 관한 스킬입니다.\n\n&3XP 획득:\n&e나무 블록을 부술 때마다 경험치를 얻습니다. -Guides.Woodcutting.Section.1=&3나무꾼은 어떻게 작동하나요?\n&e나무꾼은 액티브 능력으로, 도끼를 들고 우클릭하여\n&e나무 베기를 활성화할 수 있습니다. 이렇게 하면\n&e전체 나무가 즉시 부서지고 한 번에 모든\n&나무 블록이 떨어집니다. -Guides.Woodcutting.Section.2=&3나뭇잎 청소는 어떻게 작동하나요?\n&e나뭇잎 청소는 패시브 능력으로, 도끼로\n&맞힌 잎사귀 블록이 즉시 부서지게 합니다.\n&기본적으로 이 능력은 레벨 100에서 해금됩니다. -Guides.Woodcutting.Section.3=&3드롭 2배는 어떻게 작동하나요?\n&e이 패시브 능력은 베는 나무마다 추가로\n&한 개의 블록을 얻을 수 있는 기회를 제공합니다. - -#INSPECT -Inspect.Offline= &c그 플레이어는 오프라인 상태이므로 검사할 수 없습니다. 오프라인 플레이어를 검사하려면 권한이 필요합니다. -Inspect.OfflineStats=mcMMO 오프라인 유저 스텟은 &e{0} 입니다 -Inspect.Stats=&amcMMO 스텟은 &e{0} 입니다 -Inspect.TooFar=당신은 그 플레이어와 너무 멀리 떨어져 있어 검사할 수 없습니다! - -#ITEMS -Item.ChimaeraWing.Fail=**키메라의 날개 실패!** -Item.ChimaeraWing.Pass=**키메라의 날개** -Item.ChimaeraWing.Name=키메라의 날개 -Item.ChimaeraWing.Lore=&7당신의 침대로 텔레포트합니다. -Item.Generic.Wait=키메라의 날개를 다시 사용하려면 &e({0}초) &c기다려야 합니다! -Item.Injured.Wait=당신은 최근에 부상을 당했으며 이것을 사용하려면 &e({0}초) &f기다려야 합니다 -Item.FluxPickaxe.Name=용해 곡괭이 -Item.FluxPickaxe.Lore.1=&7광물을 즉시 제련할 기회를 가집니다. -Item.FluxPickaxe.Lore.2=&7제련 요구 레벨 {0} 이상 - -#TELEPORTATION -Teleport.Commencing=&7텔레포트가 &6({0}) &7초안에 시작됩니다, 가만히 기다려주세요... +Guides.Page.Invalid=유효한 페이지 번호가 아닙니다! +Guides.Page.OutOfRange=해당 페이지가 존재하지 않습니다. 전체 페이지 수는 {0}입니다. +Guides.Usage= 사용법: /{0} ? [페이지] +##곡예 +Guides.Acrobatics.Section.0=&3곡예에 대해:\n&e곡예는 mcMMO에서 우아하게 움직이는 예술입니다.\n&e전투 보너스 및 환경 피해 보너스를 제공합니다.\n\n&3XP 획득:\n&e이 스킬에서 XP를 획득하려면 전투에서 피격을 회피하거나\n&e높은 곳에서 떨어져서 피해를 입어야 합니다. +Guides.Acrobatics.Section.1=&3롤링이 작동하는 방법은?\n&e낙하 피해를 입을 때 자동으로 피해를 무효화할\n&epassive한 확률이 있습니다.\n&eeSneak 버튼을 누르고 낙하 중에 확률을 두배로 늘릴 수 있습니다.\n&e이로 인해 표준 롤이 아닌 우아한 롤이 발생합니다.\n&e우아한 롤은 일반 롤과 유사하지만 두 배의 확률로\n&e발생하며 표준 롤보다 더 많은 피해 안전을 제공합니다.\n&e롤링 확률은 스킬 레벨에 결합됩니다. +Guides.Acrobatics.Section.2=&3회피가 작동하는 방법은?\n&e회피는 전투 중에\n&e입은 피해를 절반으로 줄이는 자동 확률입니다.\n&e스킬 레벨에 결합됩니다. +##연금술 +Guides.Alchemy.Section.0=[[DARK_AQUA]]연금술에 대해:\n[[YELLOW]]연금술는 포션 제조와 관련이 있습니다.\n[[YELLOW]]포션 제조 시간을 빠르게 할 뿐만 아니라\n[[YELLOW]]이전에 얻을 수 없던 새로운 포션도 추가합니다.\n\n\n[[DARK_AQUA]]XP 획득:\n[[YELLOW]]이 스킬에서 XP를 획득하려면 포션을 제조해야 합니다. +Guides.Alchemy.Section.1=[[DARK_AQUA]]카탈리시스가 작동하는 방법은?\n[[YELLOW]]카탈리시스는 제조 과정을 가속화합니다.\n[[YELLOW]]레벨 100에서 최대 속도가 4배까지 증가합니다.\n[[YELLOW]]이 능력은 기본적으로 레벨 100에서 잠금 해제됩니다. +Guides.Alchemy.Section.2=[[DARK_AQUA]]조제가 작동하는 방법은?\n[[YELLOW]]조제는 사용자 정의 재료로 더 많은 포션을 제조할 수 있습니다.\n[[YELLOW]]잠금 해제된 특별한 재료는 사용자의 등급에 따라 결정됩니다.\n[[YELLOW]]잠금 해제된 특별한 재료는 사용자의 등급에 따라 결정됩니다. 8개의 등급이 있습니다. +Guides.Alchemy.Section.3=[[DARK_AQUA]]조제 1단계 재료:\n[[YELLOW]]Blaze Powder, Fermented Spider Eye, Ghast Tear, Redstone,\n[[YELLOW]]Glowstone Dust, Sugar, Glistering Melon, Golden Carrot,\n[[YELLOW]]Magma Cream, Nether Wart, Spider Eye, Suplhur, Water Lily,\n[[YELLOW]]Pufferfish\n[[YELLOW]](기본 포션) +Guides.Alchemy.Section.4=[[DARK_AQUA]]조제 2단계 재료:\n[[YELLOW]]Carrot (Potion of Haste)\n[[YELLOW]]Slimeball (Potion of Dullness)\n\n[[DARK_AQUA]]조제 3단계 재료:\n[[YELLOW]]Quartz (Potion of Absorption)\n[[YELLOW]]Rabbit's Foot (Potion of Leaping) +Guides.Alchemy.Section.5=[[DARK_AQUA]]조제 4단계 재료:\n[[YELLOW]]Apple (Potion of Health Boost)\n[[YELLOW]]Rotten Flesh (Potion of Hunger)\n\n[[DARK_AQUA]]조제 5단계 재료:\n[[YELLOW]]Brown Mushroom (Potion of Nausea)\n[[YELLOW]]Ink Sack (Potion of Blindness) +Guides.Alchemy.Section.6=[[DARK_AQUA]]조제 6단계 재료:\n[[YELLOW]]Fern (Potion of Saturation)\n\n[[DARK_AQUA]]조제 7단계 재료:\n[[YELLOW]]Poisonous Potato (Potion of Decay)\n\n[[DARK_AQUA]]조제 8단계 재료:\n[[YELLOW]]Regular Golden Apple (Potion of Resistance) +#양궁 +Guides.Archery.Section.0=&3양궁에 대해:\n&e양궁는 활과 화살로 사격하는 것에 관한 것입니다.\n&e전투 보너스로는 레벨에 따라 증가하는 추가 데미지\n&ee와 PvP에서 상대를 헷갈리게 하는 능력 등이 있습니다.\n&ee이외에도 적들의 시체에서 사용한 화살 중 일부를\n&ee회수할 수 있습니다.\n\n\n&3XP 획득:\n&e이 스킬에서 XP를 획득하려면 몹이나 다른 플레이어를\n&ee사격해야 합니다. +Guides.Archery.Section.1=&3스킬샷이 작동하는 방법은?\n&e스킬샷은 사격에 추가 데미지를 제공합니다.\n&e스킬샷의 보너스 데미지는 양궁 레벨이\n&ee증가함에 따라 증가합니다.\n&e기본 설정에서 양궁 데미지는 매 50 레벨마다\n&ee 10%씩 증가하여 최대 200%의 보너스 데미지를 얻습니다. +Guides.Archery.Section.2=&3현혹이 작동하는 방법은?\n&e상대를 사격할 때 다른 플레이어를 헷갈리게 할\n&e자동 확률이 있습니다. 현혹이 발생하면 상대방을\n&ekort 올려 상당한 시간 동안 움직일 수 없게 합니다.\n&e현훅 사격은 추가적으로 4 데미지(2 하트)를 줍니다. +Guides.Archery.Section.3=&3화살 회수가 작동하는 방법은?\n&e활로 몹을 죽일 때 사용한 화살 중 일부를\n&e회수할 수 있는 자동 확률이 있습니다.\n&e양궁 레벨이 증가함에 따라 이 확률이\n&ee증가합니다.\n&e기본적으로 레벨마다 0.1%씩 증가하여 레벨 1000에서\n&ee 100%까지 증가합니다. +##참수 +Guides.Axes.Section.0=&3참수에 대해:\n&eAxe 스킬을 사용하면 나무를 베는 것 이상의 일에\n&e도끼를 사용할 수 있습니다! 몹과 플레이어를 찍고\n&e베어 경험치를 얻을 수 있습니다. 다른 몹에게\n&e넉백 효과를 적용하고 몹과 플레이어에게 치명적인\n&e크리티컬을 입힐 수도 있습니다. 레벨이 오르면\n&ee아처의 갑옷을 쉽게 깨는 수동 핸드 헬드 우드칩퍼가 됩니다.\n&3XP 획득:\n&e이 스킬에서 XP를 획득하려면 도끼로 다른 몹이나 플레이어를\n&eb 치면 됩니다. +Guides.Axes.Section.1=&3스컬 스플리터가 작동하는 방법은?\n&e이 능력은 범위 피해를 입히도록 허용합니다.\n&ee이 범위 피해는 주요 대상에 가한 피해의 절반이 됩니다.\n&e그래서 많은 몹을 쉽게 제거할 수 있습니다. +Guides.Axes.Section.2=&3치명타가 작동하는 방법은?\n&e치명타는 추가 데미지를 줄 수 있는\n&e확률을 제공하는 수동 능력입니다.\n&e기본 설정에서 Axes 2 레벨마다 0.1%의 기회를\n&e얻으며, 이로 인해 몹에게는 2배의 데미지,\n&e플레이어에게는 1.5배의 데미지를 입힐 수 있습니다. +Guides.Axes.Section.3=&3도끼 마스터리가 작동하는 방법은?\n&e도끼 마스터리는 도끼를 사용할 때 피해를 추가합니다.\n&e기본적으로 50 레벨마다 보너스 데미지가 1씩\n&e증가하여 레벨 200에서 최대 4의 추가 피해를\n&ee받습니다. +Guides.Axes.Section.4=&3갑옷 충격이 작동하는 방법은?\n&e갑옷을 파괴할만큼 충격적으로 공격하세요!\n&e갑옷 충격에는 상대의 갑옷을 손상시킬 수 있는\n&ee자동 확률이 있습니다. Axes 레벨이 오르면\n&ee이 데미지가 증가합니다. +Guides.Axes.Section.5=&3더 큰 영향이 작동하는 방법은?\n&e도끼로 몹이나 플레이어를 공격할 때 더 큰\n&e영향을 줄 수 있는 자동 확률이 있습니다.\n&e기본적으로 이 확률은 25%입니다. 이 자동 능력은\n&e넉백 II 인첸트와 유사한 극단적인 넉백 효과를\n&ee가지며 대상에게 추가 데미지를 줍니다. +##발굴 +Guides.Excavation.Section.0=&3발굴에 대해:\n&e발굴은 보물을 찾기 위해 흙을 파는 작업입니다.\n&e땅을 파면 보물을 찾을 수 있습니다.\n&e이를 통해 더 많은 보물을 찾을 수 있습니다.\n\n&3XP 획득:\n&e이 스킬에서 XP를 획득하려면 손에 삽을 들고 파야 합니다.\n&e특정한 재료만이 보물과 XP를 얻을 수 있습니다. +Guides.Excavation.Section.1=&3호환되는 재료:\n&e풀, 흙, 모래, 점토, 자갈, 버섯, 영혼 모래, 눈 +Guides.Excavation.Section.2=&3기가 드릴 브레이커 사용 방법:\n&e삽을 손에 든 상태에서 우클릭하여 도구를 준비합니다.\n&e이 상태에서 약 4초 동안 발굴 호환 재료에\n&e접촉하면 기가 드릴 브레이커를 활성화합니다. +Guides.Excavation.Section.3=&3기가 드릴 브레이커가 무엇인가요?\n&e기가 드릴 브레이커는 발굴 스킬에 링크된\n&e재사용 대기시간이 있는 능력입니다. 보물을 찾을 기회를\n&e3배로 늘리고 발굴 재료에 대한 즉시 부서를\n&ee능력화합니다. +Guides.Excavation.Section.4=&3고고학이 작동하는 방법은?\n&e발굴을 위한 모든 가능한 보물은 획득을 위한\n&e스킬 레벨 요구 사항이 있기 때문에\n&e어느 정도 도움이 되는지 어렵습니다.\n&e단지 발굴 스킬이 높을수록 찾을 수 있는\n&ee보물이 많아집니다.\n&e또한 각 발굴 호환 재료 유형마다 고유한\n&e보물 목록이 있습니다.\n&e다시 말해 Dirt에서는 다른 보물을 발견합니다.\n&ee Gravel. +Guides.Excavation.Section.5=&3발굴에 대한 참고 사항:\n&e발굴 드롭은 완전히 사용자 정의할 수 있습니다.\n&e결과는 서버마다 다릅니다. +##낚시 +Guides.Fishing.Section.0=&3낚시에 관하여:\n&e낚시 스킬을 사용하면 낚시가 다시 흥미로워집니다!\n&e숨겨진 보물을 발견하고 몹에서 아이템을 떨어뜨릴 수 있습니다.\n\n&3XP 획득:\n&e물고기를 낚아라. +Guides.Fishing.Section.1=&3보물 사냥꾼가 작동하는 방법은?\n&e이 능력을 통해 낚시로부터 보물을 발견할 수 있으며\n&e아이템의 일부가 마법부여되어 있는 작은 확률이 있습니다.\n&e낚시의 모든 가능한 보물은 어느 레벨에서나 떨어질 수 있는\n&e확률이 있습니다. 그러나 아이템의 희귀도에 따라\n&ee얼마나 자주 떨어지는지가 달라집니다.\n&e낚시 스킬이 높을수록 더 좋은 보물을 발견할 확률이\n&ee더 좋아집니다. +Guides.Fishing.Section.2=&3얼음 낚시이 작동하는 방법은?\n&e이 수동 능력을 통해 얼음 호수에서 낚시할 수 있습니다!\n&e얼음 호수에 낚싯대를 던지면 능력이 활성화되어\n&e작은 구멍을 만들어 낚시를 할 수 있습니다. +Guides.Fishing.Section.3=&3낚시꾼의 대가가 작동하는 방법은?\n&e이 수동 능력은 낚시할 때 물고기가 낚일 확률을\n&e증가시킵니다. 이 능력을 해제하면 보트에 탑승한\n&ee상태에서 낚시할 때 물고기를 잡을 확률이 높아집니다. +Guides.Fishing.Section.4=&3떨림이 작동하는 방법은?\n&e이 수동 능력을 사용하면 낚싯대로 몹에게서 아이템을\n&e떨어뜨릴 수 있습니다. \n&e몹은 일반적으로 죽을 때 떨어뜨릴 아이템을 떨어뜨립니다.\n&e생존 모드에서 획득할 수 없는 몹 해골도 획득할 수 있습니다. +Guides.Fishing.Section.5=&3어부의 다이어트가 작동하는 방법은?\n&e이 수동 능력은 물고기를 먹을 때 회복되는\n&e배고픔의 양을 증가시킵니다. +Guides.Fishing.Section.6=&3낚시에 관한 참고 사항:\n&e낚시 드롭은 완전히 사용자 정의할 수 있으므로,\n&ee결과는 서버마다 다릅니다. +##약초학 +Guides.Herbalism.Section.0=&3허브 수집에 관하여:\n&eHerbalism은 허브와 식물을 수집하는 것입니다.\n\n\n&3XP 획득:\n&e식물과 허브를 수집하십시오. +Guides.Herbalism.Section.1=&3호환되는 블록\n&e밀, 감자, 당근, 수박,\n&e호박, 사탕수수, 코코아, 꽃, 선인장, 버섯,\n&e네더 와트, 연꽃잎, 덩굴. +Guides.Herbalism.Section.2=&3녹색 에너지가 작동하는 방법은?\n&e녹색 에너지는 활성 능력으로, 괭이를 들고 있는\n&ee상태에서 마우스 오른쪽 버튼을 클릭하여 녹색 에너지를\n&e활성화할 수 있습니다.\n&e녹색 에너지는 플레이어에게 작물을 수확할 때 3배의 드롭을\n&ee얻을 기회를 부여합니다. 또한 인벤토리에서 씨앗을 사용하여\n&e블록에 생명을 주고 변형시킬 수 있는 능력을 제공합니다. +Guides.Herbalism.Section.3=&3식물 재배 (Crops)가 작동하는 방법은?\n&e이 수동 능력은 수확할 때 자동으로 작물을 재배합니다.\n&ee성공 확률은 허브 스킬에 따라 다릅니다. +Guides.Herbalism.Section.4=&3식물 재배 (Cobble/Stone Brick/Dirt)가 작동하는 방법은?\n&e이 수동 능력을 사용하면 블록을 "식물 관련" 동료로 변환할 수 있습니다.\n&e씨앗을 들고 블록을 마우스 오른쪽 버튼으로 클릭하면 됩니다. 이렇게 하면 1개의 씨앗이 소비됩니다. +Guides.Herbalism.Section.5=&3Farmer's Diet가 작동하는 방법은?\n&e이 수동 능력은 빵, 쿠키, 수박, 버섯 수프, 당근,\n&ee감자를 먹을 때 회복되는 배고픔의 양을 증가시킵니다. +Guides.Herbalism.Section.6=&3약초학의 행운이 작동하는 방법은?\n&e이 수동 능력은 일부 블록이 검술로 파괴될 때 희귀 아이템을 찾을 기회를 부여합니다. +Guides.Herbalism.Section.7=&3더블 드롭이 작동하는 방법은?\n&e이 수동 능력은 수확할 때 플레이어에게 더 많은 수확량을 제공합니다. +##채광 +Guides.Mining.Section.0=&3광업에 관하여:\n&e광업은 돌과 광물을 캐는 것으로, 채굴할 때 드롭되는 자원의 양을\n&e증가시켜줍니다.\n\n&3XP 획득:\n&e이 스킬에서 XP를 얻으려면 손에 곡괭이를 들고 채굴해야 합니다.\n&e일부 블록들만이 XP를 부여합니다. +Guides.Mining.Section.1=&3호환되는 블록:\n&e돌, 석탄 광석, 철 광석, 금 광석, 다이아몬드 광석, 레드스톤 광석,\n&e청금석 광석, 옵시디언, 이끼 난간돌, 엔더 돌,\n&e발광석, 네더랙. +Guides.Mining.Section.2=&3슈퍼 브레이커를 사용하는 방법은?\n&e손에 곡괭이를 들고 마우스 오른쪽 버튼을 클릭하여 도구를\n&e준비합니다. 이 상태에서 광업 호환되는 블록에 접촉하여\n&e슈퍼 브레이커를 활성화합니다. +Guides.Mining.Section.3=&3슈퍼 브레이커란 무엇인가요?\n&e슈퍼 브레이커는 광업 스킬에 결합된 쿨다운이 있는 능력입니다.\n&e추가 아이템이 드롭되는 확률을 세 배로 증가시키고,\n&e광업 블록에 대해 즉시 파괴할 수 있습니다. +Guides.Mining.Section.4=&3폭파 광업을 사용하는 방법은?\n&e손에 곡괭이를 들고,\n&e웅크리고 TNT를 거리에서 마우스 오른쪽 버튼으로 클릭합니다.\n&e그러면 TNT가 즉시 폭발합니다. +Guides.Mining.Section.5=&3폭파 광업이 작동하는 방법은?\n&e폭파 광업은 광업 스킬에 결합된 쿨다운이 있는 능력입니다.\n&eTNT와 함께 채굴할 때 보너스를 제공하고,\n&eTNT를 원격으로 폭파할 수 있습니다. 폭파 광업에는 세 가지 부분이 있습니다.\n&e첫 번째는 Bigger Bombs로, 폭발 반경을 증가시킵니다.\n&e두 번째는 철거 전문가로, TNT 폭발로부터의\n&e피해를 감소시킵니다. 세 번째 부분은 단순히\n&eTNT로부터 드롭되는 광석의 양을 증가시키고\n&e파편을 감소시킵니다. +##수리하다 +Guides.Repair.Section.0=&3수리에 관하여:\n&e수리를 사용하면 철 블록을 사용하여 갑옷과\n&e도구를 수리할 수 있습니다.\n\n&3XP 획득:\n&emcMMO 모루를 사용하여 도구나 갑옷을 수리하세요.\n&edefault로 철 블록이며, Vanilla Minecraft 모루와\n&e혼동해서는 안 됩니다. +Guides.Repair.Section.1=&3어떻게 수리를 사용할 수 있나요?\n&emcMMO 모루를 설치하고 현재 들고 있는 아이템을 수리하려면\n&emcMMO 모루를 클릭합니다. 이렇게 하면 사용할 때마다 1개의 아이템이 소모됩니다. +Guides.Repair.Section.2=&3수리 마스터리가 작동하는 방법은?\n&e수리 마스터리는 수리 양을 증가시킵니다. 추가로 수리된\n&e양은 수리 스킬 레벨에 영향을 받습니다. +Guides.Repair.Section.3=&3슈퍼 수리가 작동하는 방법은?\n&e슈퍼 수리는 수동 능력입니다. 아이템을 수리할 때\n&ee더 효과적으로 아이템을 수리할 기회를 부여합니다. +Guides.Repair.Section.4=&3비밀의 단조가 작동하는 방법은?\n&e이 수동 능력을 사용하면 특정 확률로 인챈트된 아이템을 수리하고\n&ee던지지 않을 수 있습니다. 인챈트는 현재 레벨에 따라\n&ee그대로 유지되거나 낮은 수준으로 강등되거나\n&ee완전히 손실될 수 있습니다. +##산출기능술 +Guides.Salvage.Section.0=&3산출에 관하여:\n&e산출을 사용하면 금 블록을 사용하여 갑옷과\n&e도구를 분해할 수 있습니다.\n\n&3XP 획득:\n&e산출은 수리 및 어업의 하위 스킬이며, 여러분의 산출\n&eskil 레벨은 여러분의 낚시 및 수리 스킬 레벨에 기반합니다. +Guides.Salvage.Section.1=&3어떻게 산출을 사용할 수 있나요?\n&emcMMO 산출 모루를 설치하고 현재 들고 있는 아이템을\n&emcMMO 산출 모루를 클릭합니다. 이렇게 하면 아이템이 분해되고,\n&e아이템을 제작하는 데 사용된 자원을 돌려받습니다.\n\n&예를 들어, 철 곡괭이를 산출하면 철 막대를 얻게 됩니다. +Guides.Salvage.Section.2=&3고급 산출이 작동하는 방법은?\n&e이 능력을 잠금 해제하면 손상된 아이템을 분해할 수 있습니다.\n&e생산 비율은 레벨이 올라감에 따라 증가합니다. 높은 생산률은\n&ee더 많은 자원을 얻을 수 있다는 의미입니다.\n&e고급 산출로 인해 항상 1개의 자원을 얻게 되며,\n&e아이템이 너무 손상되어 있는 경우에는 아무것도 얻지 못합니다.\n&e부스러기를 드롭하지 않고. +Guides.Salvage.Section.3=&3이를 설명하기 위해 예를 들어보겠습니다.\n&ee.g. 우리가 20% 손상된 금 곡괭이를 산출한다면,\n&ee최대 얻을 수 있는 양은 2개입니다.\n&e(곡괭이는 각각 33.33% 내구도로 제작되었으므로 각각 66%를 나타냅니다.)\n&ee당신의 수익률이 66% 미만인 경우 2개의 막대를 얻을 수 없습니다.\n&ee그 이상인 경우 "전체 양"을 얻을 수 있습니다.\n&ee이는 2개의 막대를 얻게됩니다. +Guides.Salvage.Section.4=&3비밀의 산출이 작동하는 방법은?\n&e이 능력을 사용하면 인챈트된 아이템을 산출하여 인챈트된 책을\n&ee얻을 수 있습니다. 성공적으로 전체 또는 부분적인 인챈트를\n&ee추출할 확률은 여러분의 레벨에 따라 다릅니다.\n\n&ee인챈트가 부분적으로 추출되는 경우 인챈트된\n&ee책은 아이템에 있던 것보다 낮은 레벨의 인챈트를 가지게 됩니다. +##제련 +Guides.Smelting.Section.0=곧 출시됩니다... +##검 +Guides.Swords.Section.0=&3검에 관하여:\n&e이 스킬은 검을 사용하는 모든 사람에게 전투 보너스를 부여합니다.\n\n&3XP 획득:\n&e검을 사용하여 몹이나 다른 플레이어에게 입힌 데미지 양에 따라 XP를 획득합니다. +Guides.Swords.Section.1=&3톱날 타격이 작동하는 방법은?\n&e톱날 타격은 액티브 능력으로, 검을 들고 우클릭하여 활성화할 수 있습니다.\n&ee오브리오트 (효과 범위) 타격을 할 수 있습니다. 이 AO는\n&e보너스 25% 데미지를 가하고 럽처를 적용할 수 있습니다. +Guides.Swords.Section.2=&3반격이 작동하는 방법은?\n&e반격은 액티브 능력입니다. 몹으로부터 공격을 받을 때 블록하면\n&ee받은 데미지의 50%를 반사할 기회가 생깁니다. +Guides.Swords.Section.3=&3파열이 작동하는 방법은?\n&e파열은 적이 매 두 초마다 피해를 입게 만듭니다. 효과가 사라지거나\n&ee적이 죽을 때까지 타겟은 출혈합니다.\n&e출혈 지속 시간은 검 스킬에 따라 증가합니다. +##길들이기 +Guides.Taming.Section.0=&3테이밍에 관하여:\n&e테이밍은 길들인 늑대를 사용할 때 플레이어에게 다양한 전투 보너스를 부여합니다.\n\n&3XP 획득:\n&e이 스킬에서 XP를 얻으려면 늑대/오셀롯을 길들이거나\n&e자신의 늑대와 전투에 들어가야 합니다. +Guides.Taming.Section.1=&3야생의 부름이 작동하는 방법은?\n&e야생의 부름은 능동적인 능력으로, 뼈나 생선을 들고 있을 때 웅크리고 왼쪽 클릭하여\n&e웅크리기 + 왼쪽 클릭를 하면 늑대나 오셀롯을 소환할 수 있습니다. +Guides.Taming.Section.2=&3야수 상식이 작동하는 방법은?\n&e야수 상식을 사용하면 애완동물을 조사하고 늑대와 오셀롯의 통계를 확인할 수 있습니다.\n&ee야수 지식를 사용하려면 늑대나 오셀롯을 왼쪽 클릭합니다. +Guides.Taming.Section.3=&3천식이 작동하는 방법은?\n&e천식은 늑대의 대상에게 출혈 효과를 발생시키는 기회가 있는 패시브 능력입니다. +Guides.Taming.Section.4=&3연마된 발톱이 작동하는 방법은?\n&e연마된 발톱은 늑대가 가하는 피해에 추가 피해 보너스를 제공합니다.\n&e데미지 보너스는 길들이기 레벨에 따라 다릅니다. +Guides.Taming.Section.5=&3환경 인식이 작동하는 방법은?\n&e이 패시브 능력은 늑대가 가시나 용암과 같은 위험에 가까이 갈 때\n&e당신에게 텔레포트할 수 있도록 합니다. 그리고 늑대에게 추락\n&e피해 면역를 부여합니다. +Guides.Taming.Section.6=&3두꺼운 모피가 작동하는 방법은?\n&e이 패시브 능력은 데미지를 줄이고 늑대를 화염 내성으로 만듭니다. +Guides.Taming.Section.7=&3충격 방지가 작동하는 방법은?\n&e이 패시브 능력은 폭발로 인한 늑대에게 가해지는 피해를 줄입니다. +Guides.Taming.Section.8=&3패스트 푸드 서비스가 작동하는 방법은?\n&e이 패시브 능력은 늑대가 공격할 때마다 회복할 기회를 제공합니다. +##비무장 +Guides.Unarmed.Section.0=&3무장에 관하여:\n&e무장은 주먹으로 무기를 사용할 때 플레이어에게 다양한 전투 보너스를 부여합니다.\n\n&3XP 획득:\n&e주먹으로 몹이나 다른 플레이어에게 가한 데미지 양에 따라 XP를 획득합니다. +Guides.Unarmed.Section.1=&3광분이 작동하는 방법은?\n&e광분은 우클릭으로 활성화되는 액티브 능력입니다. 광분 모드에서는 50% 더 많은\n&e피해 를 가하고, 흙과 풀과 같은 약한 재료를 즉시 부술 수 있습니다. +Guides.Unarmed.Section.2=&3철 팔 스타일이 작동하는 방법은?\n&e철 팔 스타일은 주먹으로 몹이나 플레이어를 때릴 때 가하는 피해를 증가시킵니다. +Guides.Unarmed.Section.3=&3화살 반사가 작동하는 방법은?\n&e화살 반사는 스켈레톤이나 다른 플레이어가 발사한 화살을 반사할 기회를 주는 패시브 능력입니다.\n&e화살은 위험하게 땅에 떨어집니다. +Guides.Unarmed.Section.4=&3철제 그립이 작동하는 방법은?\n&e철제 그립은 무장해제을 카운터하는 패시브 능력입니다. 무장 레벨이 올라갈수록 무장해제을\n&e방지하는 기회가 늘어납니다. +Guides.Unarmed.Section.5=&3무장 해제가 작동하는 방법은?\n&e이 패시브 능력은 다른 플레이어를 무장해제하여 대상의 장비가 땅에 떨어지게 만듭니다. +##벌목 +Guides.Woodcutting.Section.0=&3나무꾼에 관하여:\n&e나무꾼은 나무를 베는 데에 관한 것입니다.\n\n&3XP 획득:\n&e네모난 블록을 부술 때마다 XP가 획득됩니다. +Guides.Woodcutting.Section.1=&3트리 펠러가 작동하는 방법은?\n&e트리 펠러는 액티브 능력으로, 도끼를 들고 우클릭하여 트리 펠러를 활성화할 수 있습니다.\n&ee전체 트리를 즉시 부수고 모든 나무를 한 번에 드랍합니다. +Guides.Woodcutting.Section.2=&3잎 블로어가 작동하는 방법은?\n&e잎 블로어는 도끼로 맞을 때 잎 블록이 즉시 부서지도록 하는 패시브 능력입니다.\n&e기본으로 이 능력은 레벨 100에서 잠금 해제됩니다. +Guides.Woodcutting.Section.3=&3더블 드롭이 작동하는 방법은?\n&e이 패시브 능력은 베어 나무마다 추가 블록을 얻을 기회를 제공합니다. +#검사 +Inspect.Offline= &c오프라인 플레이어를 확인할 권한이 없습니다! +Inspect.OfflineStats=오프라인 플레이어 &e{0}의 mcMMO 통계 +Inspect.Stats=&a{0}의 mcMMO 통계 +Inspect.TooFar=해당 플레이어를 확인하기에 너무 멀리 있습니다! +#아이템 +Item.ChimaeraWing.Fail=&c**카이메라 날개 실패!** +Item.ChimaeraWing.Pass=**카이메라 날개** +Item.ChimaeraWing.Name=카이메라 날개 +Item.ChimaeraWing.Lore=&7침대로 이동합니다. +Item.ChimaeraWing.NotEnough= &e{1}&c이(가) 더 필요합니다! {0}개 +Item.NotEnough= &e{1}&c이(가) 더 필요합니다! {0}개 +Item.Generic.Wait=다시 사용하기 전에 기다려야 합니다! &e({0}초) +Item.Injured.Wait=최근에 부상당했으므로 사용을 기다려야 합니다. &e({0}초) +Item.FluxPickaxe.Name=플럭스 곡괭이 +Item.FluxPickaxe.Lore.1=&7광물을 즉시 제련하는 기회가 있습니다. +Item.FluxPickaxe.Lore.2=&7제련 레벨 {0} 이상 필요 +#텔레포트 +Teleport.Commencing=&7텔레포트 시작 &6({0}초), &7제자리에 서십시오... Teleport.Cancelled=&4텔레포트 취소됨! - -#SKILLS -Skills.Child=&6(부가 스킬) -Skills.Disarmed=&4당신은 무장 해제되었습니다! -Skills.Header=-----[]&a{0}&c[]----- -Skills.NeedMore=&4당신은 &7{0}&4가 더 필요합니다 -Skills.NeedMore.Extra=&4당신은 &7{0}{1}가 더 필요합니다 -Skills.Parents = 상위 속성들 -Skills.MaxXP=최대 XP +#스킬 +Skills.Child=&6(하위 스킬) +Skills.Disarmed=&4무장이 해제되었습니다! +Skills.Header=-----[] &a{0}&c []----- +Skills.NeedMore=&4{0}를 더 필요합니다 +Skills.NeedMore.Extra=&4{0}{1}를 더 필요합니다 +Skills.Parents= 상위 스킬 Skills.Stats={0}&a{1}&3 XP(&7{2}&3/&7{3}&3) Skills.ChildStats={0}&a{1} -Skills.TooTired=스킬 재 사용 대기시간: ({0}초) -Skills.TooTired.Named=&7(&6{0}&e {1}s&7) -Skills.TooTired.Extra=&6{0}&e의 쿨타임: {1} -Skills.Cancelled={0} 취소됨! -Skills.ConfirmOrCancel=&a다시 우-클릭을 하면 확인 &6{0}&a. 좌-클릭을 하면 취소가 됩니다. -Skills.AbilityGateRequirementFail=&7이 슈퍼 능력을 사용하려면 &e{0}&7만큼의 &3{1}&7 스킬 레벨이 필요합니다. - -#STATISTICS +Skills.MaxXP=최대 +Skills.TooTired=다시 사용하려면 너무 피곤합니다. &e({0}초) +Skills.TooTired.Named=&7(&6{0}&e 초&7) +Skills.TooTired.Extra=&6{0} &e슈퍼 능력 대기시간 - {1} +Skills.Cancelled=&6{0} &c취소되었습니다! +Skills.ConfirmOrCancel=&a다시 오른쪽 클릭하여 &6{0}&a 확인하십시오. 왼쪽 클릭하여 취소하십시오. +Skills.AbilityGateRequirementFail=&7이 슈퍼 능력을 사용하려면 &3{1}&7 레벨이 더 필요합니다. +#통계 Stats.Header.Combat=&6-=전투 스킬=- -Stats.Header.Gathering=&6-=수집 스킬=- +Stats.Header.Gathering=&6-=채집 스킬=- Stats.Header.Misc=&6-=기타 스킬=- -Stats.Own.Stats=&a[mcMMO] 스텟 - -#PERKS +Stats.Own.Stats=&a[mcMMO] 통계 +#특전 Perks.XP.Name=경험치 -Perks.XP.Desc=특정 스킬에 경험치 부스트를 받음. +Perks.XP.Desc=특정 스킬에서 부스트된 XP를 받습니다. Perks.Lucky.Name=행운 -Perks.Lucky.Desc={0} 스킬과 능력에 33.3%의 더 많은 활성화 확률을 부여합니다. -Perks.Lucky.Desc.Login=특정 스킬과 능력에 33.3%의 더 많은 활성화 확률을 부여합니다. -Perks.Lucky.Bonus=&6 ({0} 운좋은 특전과 함께) +Perks.Lucky.Desc={0} 스킬과 능력이 33.3% 더 활성화됩니다. +Perks.Lucky.Desc.Login=특정 스킬과 능력이 33.3% 더 활성화됩니다. +Perks.Lucky.Bonus=&6 (행운 특전으로 {0}) Perks.Cooldowns.Name=빠른 회복 -Perks.Cooldowns.Desc=재사용대기시간을 {0}만큼 줄입니다 -Perks.ActivationTime.Name=인내력 -Perks.ActivationTime.Desc=능력 활성 시간이 {0}초로 증가합니다. -Perks.ActivationTime.Bonus=&6 ({0}초의 인내력 특전) - -#HARDCORE -Hardcore.Mode.Disabled=&6[mcMMO] 하드코어 모드 {0}가 {1}에 비활성화됨. -Hardcore.Mode.Enabled=&6[mcMMO] 하드코어 모드 {0}가 {1}에 활성화됨. -Hardcore.DeathStatLoss.Name=스킬 데스 패널티 -Hardcore.DeathStatLoss.PlayerDeath=&6[mcMMO] &4당신은 죽어서 &9{0}&4 레벨을 잃었습니다. -Hardcore.DeathStatLoss.PercentageChanged=&6[mcMMO] 스텟 감소 비율이 {0}로 변경되었습니다.. -Hardcore.Vampirism.Name=뱀파이어리즘 -Hardcore.Vampirism.Killer.Failure=&6[mcMMO] &e{0}&7님은 특별한 기술을 가지고 있지 않아 당신이 가져갈 지식이 없습니다. -Hardcore.Vampirism.Killer.Success=&6[mcMMO] &3당신은 &e{1}&3님으로부터 &9{0}&3 레벨을 훔쳤습니다. -Hardcore.Vampirism.Victim.Failure=&6[mcMMO] &e{0}&7님은 당신의 지식을 가져갈 수 없었습니다! -Hardcore.Vampirism.Victim.Success=&6[mcMMO] &e{0}&4님은 당신에게서 &9{1}&4 레벨을 훔쳐갔습니다! - +Perks.Cooldowns.Desc=쿨다운 기간을 {0} 만큼 줄입니다. +Perks.ActivationTime.Name=지속력 +Perks.ActivationTime.Desc=능력 활성화 시간을 {0}초로 증가시킵니다. +Perks.ActivationTime.Bonus=&6 ({0}초의 지속력 특전) +#하드코어 +Hardcore.Mode.Disabled=&6[mcMMO] 하드코어 모드 {0}이(가) {1}에 대해 비활성화되었습니다. +Hardcore.Mode.Enabled=&6[mcMMO] 하드코어 모드 {0}이(가) {1}에 대해 활성화되었습니다. +Hardcore.DeathStatLoss.Name=스킬 사망 페널티 +Hardcore.DeathStatLoss.PlayerDeath=&6[mcMMO] &4죽음으로 인해 &9{0} 레벨이 손실되었습니다. +Hardcore.DeathStatLoss.PercentageChanged=&6[mcMMO] 페널티 손실 비율이 {0}로 변경되었습니다. +Hardcore.Vampirism.Name=흡혈 +Hardcore.Vampirism.Killer.Failure=&6[mcMMO] &e{0}&7님은 지식을 훔치기에는 너무 미숙합니다. +Hardcore.Vampirism.Killer.Success=&6[mcMMO] &3당신은 &e{1} &9{0} 레벨을 훔쳤습니다. +Hardcore.Vampirism.Victim.Failure=&6[mcMMO] &e{0}&7님이 당신으로부터 지식을 훔치는 데 실패했습니다! +Hardcore.Vampirism.Victim.Success=&6[mcMMO] &e{0}&4님이 당신으로부터 &9{1} 레벨을 훔쳤습니다! +Hardcore.Vampirism.PercentageChanged=&6[mcMMO] 플레이어로부터 지식을 흡수하는 비율이 {0}로 변경되었습니다. #MOTD MOTD.Donate=&3기부 정보: MOTD.Hardcore.Enabled=&6[mcMMO] &3하드코어 모드 활성화됨: &4{0} -MOTD.Hardcore.DeathStatLoss.Stats=&6[mcMMO] &3데스 패널티 능력: &4{0}% -MOTD.Hardcore.Vampirism.Stats=&6[mcMMO] &3뱀파이어리즘 스텟 흡수: &4{0}% -MOTD.PerksPrefix=[mcMMO 특전] -MOTD.Version=&6[mcMMO] 구동중인 버전 &3{0} +MOTD.Hardcore.DeathStatLoss.Stats=&6[mcMMO] &3스킬 사망 페널티: &4{0}% +MOTD.Hardcore.Vampirism.Stats=&6[mcMMO] &3흡혈 스탯 흡수: &4{0}% +MOTD.PerksPrefix=&6[mcMMO 특전] +MOTD.Version=&6[mcMMO] 버전 &3{0} 실행 중 MOTD.Website=&6[mcMMO] &a{0}&e - mcMMO 웹사이트 - -#SMELTING -Smelting.Ability.FluxMining=유동 채굴 확률: &e{0} -Smelting.Ability.FuelEfficiency=연료 효율성 배율: &e{0}x -Smelting.Ability.Locked.0={0}레벨 때 기술이 해제됩니다 (바닐라 XP 부스트) -Smelting.Ability.Locked.1={0}레벨 때 기술이 해제됩니다 (유동 채굴) -Smelting.Ability.SecondSmelt=두번째 재련 확률: &e{0} -Smelting.Ability.VanillaXPBoost=바닐라 XP 배율: &e{0}x -Smelting.SubSkill.UnderstandingTheArt.Name=예술의 이해 -Smelting.SubSkill.UnderstandingTheArt.Description=아마도 동굴에서 너무 많은 시간을 보내고 있을지도 모릅니다.\n제련의 다양한 속성을 강화합니다. -Smelting.SubSkill.UnderstandingTheArt.Stat=바닐라 XP 배율: &e{0}배 +#제련 +Smelting.SubSkill.UnderstandingTheArt.Name=Understanding The Art +Smelting.SubSkill.UnderstandingTheArt.Description=아마도 동굴에서 제련하는 데 너무 많은 시간을 소비하고 있습니다.\n제련의 다양한 속성을 강화합니다. +Smelting.SubSkill.UnderstandingTheArt.Stat=바닐라 경험치 배율: &e{0}배 +Smelting.Ability.Locked.0={0}+ 스킬까지 잠김 (바닐라 XP 부스트) +Smelting.Ability.Locked.1={0}+ 스킬까지 잠김 (플럭스 광산) Smelting.SubSkill.FuelEfficiency.Name=연료 효율성 +Smelting.SubSkill.FuelEfficiency.Description=제련할 때 사용되는 화로 연료의 연소 시간을 증가시킵니다. Smelting.SubSkill.FuelEfficiency.Stat=연료 효율성 배율: &e{0}배 -Smelting.SubSkill.FuelEfficiency.Description=화로에서 재련시 연료 연소 시간 증가 -Smelting.SubSkill.SecondSmelt.Name=두 번째 제련 -Smelting.SubSkill.SecondSmelt.Description=제련시 얻는 자원 2배 -Smelting.SubSkill.SecondSmelt.Stat=두 번째 재련 확률 +Smelting.SubSkill.SecondSmelt.Name=이중 제련 +Smelting.SubSkill.SecondSmelt.Description=제련으로 얻은 자원을 두 배로 늘립니다. +Smelting.SubSkill.SecondSmelt.Stat=이중 제련 확률 Smelting.Effect.4=바닐라 XP 부스트 -Smelting.Effect.5=제련중 바닐라 XP 얻기 증가 -Smelting.SubSkill.FluxMining.Name=유동 채굴 -Smelting.SubSkill.FluxMining.Description=채굴중 광물 즉시 재련 확률 -Smelting.SubSkill.FluxMining.Stat=유동 채굴 확률 -Smelting.FluxMining.Success=&a광물이 재련되었습니다! -Smelting.Listener=제련(Smelting): +Smelting.Effect.5=제련 시 얻는 바닐라 XP를 증가시킵니다. +Smelting.SubSkill.FluxMining.Name=플럭스 광산 +Smelting.SubSkill.FluxMining.Description=광물이 채굴되는 동안 즉시 제련되는 확률이 있습니다. +Smelting.SubSkill.FluxMining.Stat=플럭스 광산 확률 +Smelting.Listener=제련: Smelting.SkillName=제련 - - -#COMMAND DESCRIPTIONS -Commands.Description.addlevels=mcMMO 레벨을 유저에게 추가 -Commands.Description.adminchat=mcMMO 관리자 채팅 켜기/끄기나 관리자 채팅 메세지 보내기 -Commands.Description.addxp=mcMMO 경험치를 유저에게 추가 -Commands.Description.hardcore=mcMMO 하드코어 확률 수정이나 하드코어 모드 켜기/끄기 -Commands.Description.inspect=다른 플레이어의 mcMMO 자세한 정보 보기 -Commands.Description.mcability=mcMMO 우-클릭 능력 켜기/끄기 -Commands.Description.mccooldown=모든 mcMMO 능력 재 사용 대기시간 보기 -Commands.Description.mcchatspy=mcMMO 파티 채팅 감시 켜기/끄기 -Commands.Description.mcgod=mcMMO 불사신-모드 켜기/끄기 -Commands.Description.mchud=mcMMO HUD 방식 변경 -Commands.Description.mcmmo=mcMMO 제작자 설명 보기 -Commands.Description.mcnotify=mcMMO 능력 채팅 알림 보기 켜기/끄기 -Commands.Description.mcpurge={0} 달 이상 접속안한 유저의 mcMMO 레벨과 유저를 mcMMO 데이터베이스에서 초기화시킴 -Commands.Description.mcrank=플레이어 mcMMO 순위 보기 -Commands.Description.mcrefresh=모든 mcMMO 쿨다운 초기화 -Commands.Description.mcremove=유저 mcMMO 데이터베이스 삭제 -Commands.Description.mcscoreboard=당신의 mcMMO 점수판 관리 -Commands.Description.mcstats=자신의 mcMMO 레벨과 XP 보기 -Commands.Description.mctop=mcMMO 점수표 보기 -Commands.Description.mmoedit=유저의 mcMMO 레벨 수정 -Commands.Description.mmodebug=블록을 때릴 때 유용한 정보를 출력하는 디버그 모드를 켜기/끄기. -Commands.Description.mmoupdate=mcMMO 데이터베이스를 flatfile에서 MySQL로 전환 -Commands.Description.mcconvert=데이터베이스 타입 또는 경험 공식 타입 전환 -Commands.Description.mmoshowdb=현재 데이터베이스 타입 이름 보기(나중에 /mmoupdate와 함께 쓰입니다) -Commands.Description.party=다양한 mcMMO 파티 설정 관리 -Commands.Description.partychat=mcMMO 파티 채팅 켜기/끄기나 파티 채팅 메세지 보내기 -Commands.Description.ptp=mcMMO 파티 맴버 텔레포트 -Commands.Description.Skill=mcMMO 기술 {0}의 자세한 정보 보기 -Commands.Description.skillreset=유저의 mcMMO 레벨 재설정 -Commands.Description.vampirism=mcMMO 뱀파이어리즘 비율이나 뱀파이어리즘 모드 켜기/끄기 -Commands.Description.xplock=명확한 mcMMO 기술의 mcMMO xp 바를 잠금 -Commands.Description.xprate=mcMMO XP 배율 수정이나 mcMMO XP 이벤트 시작 - -#UPDATE CHECKER -UpdateChecker.outdated=당신은 mcMMO 구버전을 사용중입니다! -UpdateChecker.newavailable=신 버전이 Spigot에 업로드되어 있습니다. - -#SCOREBOARD HEADERS -Scoreboard.Header.PlayerStats=mcMMO 스텟 -Scoreboard.Header.PlayerCooldowns=mcMMO 재 사용 대기시간 -Scoreboard.Header.PlayerRank=mcMMO 순위 -Scoreboard.Header.PlayerInspect=mcMMO 스텟: -Scoreboard.Header.PowerLevel=총 레벨 -Scoreboard.Misc.PowerLevel=&6총 레벨 +#명령어 설명 +Commands.Description.addlevels=사용자에게 mcMMO 레벨을 추가합니다. +Commands.Description.adminchat=mcMMO 관리자 채팅을 켜거나 끕니다. 또는 관리자 채팅 메시지를 보냅니다. +Commands.Description.addxp=사용자에게 mcMMO XP를 추가합니다. +Commands.Description.hardcore=mcMMO 하드코어 백분율을 수정하거나 하드코어 모드를 켜거나 끕니다. +Commands.Description.inspect=다른 플레이어에 대한 자세한 mcMMO 정보를 확인합니다. +Commands.Description.mcability=오른쪽 클릭으로 mcMMO 능력 사용을 준비하거나 해제합니다. +Commands.Description.mccooldown=모든 mcMMO 능력 쿨다운을 보여줍니다. +Commands.Description.mcchatspy=mcMMO 파티 채팅 감시를 켜거나 끕니다. +Commands.Description.mcgod=mcMMO 갓 모드를 켜거나 끕니다. +Commands.Description.mchud=mcMMO HUD 스타일을 변경합니다. +Commands.Description.mcmmo=mcMMO에 대한 간략한 설명을 표시합니다. +Commands.Description.mcnotify=mcMMO 능력 채팅 표시 알림을 켜거나 끕니다. +Commands.Description.mcpurge=mcMMO 데이터베이스에서 mcMMO 레벨이 없거나 지정된 개월 동안 접속하지 않은 사용자를 제거합니다. +Commands.Description.mcrank=플레이어의 mcMMO 순위를 표시합니다. +Commands.Description.mcrefresh=mcMMO의 모든 쿨다운을 새로 고칩니다. +Commands.Description.mcremove=사용자를 mcMMO 데이터베이스에서 제거합니다. +Commands.Description.mcscoreboard=mcMMO 스코어보드를 관리합니다. +Commands.Description.mcstats=사용자의 mcMMO 레벨과 XP를 표시합니다. +Commands.Description.mctop=mcMMO 리더보드를 표시합니다. +Commands.Description.mmoedit=사용자의 mcMMO 레벨을 편집합니다. +Commands.Description.mmodebug=디버그 모드를 토글하여 블록을 탭할 때 유용한 정보를 출력합니다. +Commands.Description.mmoupdate=이전 데이터베이스에서 현재 데이터베이스로 mcMMO 데이터베이스를 마이그레이션합니다. +Commands.Description.mcconvert=데이터베이스 유형이나 경험치 공식 유형을 변환합니다. +Commands.Description.mmoshowdb=현재 데이터베이스 유형의 이름을 표시합니다 (/mmoupdate에서 사용됨). +Commands.Description.party=다양한 mcMMO 파티 설정을 제어합니다. +Commands.Description.partychat=mcMMO 파티 채팅을 켜거나 끕니다. 또는 파티 채팅 메시지를 보냅니다. +Commands.Description.ptp=mcMMO 파티 멤버로 텔레포트합니다. +Commands.Description.Skill={0}에 대한 자세한 mcMMO 스킬 정보를 표시합니다. +Commands.Description.skillreset=사용자의 mcMMO 레벨을 재설정합니다. +Commands.Description.vampirism=mcMMO 흡혈 백분율을 수정하거나 흡혈 모드를 켜거나 끕니다. +Commands.Description.xplock=mcMMO XP 바를 특정 mcMMO 스킬에 잠급니다. +Commands.Description.xprate=mcMMO XP 비율을 수정하거나 mcMMO XP 이벤트를 시작합니다. +#업데이트 체크 +UpdateChecker.Outdated=사용 중인 mcMMO 버전이 오래되었습니다! +UpdateChecker.NewAvailable=Spigot에서 새 버전을 사용할 수 있습니다. +#스코어보드 헤더 +Scoreboard.Header.PlayerStats=&emcMMO 통계 +Scoreboard.Header.PlayerCooldowns=&emcMMO 쿨다운 +Scoreboard.Header.PlayerRank=&emcMMO 순위 +Scoreboard.Header.PlayerInspect=&emcMMO 통계: {0} +Scoreboard.Header.PowerLevel=&c파워 레벨 +Scoreboard.Misc.PowerLevel=&6파워 레벨 Scoreboard.Misc.Level=&3레벨 Scoreboard.Misc.CurrentXP=&a현재 XP -Scoreboard.Misc.RemainingXP=남은 XP -Scoreboard.Misc.Cooldown=&d재 사용 대기시간 -Scoreboard.Misc.Overall=&6종합 +Scoreboard.Misc.RemainingXP=&e남은 XP +Scoreboard.Misc.Cooldown=&d쿨다운 +Scoreboard.Misc.Overall=&6전체 Scoreboard.Misc.Ability=능력 - -#DATABASE RECOVERY -Profile.PendingLoad=&c당신의 mcMMO 프로파일을 아직 불러오지 못했습니다. -Profile.Loading.Success=&a당신의 mcMMO 프로파일을 불러왔습니다. -Profile.Loading.Failure=mcMMO는 여전히 당신의 데이터를 읽을 수 없습니다. 당신은 아마도 &b서버관리자와 연락&c하기를 원할 것입니다.\n&e당신은 여전히 서버에서 게임중이지만, 당신은 &lmcMMO 레벨이 없고&e 당신이 얻은 어느 XP도 &l저장되지 않을 겁니다&e. -Profile.Loading.AdminFailureNotice=&4[A]&c mcMMO는 &e{0}&c 플레이어 데이터 읽기가 불가능합니다. &d당신의 데이터베이스 설치를 검사해주세요. -Profile.Loading.FailurePlayer=mcMMO가 데이터를 로드하는 데 문제가 발생했습니다. 로드 시도 횟수: {0}회. 이 문제에 대해 서버 관리자에게 문의하십시오. 데이터가 로드되지 않은 동안 XP를 획득하거나 기술을 사용할 수 없습니다. -Profile.Loading.FailureNotice=&4[A]&c mcMMO는 &e{0}&c 플레이어 데이터를 로드하지 못했습니다. &d데이터베이스 설정을 확인하십시오. 현재까지 시도한 횟수: {1}. -Commands.XPBar.Usage=올바른 사용법: /mmoxpbar <스킬명 | reset> -Commands.Description.mmoxpbar=mcMMO XP 바에 대한 플레이어 설정 -Commands.Description.mmocompat=mcMMO의 호환 모드 또는 완전한 기능 여부에 대한 정보 - +#데이터베이스 복구 +Profile.PendingLoad=&c당신의 mcMMO 플레이어 데이터가 아직로드되지 않았습니다. +Profile.Loading.Success=&a당신의 mcMMO 프로필이 로드되었습니다. +Profile.Loading.FailurePlayer=&cmcMMO는 데이터로드에 문제가 있습니다. 우리는 &a{0}번&c 시도했습니다. &c이 문제에 대해 서버 관리자에게 문의하십시오. mcMMO는 데이터가로드되지 않은 동안 XP를 획득하거나 기술을 사용할 수 없습니다. +Profile.Loading.FailureNotice=&4[A]&c mcMMO가 &e{0}&c의 플레이어 데이터를로드하지 못했습니다. &d데이터베이스 설정을 확인하십시오. 지금까지 시도한 시도 {1}. #Holiday -Holiday.AprilFools.Levelup=&6{0}은(는) 이제 &a{1}&6레벨입니다! -Holiday.Anniversary=&9{0}주년을 축하합니다!\n&9nossr50의 모든 작업과 개발자들을 기리기 위해 불꽃쇼를 선물합니다! - +Holiday.AprilFools.Levelup=&6{0}님이 이제 레벨 &a{1}&6입니다! +Holiday.Anniversary=&9{0}주년을 축하합니다!\n&9모든 nossr50의 작업과 개발자들을 기념하여 불꽃놀이를 여기에 선사합니다! #Reminder Messages -Reminder.Squelched=&7알림: 현재 mcMMO로부터 알림을 받지 않고 있습니다. 알림을 활성화하려면 다시 /mcnotify 명령을 실행하십시오. 이것은 자동으로 매 시간마다 알림이 전송되는 것입니다. - +Reminder.Squelched=&7리마인더: 당신은 현재 mcMMO로부터 알림을받지 않습니다. 알림을 받으려면 다시 /mcnotify 명령을 실행하십시오. 이것은 자동으로 시간당 알림입니다. #Locale -Locale.Reloaded=&a번역을 리로드했습니다! - +Locale.Reloaded=&a로케일이 다시로드되었습니다! #Player Leveling Stuff -LevelCap.PowerLevel=&6(&amcMMO&6) &e파워 레벨 &c{0}&e에 도달했습니다. 이제부터 스킬 레벨이 상승하지 않습니다. -LevelCap.Skill=&6(&amcMMO&6) &e&6{1}&e 스킬의 레벨 캡 &c{0}&e에 도달했습니다. 이제부터 이 스킬의 레벨이 상승하지 않습니다. -Commands.XPBar.Usage=올바른 사용법: /mmoxpbar <스킬명 | reset> +LevelCap.PowerLevel=&6(&amcMMO&6) &e파워 레벨 상한에 도달했습니다: &c{0}&e. 여기서부터는 더 이상 기술을 레벨링하지 않습니다. +LevelCap.Skill=&6(&amcMMO&6) &e&6{1}&e의 레벨 상한에 도달했습니다: &c{0}&e. 여기서부터는 이 기술의 레벨이 증가하지 않습니다. +Commands.XPBar.Usage=올바른 사용법은 /mmoxpbar <기술명 | 재설정> <표시 | 숨기기> 입니다. Commands.Description.mmoxpbar=mcMMO XP 바에 대한 플레이어 설정 -Commands.Description.mmocompat=mcMMO의 호환 모드 또는 완전한 기능 여부에 대한 정보 -Compatibility.Layer.Unsupported=&6{0}&6의 호환성은 이 버전의 Minecraft에서 지원되지 않습니다. -Compatibility.Layer.PartialSupport=&6{0}&6의 호환성은 이 버전의 Minecraft에서 완전히 지원되지 않지만, mcMMO는 일부 기능을 에뮬레이션하기 위해 보조 시스템을 실행 중입니다. -Commands.XPBar.DisableAll=&6모든 mcMMO XP 바가 비활성화되었습니다. 기본 설정으로 복원하려면 /mmoxpbar reset을 사용하세요. - +Commands.Description.mmocompat=mcMMO에 대한 정보 및 호환 모드 또는 완전 기능적 모드 여부. +Compatibility.Layer.Unsupported=&6{0}&6의 호환성은 이 Minecraft 버전에서 지원되지 않습니다. +Compatibility.Layer.PartialSupport=&6{0}&6의 호환성은 이 Minecraft 버전에서 완전히 지원되지 않지만, mcMMO는 일부 부족한 기능을 모방하기위한 보조 시스템을 실행 중입니다. +Commands.XPBar.DisableAll=&6모든 mcMMO XP 바가 비활성화되었습니다. 기본 설정으로 복원하려면 /mmoxpbar 재설정을 사용하세요. #Modern Chat Settings Chat.Style.Admin=&b(A) &r{0} &b\u2192 &r{1} Chat.Style.Party=&a(P) &r{0} &a\u2192 &r{1} Chat.Style.Party.Leader=&a(P) &r{0} &6\u2192 &r{1} Chat.Identity.Console=&6* 콘솔 * -Chat.Channel.On=&6(&amcMMO-Chat&6) &e당신의 채팅 메시지는 이제 &a{0}&e 채팅 채널로 자동 전달됩니다. -Chat.Channel.Off=&6(&amcMMO-Chat&6) &7당신의 채팅 메시지는 더 이상 특정 채팅 채널로 자동 전달되지 않습니다. +Chat.Channel.On=&6(&amcMMO-채팅&6) &e당신의 채팅 메시지는 이제 &a{0}&e 채널로 자동 전달됩니다. +Chat.Channel.Off=&6(&amcMMO-채팅&6) &7당신의 채팅 메시지는 더 이상 특정 채팅 채널로 자동 전달되지 않습니다. Chat.Spy.Party=&6[&eSPY&6-&a{2}&6] &r{0} &b\u2192 &r{1} -Broadcasts.LevelUpMilestone=&6(&amcMMO&6) {0}&7님이 &3{2}&7에서 레벨 &a{1}&7에 도달했습니다! -Broadcasts.PowerLevelUpMilestone=&6(&amcMMO&6) {0}&7님이 파워 레벨 &a{1}&7에 도달했습니다! -Scoreboard.Recovery=mcMMO 점수판을 복구하는 중입니다... -Scoreboard.Disabled=이 서버의 mcMMO 점수판이 비활성화되었습니다. 이 설정은 mcMMO/config.yml에서 찾을 수 있습니다. -Scoreboard.NotSetupYet=당신의 mcMMO 점수판이 아직 설정되지 않았습니다. 나중에 다시 시도해보세요. +Broadcasts.LevelUpMilestone=&6(&amcMMO&6) {0}&7님이 &3{2}&7에서 레벨 &a{1}&7달렸습니다! +Broadcasts.PowerLevelUpMilestone=&6(&amcMMO&6) {0}&7님이 &a{1}&7단계의 파워 레벨에 도달했습니다! +Scoreboard.Recovery=mcMMO 스코어보드를 복구하는 중입니다... +Scoreboard.Disabled=이 서버의 mcMMO 스코어보드가 비활성화되었습니다. 이 설정은 mcMMO/config.yml에서 찾을 수 있습니다. +Scoreboard.NotSetupYet=당신의 mcMMO 스코어보드가 아직 설정되지 않았습니다. 나중에 다시 시도하십시오. diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index a8b56712c..198d465fc 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -20,9 +20,6 @@ folia-supported: true api-version: 1.13 commands: -# mmodroptreasures: -# description: An admin command used to spawn treasure drops -# permission: mcmmo.commands.droptreasures mmoxpbar: aliases: xpbarsettings description: Change XP bar settings @@ -124,12 +121,18 @@ commands: archery: description: Detailed mcMMO skill info permission: mcmmo.commands.archery + crossbows: + description: Detailed mcMMO skill info + permission: mcmmo.commands.crossbows swords: description: Detailed mcMMO skill info permission: mcmmo.commands.swords taming: description: Detailed mcMMO skill info permission: mcmmo.commands.taming + tridents: + description: Detailed mcMMO skill info + permission: mcmmo.commands.tridents unarmed: description: Detailed mcMMO skill info permission: mcmmo.commands.unarmed @@ -151,6 +154,10 @@ commands: salvage: description: Detailed mcMMO skill info permission: mcmmo.commands.salvage + mmopower: + description: Shows skill mastery and power level info + permission: mcmmo.commands.mmopower + aliases: [mmopowerlevel, powerlevel] adminchat: aliases: [ac, a] description: Toggle Admin chat or send admin chat messages @@ -234,6 +241,7 @@ permissions: mcmmo.ability.alchemy.all: true mcmmo.ability.archery.all: true mcmmo.ability.axes.all: true + mcmmo.ability.crossbows.all: true mcmmo.ability.excavation.all: true mcmmo.ability.fishing.all: true mcmmo.ability.herbalism.all: true @@ -243,6 +251,7 @@ permissions: mcmmo.ability.smelting.all: true mcmmo.ability.swords.all: true mcmmo.ability.taming.all: true + mcmmo.ability.tridents.all: true mcmmo.ability.unarmed.all: true mcmmo.ability.woodcutting.all: true mcmmo.ability.acrobatics.*: @@ -281,12 +290,15 @@ permissions: mcmmo.ability.archery.all: description: Allows access to all Archery abilities children: + mcmmo.ability.archery.explosiveshot: true mcmmo.ability.archery.skillshot: true mcmmo.ability.archery.daze: true mcmmo.ability.archery.arrowretrieval: true mcmmo.ability.archery.archerylimitbreak: true + mcmmo.ability.archery.explosiveshot: + description: Allows access to the Explosive Shot super ability for Archery mcmmo.ability.archery.archerylimitbreak: - description: Adds damage to bows and crossbows + description: Adds damage to bows mcmmo.ability.archery.skillshot: description: Allows bonus damage from the Archery SkillShot ability mcmmo.ability.archery.daze: @@ -319,6 +331,22 @@ permissions: description: Allows access to the Impact ability mcmmo.ability.axes.skullsplitter: description: Allows access to the Skull Splitter ability + mcmmo.ability.crossbows.*: + description: Allows access to all Crossbows abilities + children: + mcmmo.ability.crossbows.all: true + mcmmo.ability.crossbows.all: + description: Allows access to all Crossbows abilities + children: + mcmmo.ability.crossbows.trickshot: true + mcmmo.ability.crossbows.poweredshot: true + mcmmo.ability.crossbows.crossbowslimitbreak: true + mcmmo.ability.crossbows.crossbowslimitbreak: + description: Adds damage to crossbows + mcmmo.ability.crossbows.trickshot: + description: Allows access to the Trick Shot ability + mcmmo.ability.crossbows.poweredshot: + description: Allows access to the Powered Shot ability mcmmo.ability.excavation.*: default: false description: Allows access to all Excavation abilities @@ -376,6 +404,9 @@ permissions: mcmmo.ability.herbalism.greenthumb.all: true mcmmo.ability.herbalism.hylianluck: true mcmmo.ability.herbalism.shroomthumb: true + mcmmo.ability.herbalism.verdantbounty: true + mcmmo.ability.herbalism.verdantbounty: + description: Allows access to end game progression for Herbalism mcmmo.ability.herbalism.doubledrops: description: Allows double drop chance from Herbalism mcmmo.ability.herbalism.farmersdiet: @@ -456,6 +487,7 @@ permissions: mcmmo.ability.mining.blastmining.all: true mcmmo.ability.mining.doubledrops: true mcmmo.ability.mining.superbreaker: true + mcmmo.ability.mining.motherlode: true mcmmo.ability.mining.blastmining.*: default: false description: Allows access to all Blast Mining abilities @@ -473,6 +505,8 @@ permissions: description: Allows access to the Demolitions Expertise ability mcmmo.ability.mining.blastmining.detonate: description: Allows for remote TNT detonation + mcmmo.ability.mining.motherlode: + description: Allows access to mother lode subskill mcmmo.ability.mining.doubledrops: description: Allows double drop chance when mining mcmmo.ability.mining.superbreaker: @@ -677,6 +711,20 @@ permissions: description: Allows access to the Thick Fur ability mcmmo.ability.taming.pummel: description: Allows access to the Pummel ability + mcmmo.ability.tridents.*: + default: false + description: Allows access to all Trident abilities + children: + mcmmo.ability.tridents.all: true + mcmmo.ability.tridents.all: + description: Allows access to all Trident abilities + children: + mcmmo.ability.tridents.impale: true + mcmmo.ability.tridents.tridentslimitbreak: true + mcmmo.ability.tridents.impale: + description: Allows access to tridents Impale ability + mcmmo.ability.tridents.tridentslimitbreak: + description: Adds damage to tridents mcmmo.ability.unarmed.*: default: false description: Allows access to all Unarmed abilities @@ -721,6 +769,9 @@ permissions: mcmmo.ability.woodcutting.knockonwood: true mcmmo.ability.woodcutting.leafblower: true mcmmo.ability.woodcutting.treefeller: true + mcmmo.ability.woodcutting.cleancuts: true + mcmmo.ability.woodcutting.cleancuts: + description: Allows access to end game progression for Woodcutting mcmmo.ability.woodcutting.knockonwood: description: Allows access to Knock on Wood subskill mcmmo.ability.woodcutting.splinter: @@ -802,6 +853,8 @@ permissions: mcmmo.commands.alchemy: true mcmmo.commands.archery: true mcmmo.commands.axes: true + mcmmo.commands.crossbows: true + mcmmo.commands.tridents: true mcmmo.commands.excavation: true mcmmo.commands.fishing: true mcmmo.commands.herbalism: true @@ -824,6 +877,7 @@ permissions: mcmmo.commands.taming: true mcmmo.commands.unarmed: true mcmmo.commands.woodcutting: true + mcmmo.commands.mmopower: true mcmmo.commands.defaultsop: description: Implies all default op mcmmo.commands permissions. children: @@ -871,29 +925,16 @@ permissions: description: Allows access to the archery command mcmmo.commands.axes: description: Allows access to the axes command + mcmmo.commands.crossbows: + description: Allows access to the crossbows command mcmmo.commands.excavation: description: Allows access to the excavation command mcmmo.commands.fishing: description: Allows access to the fishing command -# mcmmo.commands.hardcore.*: -# default: false -# description: Implies access to all mcmmo.commands.hardcore permissions -# children: -# mcmmo.commands.hardcore.all: true -# mcmmo.commands.hardcore.all: -# description: Implies access to all mcmmo.commands.hardcore permissions -# children: -# mcmmo.commands.hardcore: true -# mcmmo.commands.hardcore.modify: true -# mcmmo.commands.hardcore.toggle: true -# mcmmo.commands.hardcore: -# description: Allows access to the hardcore command -# mcmmo.commands.hardcore.modify: -# description: Allows access to the hardcore command to modify the hardcore rate -# mcmmo.commands.hardcore.toggle: -# description: Allows access to the hardcore command to toggle hardcore on/off mcmmo.commands.herbalism: description: Allows access to the herbalism command + mcmmo.commands.tridents: + description: Allows access to the tridents command mcmmo.commands.inspect.*: default: false description: Implies access to all mcmmo.commands.inspect permissions @@ -999,6 +1040,7 @@ permissions: mcmmo.commands.mctop.alchemy: true mcmmo.commands.mctop.archery: true mcmmo.commands.mctop.axes: true + mcmmo.commands.mctop.crossbows: true mcmmo.commands.mctop.excavation: true mcmmo.commands.mctop.fishing: true mcmmo.commands.mctop.herbalism: true @@ -1008,6 +1050,7 @@ permissions: mcmmo.commands.mctop.smelting: true mcmmo.commands.mctop.swords: true mcmmo.commands.mctop.taming: true + mcmmo.commands.mctop.tridents: true mcmmo.commands.mctop.unarmed: true mcmmo.commands.mctop.woodcutting: true mcmmo.commands.mctop: @@ -1020,6 +1063,8 @@ permissions: description: Allows access to the mctop command for archery mcmmo.commands.mctop.axes: description: Allows access to the mctop command for axes + mcmmo.commands.mctop.crossbows: + description: Allows access to the mctop command for crossbows mcmmo.commands.mctop.excavation: description: Allows access to the mctop command for excavation mcmmo.commands.mctop.fishing: @@ -1038,6 +1083,8 @@ permissions: description: Allows access to the mctop command for swords mcmmo.commands.mctop.taming: description: Allows access to the mctop command for taming + mcmmo.commands.mctop.tridents: + description: Allows access to the mctop command for tridents mcmmo.commands.mctop.unarmed: description: Allows access to the mctop command for unarmed mcmmo.commands.mctop.woodcutting: @@ -1452,6 +1499,9 @@ permissions: mcmmo.perks.lucky.axes: default: false description: Gives Axes abilities & skills a 33.3% better chance to activate. + mcmmo.perks.lucky.crossbows: + default: false + description: Gives Crossbows abilities & skills a 33.3% better chance to activate. mcmmo.perks.lucky.excavation: default: false description: Gives Excavation abilities & skills a 33.3% better chance to activate. @@ -1479,6 +1529,9 @@ permissions: mcmmo.perks.lucky.taming: default: false description: Gives Taming abilities & skills a 33.3% better chance to activate. + mcmmo.perks.lucky.tridents: + default: false + description: Gives Tridents abilities & skills a 33.3% better chance to activate. mcmmo.perks.lucky.unarmed: default: false description: Gives Unarmed abilities & skills a 33.3% better chance to activate. @@ -1520,6 +1573,7 @@ permissions: mcmmo.perks.xp.150percentboost.alchemy: true mcmmo.perks.xp.150percentboost.archery: true mcmmo.perks.xp.150percentboost.axes: true + mcmmo.perks.xp.150percentboost.crossbows: true mcmmo.perks.xp.150percentboost.excavation: true mcmmo.perks.xp.150percentboost.fishing: true mcmmo.perks.xp.150percentboost.herbalism: true @@ -1528,6 +1582,7 @@ permissions: mcmmo.perks.xp.150percentboost.smelting: true mcmmo.perks.xp.150percentboost.swords: true mcmmo.perks.xp.150percentboost.taming: true + mcmmo.perks.xp.150percentboost.tridents: true mcmmo.perks.xp.150percentboost.unarmed: true mcmmo.perks.xp.150percentboost.woodcutting: true mcmmo.perks.xp.150percentboost.acrobatics: @@ -1542,6 +1597,9 @@ permissions: mcmmo.perks.xp.150percentboost.axes: default: false description: Multiplies incoming Axes XP by 2.5 + mcmmo.perks.xp.150percentboost.crossbows: + default: false + description: Multiplies incoming Crossbows XP by 2.5 mcmmo.perks.xp.150percentboost.excavation: default: false description: Multiplies incoming Excavation XP by 2.5 @@ -1566,6 +1624,9 @@ permissions: mcmmo.perks.xp.150percentboost.taming: default: false description: Multiplies incoming Taming XP by 2.5 + mcmmo.perks.xp.150percentboost.tridents: + default: false + description: Multiplies incoming Tridents XP by 2.5 mcmmo.perks.xp.150percentboost.unarmed: default: false description: Multiplies incoming Unarmed XP by 2.5 @@ -1590,6 +1651,7 @@ permissions: mcmmo.perks.xp.50percentboost.alchemy: true mcmmo.perks.xp.50percentboost.archery: true mcmmo.perks.xp.50percentboost.axes: true + mcmmo.perks.xp.50percentboost.crossbows: true mcmmo.perks.xp.50percentboost.excavation: true mcmmo.perks.xp.50percentboost.fishing: true mcmmo.perks.xp.50percentboost.herbalism: true @@ -1598,6 +1660,7 @@ permissions: mcmmo.perks.xp.50percentboost.smelting: true mcmmo.perks.xp.50percentboost.swords: true mcmmo.perks.xp.50percentboost.taming: true + mcmmo.perks.xp.50percentboost.tridents: true mcmmo.perks.xp.50percentboost.unarmed: true mcmmo.perks.xp.50percentboost.woodcutting: true mcmmo.perks.xp.50percentboost.acrobatics: @@ -1612,6 +1675,9 @@ permissions: mcmmo.perks.xp.50percentboost.axes: default: false description: Multiplies incoming Axes XP by 1.5 + mcmmo.perks.xp.50percentboost.crossbows: + default: false + description: Multiplies incoming Crossbows XP by 1.5 mcmmo.perks.xp.50percentboost.excavation: default: false description: Multiplies incoming Excavation XP by 1.5 @@ -1636,6 +1702,9 @@ permissions: mcmmo.perks.xp.50percentboost.taming: default: false description: Multiplies incoming Taming XP by 1.5 + mcmmo.perks.xp.50percentboost.tridents: + default: false + description: Multiplies incoming Tridents XP by 1.5 mcmmo.perks.xp.50percentboost.unarmed: default: false description: Multiplies incoming Unarmed XP by 1.5 @@ -1656,20 +1725,22 @@ permissions: default: false description: Multiplies incoming XP by 1.25 children: - mcmmo.perks.xp.25percentboost.acrobatics: true - mcmmo.perks.xp.25percentboost.alchemy: true - mcmmo.perks.xp.25percentboost.archery: true - mcmmo.perks.xp.25percentboost.axes: true - mcmmo.perks.xp.25percentboost.excavation: true - mcmmo.perks.xp.25percentboost.fishing: true - mcmmo.perks.xp.25percentboost.herbalism: true - mcmmo.perks.xp.25percentboost.mining: true - mcmmo.perks.xp.25percentboost.repair: true - mcmmo.perks.xp.25percentboost.smelting: true - mcmmo.perks.xp.25percentboost.swords: true - mcmmo.perks.xp.25percentboost.taming: true - mcmmo.perks.xp.25percentboost.unarmed: true - mcmmo.perks.xp.25percentboost.woodcutting: true + mcmmo.perks.xp.25percentboost.acrobatics: true + mcmmo.perks.xp.25percentboost.alchemy: true + mcmmo.perks.xp.25percentboost.archery: true + mcmmo.perks.xp.25percentboost.axes: true + mcmmo.perks.xp.25percentboost.crossbows: true + mcmmo.perks.xp.25percentboost.excavation: true + mcmmo.perks.xp.25percentboost.fishing: true + mcmmo.perks.xp.25percentboost.herbalism: true + mcmmo.perks.xp.25percentboost.mining: true + mcmmo.perks.xp.25percentboost.repair: true + mcmmo.perks.xp.25percentboost.smelting: true + mcmmo.perks.xp.25percentboost.swords: true + mcmmo.perks.xp.25percentboost.taming: true + mcmmo.perks.xp.25percentboost.tridents: true + mcmmo.perks.xp.25percentboost.unarmed: true + mcmmo.perks.xp.25percentboost.woodcutting: true mcmmo.perks.xp.25percentboost.acrobatics: default: false description: Multiplies incoming Acrobatics XP by 1.25 @@ -1682,6 +1753,9 @@ permissions: mcmmo.perks.xp.25percentboost.axes: default: false description: Multiplies incoming Axes XP by 1.25 + mcmmo.perks.xp.25percentboost.crossbows: + default: false + description: Multiplies incoming Crossbows XP by 1.25 mcmmo.perks.xp.25percentboost.excavation: default: false description: Multiplies incoming Excavation XP by 1.25 @@ -1706,6 +1780,9 @@ permissions: mcmmo.perks.xp.25percentboost.taming: default: false description: Multiplies incoming Taming XP by 1.25 + mcmmo.perks.xp.25percentboost.tridents: + default: false + description: Multiplies incoming Tridents XP by 1.25 mcmmo.perks.xp.25percentboost.unarmed: default: false description: Multiplies incoming Unarmed XP by 1.5 @@ -1730,6 +1807,7 @@ permissions: mcmmo.perks.xp.10percentboost.alchemy: true mcmmo.perks.xp.10percentboost.archery: true mcmmo.perks.xp.10percentboost.axes: true + mcmmo.perks.xp.10percentboost.crossbows: true mcmmo.perks.xp.10percentboost.excavation: true mcmmo.perks.xp.10percentboost.fishing: true mcmmo.perks.xp.10percentboost.herbalism: true @@ -1738,6 +1816,7 @@ permissions: mcmmo.perks.xp.10percentboost.smelting: true mcmmo.perks.xp.10percentboost.swords: true mcmmo.perks.xp.10percentboost.taming: true + mcmmo.perks.xp.10percentboost.tridents: true mcmmo.perks.xp.10percentboost.unarmed: true mcmmo.perks.xp.10percentboost.woodcutting: true mcmmo.perks.xp.10percentboost.acrobatics: @@ -1752,6 +1831,9 @@ permissions: mcmmo.perks.xp.10percentboost.axes: default: false description: Multiplies incoming Axes XP by 1.1 + mcmmo.perks.xp.10percentboost.crossbows: + default: false + description: Multiplies incoming Crossbows XP by 1.1 mcmmo.perks.xp.10percentboost.excavation: default: false description: Multiplies incoming Excavation XP by 1.1 @@ -1776,6 +1858,9 @@ permissions: mcmmo.perks.xp.10percentboost.taming: default: false description: Multiplies incoming Taming XP by 1.1 + mcmmo.perks.xp.10percentboost.tridents: + default: false + description: Multiplies incoming Tridents XP by 1.1 mcmmo.perks.xp.10percentboost.unarmed: default: false description: Multiplies incoming Unarmed XP by 1.1 @@ -1800,6 +1885,7 @@ permissions: mcmmo.perks.xp.customboost.alchemy: true mcmmo.perks.xp.customboost.archery: true mcmmo.perks.xp.customboost.axes: true + mcmmo.perks.xp.customboost.crossbows: true mcmmo.perks.xp.customboost.excavation: true mcmmo.perks.xp.customboost.fishing: true mcmmo.perks.xp.customboost.herbalism: true @@ -1808,6 +1894,7 @@ permissions: mcmmo.perks.xp.customboost.smelting: true mcmmo.perks.xp.customboost.swords: true mcmmo.perks.xp.customboost.taming: true + mcmmo.perks.xp.customboost.tridents: true mcmmo.perks.xp.customboost.unarmed: true mcmmo.perks.xp.customboost.woodcutting: true mcmmo.perks.xp.customboost.acrobatics: @@ -1822,6 +1909,9 @@ permissions: mcmmo.perks.xp.customboost.axes: default: false description: Multiplies incoming Axes XP by the boost amount defined in the experience config + mcmmo.perks.xp.customboost.crossbows: + default: false + description: Multiplies incoming Crossbows XP by the boost amount defined in the experience config mcmmo.perks.xp.customboost.excavation: default: false description: Multiplies incoming Excavation XP by the boost amount defined in the experience config @@ -1846,6 +1936,9 @@ permissions: mcmmo.perks.xp.customboost.taming: default: false description: Multiplies incoming Taming XP by the boost amount defined in the experience config + mcmmo.perks.xp.customboost.tridents: + default: false + description: Multiplies incoming Tridents XP by the boost amount defined in the experience config mcmmo.perks.xp.customboost.unarmed: default: false description: Multiplies incoming Unarmed XP by the boost amount defined in the experience config @@ -1870,6 +1963,7 @@ permissions: mcmmo.perks.xp.double.alchemy: true mcmmo.perks.xp.double.archery: true mcmmo.perks.xp.double.axes: true + mcmmo.perks.xp.double.crossbows: true mcmmo.perks.xp.double.excavation: true mcmmo.perks.xp.double.fishing: true mcmmo.perks.xp.double.herbalism: true @@ -1878,6 +1972,7 @@ permissions: mcmmo.perks.xp.double.smelting: true mcmmo.perks.xp.double.swords: true mcmmo.perks.xp.double.taming: true + mcmmo.perks.xp.double.tridents: true mcmmo.perks.xp.double.unarmed: true mcmmo.perks.xp.double.woodcutting: true mcmmo.perks.xp.double.acrobatics: @@ -1892,6 +1987,9 @@ permissions: mcmmo.perks.xp.double.axes: default: false description: Doubles incoming Axes XP + mcmmo.perks.xp.double.crossbows: + default: false + description: Doubles incoming Crossbows XP mcmmo.perks.xp.double.excavation: default: false description: Doubles incoming Excavation XP @@ -1916,6 +2014,9 @@ permissions: mcmmo.perks.xp.double.taming: default: false description: Doubles incoming Taming XP + mcmmo.perks.xp.double.tridents: + default: false + description: Doubles incoming Tridents XP mcmmo.perks.xp.double.unarmed: default: false description: Doubles incoming Unarmed XP @@ -1940,6 +2041,7 @@ permissions: mcmmo.perks.xp.quadruple.alchemy: true mcmmo.perks.xp.quadruple.archery: true mcmmo.perks.xp.quadruple.axes: true + mcmmo.perks.xp.quadruple.crossbows: true mcmmo.perks.xp.quadruple.excavation: true mcmmo.perks.xp.quadruple.fishing: true mcmmo.perks.xp.quadruple.herbalism: true @@ -1948,6 +2050,7 @@ permissions: mcmmo.perks.xp.quadruple.smelting: true mcmmo.perks.xp.quadruple.swords: true mcmmo.perks.xp.quadruple.taming: true + mcmmo.perks.xp.quadruple.tridents: true mcmmo.perks.xp.quadruple.unarmed: true mcmmo.perks.xp.quadruple.woodcutting: true mcmmo.perks.xp.quadruple.acrobatics: @@ -1962,6 +2065,9 @@ permissions: mcmmo.perks.xp.quadruple.axes: default: false description: Quadruples incoming Axes XP + mcmmo.perks.xp.quadruple.crossbows: + default: false + description: Quadruples incoming Crossbows XP mcmmo.perks.xp.quadruple.excavation: default: false description: Quadruples incoming Excavation XP @@ -1986,6 +2092,9 @@ permissions: mcmmo.perks.xp.quadruple.taming: default: false description: Quadruples incoming Taming XP + mcmmo.perks.xp.quadruple.tridents: + default: false + description: Quadruples incoming Tridents XP mcmmo.perks.xp.quadruple.unarmed: default: false description: Quadruples incoming Unarmed XP @@ -2010,6 +2119,7 @@ permissions: mcmmo.perks.xp.triple.alchemy: true mcmmo.perks.xp.triple.archery: true mcmmo.perks.xp.triple.axes: true + mcmmo.perks.xp.triple.crossbows: true mcmmo.perks.xp.triple.excavation: true mcmmo.perks.xp.triple.fishing: true mcmmo.perks.xp.triple.herbalism: true @@ -2018,6 +2128,7 @@ permissions: mcmmo.perks.xp.triple.smelting: true mcmmo.perks.xp.triple.swords: true mcmmo.perks.xp.triple.taming: true + mcmmo.perks.xp.triple.tridents: true mcmmo.perks.xp.triple.unarmed: true mcmmo.perks.xp.triple.woodcutting: true mcmmo.perks.xp.triple.acrobatics: @@ -2032,6 +2143,9 @@ permissions: mcmmo.perks.xp.triple.axes: default: false description: Triples incoming Axes XP + mcmmo.perks.xp.triple.crossbows: + default: false + description: Triples incoming Crossbows XP mcmmo.perks.xp.triple.excavation: default: false description: Triples incoming Excavation XP @@ -2056,6 +2170,9 @@ permissions: mcmmo.perks.xp.triple.taming: default: false description: Triples incoming Taming XP + mcmmo.perks.xp.triple.tridents: + default: false + description: Triples incoming Tridents XP mcmmo.perks.xp.triple.unarmed: default: false description: Triples incoming Unarmed XP @@ -2091,6 +2208,8 @@ permissions: mcmmo.skills.taming: true mcmmo.skills.unarmed: true mcmmo.skills.woodcutting: true + mcmmo.skills.crossbows: true + mcmmo.skills.tridents: true mcmmo.skills.acrobatics: description: Allows access to the Acrobatics skill children: @@ -2111,6 +2230,11 @@ permissions: children: mcmmo.ability.axes.all: true mcmmo.commands.axes: true + mcmmo.skills.crossbows: + description: Allows access to the Crossbows skill + children: + mcmmo.ability.crossbows.all: true + mcmmo.commands.crossbows: true mcmmo.skills.excavation: description: Allows access to the Excavation skill children: @@ -2156,6 +2280,11 @@ permissions: children: mcmmo.ability.taming.all: true mcmmo.commands.taming: true + mcmmo.skills.tridents: + description: Allows access to the Tridents skill + children: + mcmmo.ability.tridents.all: true + mcmmo.commands.tridents: true mcmmo.skills.unarmed: description: Allows access to the Unarmed skill children: @@ -2169,16 +2298,3 @@ permissions: mcmmo.showversion: default: true description: Show mcMMO version number in /mcmmo and motd - mcmmo.tools.*: - default: false - description: Implies all mcmmo.tools permissions. - children: - mcmmo.tools.all: true - mcmmo.tools.all: - default: false - description: Implies all mcmmo.tools permissions. - children: - mcmmo.tools.updatecheck: true - mcmmo.tools.updatecheck: - default: false - description: Notifies admins if there is a new version of mcMMO available diff --git a/src/main/resources/skillranks.yml b/src/main/resources/skillranks.yml index 1a305921b..590ddde11 100644 --- a/src/main/resources/skillranks.yml +++ b/src/main/resources/skillranks.yml @@ -201,6 +201,129 @@ Axes: Rank_2: 100 Rank_3: 150 Rank_4: 200 +Crossbows: + TrickShot: + Standard: + Rank_1: 5 + Rank_2: 20 + Rank_3: 40 + RetroMode: + Rank_1: 50 + Rank_2: 200 + Rank_3: 400 + PoweredShot: + Standard: + Rank_1: 1 + Rank_2: 10 + Rank_3: 15 + Rank_4: 20 + Rank_5: 25 + Rank_6: 30 + Rank_7: 35 + Rank_8: 40 + Rank_9: 45 + Rank_10: 50 + Rank_11: 55 + Rank_12: 60 + Rank_13: 65 + Rank_14: 70 + Rank_15: 75 + Rank_16: 80 + Rank_17: 85 + Rank_18: 90 + Rank_19: 95 + Rank_20: 100 + RetroMode: + Rank_1: 1 + Rank_2: 100 + Rank_3: 150 + Rank_4: 200 + Rank_5: 250 + Rank_6: 300 + Rank_7: 350 + Rank_8: 400 + Rank_9: 450 + Rank_10: 500 + Rank_11: 550 + Rank_12: 600 + Rank_13: 650 + Rank_14: 700 + Rank_15: 750 + Rank_16: 800 + Rank_17: 850 + Rank_18: 900 + Rank_19: 950 + Rank_20: 1000 + CrossbowsLimitBreak: + Standard: + Rank_1: 10 + Rank_2: 20 + Rank_3: 30 + Rank_4: 40 + Rank_5: 50 + Rank_6: 60 + Rank_7: 70 + Rank_8: 80 + Rank_9: 90 + Rank_10: 100 + RetroMode: + Rank_1: 100 + Rank_2: 200 + Rank_3: 300 + Rank_4: 400 + Rank_5: 500 + Rank_6: 600 + Rank_7: 700 + Rank_8: 800 + Rank_9: 900 + Rank_10: 1000 +Tridents: + TridentsLimitBreak: + Standard: + Rank_1: 10 + Rank_2: 20 + Rank_3: 30 + Rank_4: 40 + Rank_5: 50 + Rank_6: 60 + Rank_7: 70 + Rank_8: 80 + Rank_9: 90 + Rank_10: 100 + RetroMode: + Rank_1: 100 + Rank_2: 200 + Rank_3: 300 + Rank_4: 400 + Rank_5: 500 + Rank_6: 600 + Rank_7: 700 + Rank_8: 800 + Rank_9: 900 + Rank_10: 1000 + Impale: + Standard: + Rank_1: 5 + Rank_2: 15 + Rank_3: 25 + Rank_4: 35 + Rank_5: 45 + Rank_6: 55 + Rank_7: 65 + Rank_8: 75 + Rank_9: 85 + Rank_10: 100 + RetroMode: + Rank_1: 50 + Rank_2: 150 + Rank_3: 250 + Rank_4: 350 + Rank_5: 450 + Rank_6: 550 + Rank_7: 650 + Rank_8: 750 + Rank_9: 850 + Rank_10: 1000 Taming: BeastLore: Standard: @@ -321,6 +444,11 @@ Salvage: Rank_7: 850 Rank_8: 1000 Mining: + MotherLode: + Standard: + Rank_1: 100 + RetroMode: + Rank_1: 1000 DoubleDrops: Standard: Rank_1: 1 @@ -368,6 +496,11 @@ Herbalism: Rank_1: 1 RetroMode: Rank_1: 1 + VerdantBounty: + Standard: + Rank_1: 100 + RetroMode: + Rank_1: 1000 GreenTerra: Standard: Rank_1: 5 @@ -398,6 +531,11 @@ Herbalism: Rank_4: 800 Rank_5: 1000 Fishing: + Mastery: + Standard: + Rank_1: 100 + RetroMode: + Rank_1: 1000 MagicHunter: Standard: Rank_1: 20 @@ -637,6 +775,11 @@ Woodcutting: Rank_1: 1 RetroMode: Rank_1: 1 + CleanCuts: + Standard: + Rank_1: 100 + RetroMode: + Rank_1: 1000 KnockOnWood: Standard: Rank_1: 30 diff --git a/src/test/java/com/gmail/nossr50/MMOTestEnvironment.java b/src/test/java/com/gmail/nossr50/MMOTestEnvironment.java new file mode 100644 index 000000000..d995ed569 --- /dev/null +++ b/src/test/java/com/gmail/nossr50/MMOTestEnvironment.java @@ -0,0 +1,216 @@ +package com.gmail.nossr50; + +import com.gmail.nossr50.api.exceptions.InvalidSkillException; +import com.gmail.nossr50.config.AdvancedConfig; +import com.gmail.nossr50.config.ChatConfig; +import com.gmail.nossr50.config.GeneralConfig; +import com.gmail.nossr50.config.RankConfig; +import com.gmail.nossr50.config.experience.ExperienceConfig; +import com.gmail.nossr50.config.party.PartyConfig; +import com.gmail.nossr50.datatypes.player.McMMOPlayer; +import com.gmail.nossr50.datatypes.player.PlayerProfile; +import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +import com.gmail.nossr50.datatypes.skills.SubSkillType; +import com.gmail.nossr50.util.*; +import com.gmail.nossr50.util.blockmeta.ChunkManager; +import com.gmail.nossr50.util.player.UserManager; +import com.gmail.nossr50.util.skills.RankUtils; +import com.gmail.nossr50.util.skills.SkillTools; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.plugin.PluginManager; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.UUID; +import java.util.logging.Logger; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public abstract class MMOTestEnvironment { + protected MockedStatic mockedMcMMO; + protected MockedStatic mockedChatConfig; + protected MockedStatic experienceConfig; + protected MockedStatic mockedPermissions; + protected MockedStatic mockedRankUtils; + protected MockedStatic mockedUserManager; + protected MockedStatic mockedMisc; + protected MockedStatic mockedSkillTools; + protected MockedStatic mockedEventUtils; + protected TransientEntityTracker transientEntityTracker; + protected AdvancedConfig advancedConfig; + protected PartyConfig partyConfig; + protected GeneralConfig generalConfig; + protected RankConfig rankConfig; + protected SkillTools skillTools; + protected Server server; + protected PluginManager pluginManager; + protected World world; + + /* Mocks */ + protected Player player; + + protected UUID playerUUID = UUID.randomUUID(); + protected ItemStack itemInMainHand; + + protected PlayerInventory playerInventory; + protected PlayerProfile playerProfile; + protected McMMOPlayer mmoPlayer; + protected String playerName = "testPlayer"; + + protected ChunkManager chunkManager; + protected MaterialMapStore materialMapStore; + + protected void mockBaseEnvironment(Logger logger) throws InvalidSkillException { + mockedMcMMO = Mockito.mockStatic(mcMMO.class); + mcMMO.p = Mockito.mock(mcMMO.class); + when(mcMMO.p.getLogger()).thenReturn(logger); + + // place store + chunkManager = Mockito.mock(ChunkManager.class); + when(mcMMO.getPlaceStore()).thenReturn(chunkManager); + + // shut off mod manager for woodcutting + when(mcMMO.getModManager()).thenReturn(Mockito.mock(ModManager.class)); + when(mcMMO.getModManager().isCustomLog(any())).thenReturn(false); + + // chat config + mockedChatConfig = Mockito.mockStatic(ChatConfig.class); + when(ChatConfig.getInstance()).thenReturn(Mockito.mock(ChatConfig.class)); + + // general config + mockGeneralConfig(); + + // party config + mockPartyConfig(); + + // rank config + mockRankConfig(); + + // wire advanced config + mockAdvancedConfig(); + + // wire experience config + mockExperienceConfig(); + + // wire skill tools + this.skillTools = new SkillTools(mcMMO.p); + when(mcMMO.p.getSkillTools()).thenReturn(skillTools); + + this.transientEntityTracker = new TransientEntityTracker(); + when(mcMMO.getTransientEntityTracker()).thenReturn(transientEntityTracker); + + mockPermissions(); + + mockedRankUtils = Mockito.mockStatic(RankUtils.class); + + // wire server + this.server = Mockito.mock(Server.class); + when(mcMMO.p.getServer()).thenReturn(server); + + // wire plugin manager + this.pluginManager = Mockito.mock(PluginManager.class); + when(server.getPluginManager()).thenReturn(pluginManager); + + // wire world + this.world = Mockito.mock(World.class); + + // wire Misc + this.mockedMisc = Mockito.mockStatic(Misc.class); + when(Misc.getBlockCenter(any())).thenReturn(new Location(world, 0, 0, 0)); + + // setup player and player related mocks after everything else + this.player = Mockito.mock(Player.class); + when(player.getUniqueId()).thenReturn(playerUUID); + + // wire inventory + this.playerInventory = Mockito.mock(PlayerInventory.class); + when(player.getInventory()).thenReturn(playerInventory); + + // PlayerProfile and McMMOPlayer are partially mocked + playerProfile = new PlayerProfile("testPlayer", player.getUniqueId(), 0); + mmoPlayer = Mockito.spy(new McMMOPlayer(player, playerProfile)); + + // wire user manager + this.mockedUserManager = Mockito.mockStatic(UserManager.class); + when(UserManager.getPlayer(player)).thenReturn(mmoPlayer); + + this.materialMapStore = new MaterialMapStore(); + when(mcMMO.getMaterialMapStore()).thenReturn(materialMapStore); + } + + private void mockPermissions() { + mockedPermissions = Mockito.mockStatic(Permissions.class); + when(Permissions.isSubSkillEnabled(any(Player.class), any(SubSkillType.class))).thenReturn(true); + when(Permissions.canUseSubSkill(any(Player.class), any(SubSkillType.class))).thenReturn(true); + when(Permissions.isSubSkillEnabled(any(Player.class), any(SubSkillType.class))).thenReturn(true); + when(Permissions.canUseSubSkill(any(Player.class), any(SubSkillType.class))).thenReturn(true); + when(Permissions.lucky(player, PrimarySkillType.WOODCUTTING)).thenReturn(false); // player is not lucky + } + + private void mockRankConfig() { + rankConfig = Mockito.mock(RankConfig.class); + } + + private void mockAdvancedConfig() { + this.advancedConfig = Mockito.mock(AdvancedConfig.class); + when(mcMMO.p.getAdvancedConfig()).thenReturn(advancedConfig); + } + + private void mockGeneralConfig() { + generalConfig = Mockito.mock(GeneralConfig.class); + when(generalConfig.getTreeFellerThreshold()).thenReturn(100); + when(generalConfig.getDoubleDropsEnabled(PrimarySkillType.WOODCUTTING, Material.OAK_LOG)).thenReturn(true); + when(generalConfig.getLocale()).thenReturn("en_US"); + when(mcMMO.p.getGeneralConfig()).thenReturn(generalConfig); + } + + private void mockPartyConfig() { + partyConfig = Mockito.mock(PartyConfig.class); + when(partyConfig.isPartyEnabled()).thenReturn(false); + when(mcMMO.p.getPartyConfig()).thenReturn(partyConfig); + } + + private void mockExperienceConfig() { + experienceConfig = Mockito.mockStatic(ExperienceConfig.class); + + when(ExperienceConfig.getInstance()).thenReturn(Mockito.mock(ExperienceConfig.class)); + + // Combat + when(ExperienceConfig.getInstance().getCombatXP("Cow")).thenReturn(1D); + } + + protected void cleanupBaseEnvironment() { + // Clean up resources here if needed. + if (mockedMcMMO != null) { + mockedMcMMO.close(); + } + if (experienceConfig != null) { + experienceConfig.close(); + } + if (mockedChatConfig != null) { + mockedChatConfig.close(); + } + if (mockedPermissions != null) { + mockedPermissions.close(); + } + if (mockedRankUtils != null) { + mockedRankUtils.close(); + } + if (mockedUserManager != null) { + mockedUserManager.close(); + } + if (mockedMisc != null) { + mockedMisc.close(); + } + if (mockedEventUtils != null) { + mockedEventUtils.close(); + } + } +} diff --git a/src/test/java/com/gmail/nossr50/database/FlatFileDatabaseManagerTest.java b/src/test/java/com/gmail/nossr50/database/FlatFileDatabaseManagerTest.java index f626002c1..23463cbdb 100644 --- a/src/test/java/com/gmail/nossr50/database/FlatFileDatabaseManagerTest.java +++ b/src/test/java/com/gmail/nossr50/database/FlatFileDatabaseManagerTest.java @@ -31,7 +31,6 @@ import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; -//This class uses JUnit5/Jupiter class FlatFileDatabaseManagerTest { public static final @NotNull String TEST_FILE_NAME = "test.mcmmo.users"; @@ -39,7 +38,7 @@ class FlatFileDatabaseManagerTest { public static final @NotNull String BAD_DATA_FILE_LINE_TWENTY_THREE = "nossr51:baddata:::baddata:baddata:640:baddata:1000:1000:1000:baddata:baddata:baddata:baddata:16:0:500:20273:0:0:0:0::1000:0:0:baddata:1593543012:0:0:0:0::1000:0:0:baddata:IGNORED:1000:0:588fe472-1c82-4c4e-9aa1-7eefccb277e3:1:0:"; public static final @NotNull String DB_BADDATA = "baddatadb.users"; public static final @NotNull String DB_HEALTHY = "healthydb.users"; - public static final @NotNull String HEALTHY_DB_LINE_1 = "nossr50:1:IGNORED:IGNORED:10:2:20:3:4:5:6:7:8:9:10:30:40:50:60:70:80:90:100:IGNORED:11:110:111:222:333:444:555:666:777:IGNORED:12:120:888:IGNORED:HEARTS:13:130:588fe472-1c82-4c4e-9aa1-7eefccb277e3:1111:999:2020:"; + public static final @NotNull String HEALTHY_DB_LINE_1 = "nossr50:1:IGNORED:IGNORED:10:2:20:3:4:5:6:7:8:9:10:30:40:50:60:70:80:90:100:IGNORED:11:110:111:222:333:444:555:666:777:IGNORED:12:120:888:IGNORED:HEARTS:13:130:588fe472-1c82-4c4e-9aa1-7eefccb277e3:1111:999:2020:140:14:150:15:1111:2222:3333"; public static final @NotNull String HEALTHY_DB_LINE_ONE_UUID_STR = "588fe472-1c82-4c4e-9aa1-7eefccb277e3"; public static final String DB_MISSING_LAST_LOGIN = "missinglastlogin.users"; public static final String LINE_TWO_FROM_MISSING_DB = "nossr50:1:IGNORED:IGNORED:10:2:20:3:4:5:6:7:8:9:10:30:40:50:60:70:80:90:100:IGNORED:11:110:111:222:333:444:555:666:777:IGNORED:12:120:888:0:HEARTS:13:130:588fe472-1c82-4c4e-9aa1-7eefccb277e3:1111:999:"; @@ -52,16 +51,19 @@ class FlatFileDatabaseManagerTest { int expectedLvlMining = 1, expectedLvlWoodcutting = 2, expectedLvlRepair = 3, expectedLvlUnarmed = 4, expectedLvlHerbalism = 5, expectedLvlExcavation = 6, expectedLvlArchery = 7, expectedLvlSwords = 8, expectedLvlAxes = 9, expectedLvlAcrobatics = 10, - expectedLvlTaming = 11, expectedLvlFishing = 12, expectedLvlAlchemy = 13; + expectedLvlTaming = 11, expectedLvlFishing = 12, expectedLvlAlchemy = 13, expectedLvlCrossbows = 14, + expectedLvlTridents = 15; float expectedExpMining = 10, expectedExpWoodcutting = 20, expectedExpRepair = 30, expectedExpUnarmed = 40, expectedExpHerbalism = 50, expectedExpExcavation = 60, expectedExpArchery = 70, expectedExpSwords = 80, expectedExpAxes = 90, expectedExpAcrobatics = 100, - expectedExpTaming = 110, expectedExpFishing = 120, expectedExpAlchemy = 130; + expectedExpTaming = 110, expectedExpFishing = 120, expectedExpAlchemy = 130, expectedExpCrossbows = 140, + expectedExpTridents = 150; long expectedBerserkCd = 111, expectedGigaDrillBreakerCd = 222, expectedTreeFellerCd = 333, expectedGreenTerraCd = 444, expectedSerratedStrikesCd = 555, expectedSkullSplitterCd = 666, - expectedSuperBreakerCd = 777, expectedBlastMiningCd = 888, expectedChimaeraWingCd = 999; + expectedSuperBreakerCd = 777, expectedBlastMiningCd = 888, expectedChimaeraWingCd = 999, + expectedSuperShotgunCd = 1111, expectedTridentSuperCd = 2222, expectedExplosiveShotCd = 3333; int expectedScoreboardTips = 1111; Long expectedLastLogin = 2020L; @@ -226,7 +228,6 @@ class FlatFileDatabaseManagerTest { logger.info("File Path: "+healthyDB.getAbsolutePath()); assertArrayEquals(HEALTHY_DB_LINE_1.split(":"), dataFromFile.get(0)); assertEquals(dataFromFile.get(0)[FlatFileDatabaseManager.UUID_INDEX], HEALTHY_DB_LINE_ONE_UUID_STR); - UUID healthDBEntryOneUUID = UUID.fromString(HEALTHY_DB_LINE_ONE_UUID_STR); db = new FlatFileDatabaseManager(healthyDB, logger, PURGE_TIME, 0, true); List flagsFound = db.checkFileHealthAndStructure(); @@ -451,14 +452,13 @@ class FlatFileDatabaseManagerTest { if(SkillTools.isChildSkill(primarySkillType)) continue; -// logger.info("Checking expected values for: "+primarySkillType); -// logger.info("Profile Level Value: "+profile.getSkillLevel(primarySkillType)); -// logger.info("Expected Lvl Value: "+getExpectedLevelHealthyDBEntryOne(primarySkillType)); -// logger.info("Profile Exp Value: "+profile.getSkillXpLevelRaw(primarySkillType)); -// logger.info("Expected Exp Value: "+getExpectedExperienceHealthyDBEntryOne(primarySkillType)); + int expectedLevelHealthyDBEntryOne = getExpectedLevelHealthyDBEntryOne(primarySkillType); + int skillLevel = profile.getSkillLevel(primarySkillType); + assertEquals(expectedLevelHealthyDBEntryOne, skillLevel); - assertEquals(getExpectedLevelHealthyDBEntryOne(primarySkillType), profile.getSkillLevel(primarySkillType)); - assertEquals(getExpectedExperienceHealthyDBEntryOne(primarySkillType), profile.getSkillXpLevelRaw(primarySkillType), 0); + float expectedExperienceHealthyDBEntryOne = getExpectedExperienceHealthyDBEntryOne(primarySkillType); + float skillXpLevelRaw = profile.getSkillXpLevelRaw(primarySkillType); + assertEquals(expectedExperienceHealthyDBEntryOne, skillXpLevelRaw, 0); } //Check the other things @@ -472,29 +472,24 @@ class FlatFileDatabaseManagerTest { } private long getExpectedSuperAbilityDATS(@NotNull SuperAbilityType superAbilityType) { - switch(superAbilityType) { - case BERSERK: - return expectedBerserkCd; - case SUPER_BREAKER: - return expectedSuperBreakerCd; - case GIGA_DRILL_BREAKER: - return expectedGigaDrillBreakerCd; - case GREEN_TERRA: - return expectedGreenTerraCd; - case SKULL_SPLITTER: - return expectedSkullSplitterCd; - case TREE_FELLER: - return expectedTreeFellerCd; - case SERRATED_STRIKES: - return expectedSerratedStrikesCd; - case BLAST_MINING: - return expectedBlastMiningCd; - } + return switch (superAbilityType) { + case BERSERK -> expectedBerserkCd; + case SUPER_BREAKER -> expectedSuperBreakerCd; + case GIGA_DRILL_BREAKER -> expectedGigaDrillBreakerCd; + case GREEN_TERRA -> expectedGreenTerraCd; + case SKULL_SPLITTER -> expectedSkullSplitterCd; + case SUPER_SHOTGUN -> expectedSuperShotgunCd; + case TREE_FELLER -> expectedTreeFellerCd; + case SERRATED_STRIKES -> expectedSerratedStrikesCd; + case BLAST_MINING -> expectedBlastMiningCd; + case TRIDENTS_SUPER_ABILITY -> expectedTridentSuperCd; + case EXPLOSIVE_SHOT -> expectedExplosiveShotCd; + default -> throw new RuntimeException("Values not defined for super ability please add " + + "values for " + superAbilityType.toString() + " to the test"); + }; - return -1; } - //TODO: Why is this stuff a float? private float getExpectedExperienceHealthyDBEntryOne(@NotNull PrimarySkillType primarySkillType) { switch(primarySkillType) { case ACROBATICS: @@ -505,6 +500,8 @@ class FlatFileDatabaseManagerTest { return expectedExpArchery; case AXES: return expectedExpAxes; + case CROSSBOWS: + return expectedExpCrossbows; case EXCAVATION: return expectedExpExcavation; case FISHING: @@ -522,13 +519,15 @@ class FlatFileDatabaseManagerTest { return expectedExpSwords; case TAMING: return expectedExpTaming; + case TRIDENTS: + return expectedExpTridents; case UNARMED: return expectedExpUnarmed; case WOODCUTTING: return expectedExpWoodcutting; } - return -1; + throw new RuntimeException("Values for skill not defined, please add values for " + primarySkillType.toString() + " to the test"); } private int getExpectedLevelHealthyDBEntryOne(@NotNull PrimarySkillType primarySkillType) { @@ -541,6 +540,8 @@ class FlatFileDatabaseManagerTest { return expectedLvlArchery; case AXES: return expectedLvlAxes; + case CROSSBOWS: + return expectedLvlCrossbows; case EXCAVATION: return expectedLvlExcavation; case FISHING: @@ -558,13 +559,15 @@ class FlatFileDatabaseManagerTest { return expectedLvlSwords; case TAMING: return expectedLvlTaming; + case TRIDENTS: + return expectedLvlTridents; case UNARMED: return expectedLvlUnarmed; case WOODCUTTING: return expectedLvlWoodcutting; } - return -1; + throw new RuntimeException("Values for skill not defined, please add values for " + primarySkillType.toString() + " to the test"); } @Test diff --git a/src/test/java/com/gmail/nossr50/database/SQLDatabaseManagerTest.java b/src/test/java/com/gmail/nossr50/database/SQLDatabaseManagerTest.java new file mode 100644 index 000000000..959777ab9 --- /dev/null +++ b/src/test/java/com/gmail/nossr50/database/SQLDatabaseManagerTest.java @@ -0,0 +1,245 @@ +//package com.gmail.nossr50.database; +// +//import com.gmail.nossr50.config.AdvancedConfig; +//import com.gmail.nossr50.config.GeneralConfig; +//import com.gmail.nossr50.datatypes.MobHealthbarType; +//import com.gmail.nossr50.datatypes.player.PlayerProfile; +//import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +//import com.gmail.nossr50.mcMMO; +//import com.gmail.nossr50.util.compat.CompatibilityManager; +//import com.gmail.nossr50.util.platform.MinecraftGameVersion; +//import com.gmail.nossr50.util.skills.SkillTools; +//import com.gmail.nossr50.util.upgrade.UpgradeManager; +//import org.bukkit.entity.Player; +//import org.jetbrains.annotations.NotNull; +//import org.junit.jupiter.api.*; +//import org.mockito.MockedStatic; +//import org.mockito.Mockito; +// +//import java.util.logging.Logger; +// +//import static org.junit.jupiter.api.Assertions.*; +//import static org.mockito.ArgumentMatchers.any; +//import static org.mockito.Mockito.when; +// +//class SQLDatabaseManagerTest { +// private final static @NotNull Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); +// static MockedStatic mockedMcMMO; +// SQLDatabaseManager sqlDatabaseManager; +// static GeneralConfig generalConfig; +// static AdvancedConfig advancedConfig; +// static UpgradeManager upgradeManager; +// static CompatibilityManager compatibilityManager; +// static SkillTools skillTools; +// +// @BeforeAll +// static void setUpAll() { +// // stub mcMMO.p +// mockedMcMMO = Mockito.mockStatic(mcMMO.class); +// mcMMO.p = Mockito.mock(mcMMO.class); +// when(mcMMO.p.getLogger()).thenReturn(logger); +// +// // general config mock +// mockGeneralConfig(); +// +// // advanced config mock +// advancedConfig = Mockito.mock(AdvancedConfig.class); +// when(mcMMO.p.getAdvancedConfig()).thenReturn(advancedConfig); +// +// // starting level +// when(mcMMO.p.getAdvancedConfig().getStartingLevel()).thenReturn(0); +// +// // wire skill tools +// skillTools = new SkillTools(mcMMO.p); +// when(mcMMO.p.getSkillTools()).thenReturn(skillTools); +// +// // compatibility manager mock +// compatibilityManager = Mockito.mock(CompatibilityManager.class); +// when(mcMMO.getCompatibilityManager()).thenReturn(compatibilityManager); +// when(compatibilityManager.getMinecraftGameVersion()).thenReturn(new MinecraftGameVersion(1, 20, 4)); +// +// // upgrade manager mock +// upgradeManager = Mockito.mock(UpgradeManager.class); +// when(mcMMO.getUpgradeManager()).thenReturn(upgradeManager); +// +// // don't trigger upgrades +// when(mcMMO.getUpgradeManager().shouldUpgrade(any())).thenReturn(false); +// } +// +// private static void mockGeneralConfig() { +// generalConfig = Mockito.mock(GeneralConfig.class); +// when(generalConfig.getLocale()).thenReturn("en_US"); +// when(mcMMO.p.getGeneralConfig()).thenReturn(generalConfig); +// +// // max pool size +// when(mcMMO.p.getGeneralConfig().getMySQLMaxPoolSize(SQLDatabaseManager.PoolIdentifier.MISC)) +// .thenReturn(10); +// when(mcMMO.p.getGeneralConfig().getMySQLMaxPoolSize(SQLDatabaseManager.PoolIdentifier.LOAD)) +// .thenReturn(20); +// when(mcMMO.p.getGeneralConfig().getMySQLMaxPoolSize(SQLDatabaseManager.PoolIdentifier.SAVE)) +// .thenReturn(20); +// +// // max connections +// when(mcMMO.p.getGeneralConfig().getMySQLMaxConnections(SQLDatabaseManager.PoolIdentifier.MISC)) +// .thenReturn(30); +// when(mcMMO.p.getGeneralConfig().getMySQLMaxConnections(SQLDatabaseManager.PoolIdentifier.LOAD)) +// .thenReturn(30); +// when(mcMMO.p.getGeneralConfig().getMySQLMaxConnections(SQLDatabaseManager.PoolIdentifier.SAVE)) +// .thenReturn(30); +// +// // table prefix +// when(mcMMO.p.getGeneralConfig().getMySQLTablePrefix()).thenReturn("mcmmo_"); +// +// // public key retrieval +// when(mcMMO.p.getGeneralConfig().getMySQLPublicKeyRetrieval()).thenReturn(true); +// +// // debug +// when(mcMMO.p.getGeneralConfig().getMySQLDebug()).thenReturn(true); +// +// // use mysql +// when(mcMMO.p.getGeneralConfig().getUseMySQL()).thenReturn(true); +// +// // use ssl +// when(mcMMO.p.getGeneralConfig().getMySQLSSL()).thenReturn(true); +// +// // username +// when(mcMMO.p.getGeneralConfig().getMySQLUserName()).thenReturn("sa"); +// +// // password +// when(mcMMO.p.getGeneralConfig().getMySQLUserPassword()).thenReturn(""); +// +// // host +// when(mcMMO.p.getGeneralConfig().getMySQLServerName()).thenReturn("localhost"); +// +// // unused mob health bar thingy +// when(mcMMO.p.getGeneralConfig().getMobHealthbarDefault()).thenReturn(MobHealthbarType.HEARTS); +// } +// +// @BeforeEach +// void setUp() { +// assertNull(sqlDatabaseManager); +// sqlDatabaseManager = new SQLDatabaseManager(logger, "org.h2.Driver", true); +// } +// +// @AfterEach +// void tearDown() { +// sqlDatabaseManager = null; +// } +// +// @AfterAll +// static void tearDownAll() { +// mockedMcMMO.close(); +// } +// +// @Test +// void testGetConnectionMisc() throws Exception { +// assertNotNull(sqlDatabaseManager.getConnection(SQLDatabaseManager.PoolIdentifier.MISC)); +// } +// +// @Test +// void testGetConnectionLoad() throws Exception { +// assertNotNull(sqlDatabaseManager.getConnection(SQLDatabaseManager.PoolIdentifier.LOAD)); +// } +// +// @Test +// void testGetConnectionSave() throws Exception { +// assertNotNull(sqlDatabaseManager.getConnection(SQLDatabaseManager.PoolIdentifier.SAVE)); +// } +// +// @Test +// void testNewUser() { +// Player player = Mockito.mock(Player.class); +// when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); +// when(player.getName()).thenReturn("nossr50"); +// sqlDatabaseManager.newUser(player); +// } +// +// @Test +// void testNewUserGetSkillLevel() { +// Player player = Mockito.mock(Player.class); +// when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); +// when(player.getName()).thenReturn("nossr50"); +// PlayerProfile playerProfile = sqlDatabaseManager.newUser(player); +// +// for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { +// assertEquals(0, playerProfile.getSkillLevel(primarySkillType)); +// } +// } +// +// @Test +// void testNewUserGetSkillXpLevel() { +// Player player = Mockito.mock(Player.class); +// when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); +// when(player.getName()).thenReturn("nossr50"); +// PlayerProfile playerProfile = sqlDatabaseManager.newUser(player); +// +// for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { +// assertEquals(0, playerProfile.getSkillXpLevel(primarySkillType)); +// } +// } +// +// @Test +// void testSaveSkillLevelValues() { +// Player player = Mockito.mock(Player.class); +// when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); +// when(player.getName()).thenReturn("nossr50"); +// PlayerProfile playerProfile = sqlDatabaseManager.newUser(player); +// +// // Validate values are starting from zero +// for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { +// assertEquals(0, playerProfile.getSkillXpLevel(primarySkillType)); +// } +// +// // Change values +// for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { +// playerProfile.modifySkill(primarySkillType, 1 + primarySkillType.ordinal()); +// } +// +// boolean saveSuccess = sqlDatabaseManager.saveUser(playerProfile); +// assertTrue(saveSuccess); +// +// PlayerProfile retrievedUser = sqlDatabaseManager.loadPlayerProfile(player.getName()); +// +// // Check that values got saved +// for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { +// if (primarySkillType == PrimarySkillType.SALVAGE || primarySkillType == PrimarySkillType.SMELTING) { +// // Child skills are not saved, but calculated +// continue; +// } +// +// assertEquals(1 + primarySkillType.ordinal(), retrievedUser.getSkillLevel(primarySkillType)); +// } +// } +// +// @Test +// void testSaveSkillXpValues() { +// Player player = Mockito.mock(Player.class); +// when(player.getUniqueId()).thenReturn(java.util.UUID.randomUUID()); +// when(player.getName()).thenReturn("nossr50"); +// PlayerProfile playerProfile = sqlDatabaseManager.newUser(player); +// +// // Validate values are starting from zero +// for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { +// assertEquals(0, playerProfile.getSkillXpLevel(primarySkillType)); +// } +// +// // Change values +// for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { +// playerProfile.setSkillXpLevel(primarySkillType, 1 + primarySkillType.ordinal()); +// } +// +// sqlDatabaseManager.saveUser(playerProfile); +// +// PlayerProfile retrievedUser = sqlDatabaseManager.loadPlayerProfile(player.getName()); +// +// // Check that values got saved +// for (PrimarySkillType primarySkillType : PrimarySkillType.values()) { +// if (primarySkillType == PrimarySkillType.SALVAGE || primarySkillType == PrimarySkillType.SMELTING) { +// // Child skills are not saved, but calculated +// continue; +// } +// +// assertEquals(1 + primarySkillType.ordinal(), retrievedUser.getSkillXpLevel(primarySkillType)); +// } +// } +//} diff --git a/src/test/java/com/gmail/nossr50/locale/LocaleLoaderTest.java b/src/test/java/com/gmail/nossr50/locale/LocaleLoaderTest.java new file mode 100644 index 000000000..d7db4bee3 --- /dev/null +++ b/src/test/java/com/gmail/nossr50/locale/LocaleLoaderTest.java @@ -0,0 +1,76 @@ +package com.gmail.nossr50.locale; + +import org.bukkit.ChatColor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class LocaleLoaderTest { + + @BeforeEach + void setUp() { + } + + @AfterEach + void tearDown() { + } + + @ParameterizedTest + @ValueSource(strings = {"§cTest", "[[RED]]Test"}) + void addColorsShouldAddColorRed(String testString) { + // When + final String result = LocaleLoader.addColors(testString); + + // Then + assertThat(result).isEqualTo(ChatColor.RED + "Test"); + } + + // hex colors test + @Test + void translateHexColorCodesShouldAddRed() { + // Given + final String testString = "&#FF0000Test"; + + // When + final String result = LocaleLoader.translateHexColorCodes(testString); + + // Then + final String expectedResult = "§x§F§F§0§0§0§0Test"; + assertThat(result).isEqualTo(expectedResult); + } + + @Test + void reverseTranslateHexColorCodesShouldRemoveRed() { + // Given + final String testString = "§x§F§F§0§0§0§0Test"; + + // When + final String result = LocaleLoader.reverseTranslateHexColorCodes(testString); + + // Then + final String expectedResult = "&#FF0000Test"; + assertThat(result).isEqualTo(expectedResult); + } + + @ParameterizedTest + @ValueSource(strings = {"&#FF0000Te�FFst", "&#FF0000Te[[RED]]st", "[[BLUE]]Te[[RED]]st", "§9Te§cst"}) + void addColorsShouldAddRedAndBlue(String testString) { + // When + final String result = LocaleLoader.addColors(testString); + + // TODO: Hacky, clean this up sometime in the future + // Then + // All legal representations of the same string + final List expectedResults = List.of("§x§F§F§0§0§0§0Te§x§0§0§0§0§F§Fst", + "§x§F§F§0§0§0§0Te§x§0§0§0§0§F§Fst", + "§x§F§F§0§0§0§0Te§cst", + "§9Te§cst"); + assertThat(expectedResults).contains(result); + } +} \ No newline at end of file diff --git a/src/test/java/com/gmail/nossr50/party/PartyManagerTest.java b/src/test/java/com/gmail/nossr50/party/PartyManagerTest.java new file mode 100644 index 000000000..0d8f01ccb --- /dev/null +++ b/src/test/java/com/gmail/nossr50/party/PartyManagerTest.java @@ -0,0 +1,97 @@ +package com.gmail.nossr50.party; + +import com.gmail.nossr50.MMOTestEnvironment; +import com.gmail.nossr50.datatypes.player.McMMOPlayer; +import com.gmail.nossr50.mcMMO; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.UUID; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class PartyManagerTest extends MMOTestEnvironment { + private static final Logger logger = Logger.getLogger(PartyManagerTest.class.getName()); + + @BeforeEach + public void setUp() { + mockBaseEnvironment(logger); + + // currently unnecessary, but may be needed for future tests + Mockito.when(partyConfig.isPartyEnabled()).thenReturn(true); + } + + @AfterEach + public void tearDown() { + cleanupBaseEnvironment(); + + // disable parties in config for other tests + Mockito.when(partyConfig.isPartyEnabled()).thenReturn(false); + } + + @Test + public void createPartyWithoutPasswordShouldSucceed() { + // Given + PartyManager partyManager = new PartyManager(mcMMO.p); + String partyName = "TestParty"; + + Player player = mock(Player.class); + McMMOPlayer mmoPlayer = mock(McMMOPlayer.class); + when(mmoPlayer.getPlayer()).thenReturn(player); + when(player.getUniqueId()).thenReturn(new UUID(0, 0)); + + // When & Then + partyManager.createParty(mmoPlayer, partyName, null); + } + + @Test + public void createPartyWithPasswordShouldSucceed() { + // Given + PartyManager partyManager = new PartyManager(mcMMO.p); + String partyName = "TestParty"; + String partyPassword = "somePassword"; + + Player player = mock(Player.class); + McMMOPlayer mmoPlayer = mock(McMMOPlayer.class); + when(mmoPlayer.getPlayer()).thenReturn(player); + when(player.getUniqueId()).thenReturn(new UUID(0, 0)); + + // When & Then + partyManager.createParty(mmoPlayer, partyName, partyPassword); + } + + @Test + public void createPartyWithoutNameShouldFail() { + // Given + PartyManager partyManager = new PartyManager(mcMMO.p); + String partyPassword = "somePassword"; + + Player player = mock(Player.class); + McMMOPlayer mmoPlayer = mock(McMMOPlayer.class); + when(mmoPlayer.getPlayer()).thenReturn(player); + when(player.getUniqueId()).thenReturn(new UUID(0, 0)); + + // When & Then + assertThrows(NullPointerException.class, + () -> partyManager.createParty(mmoPlayer, null, partyPassword)); + } + + @Test + public void createPartyWithoutPlayerShouldFail() { + // Given + PartyManager partyManager = new PartyManager(mcMMO.p); + String partyName = "TestParty"; + String partyPassword = "somePassword"; + + // When & Then + assertThrows(NullPointerException.class, + () -> partyManager.createParty(null, partyName, partyPassword)); + } + +} \ No newline at end of file diff --git a/src/test/java/com/gmail/nossr50/skills/excavation/ExcavationTest.java b/src/test/java/com/gmail/nossr50/skills/excavation/ExcavationTest.java new file mode 100644 index 000000000..631696f0a --- /dev/null +++ b/src/test/java/com/gmail/nossr50/skills/excavation/ExcavationTest.java @@ -0,0 +1,120 @@ +package com.gmail.nossr50.skills.excavation; + +import com.gmail.nossr50.MMOTestEnvironment; +import com.gmail.nossr50.api.exceptions.InvalidSkillException; +import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +import com.gmail.nossr50.datatypes.skills.SubSkillType; +import com.gmail.nossr50.datatypes.treasure.ExcavationTreasure; +import com.gmail.nossr50.util.skills.RankUtils; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class ExcavationTest extends MMOTestEnvironment { + private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(ExcavationTest.class.getName()); + + + @BeforeEach + void setUp() throws InvalidSkillException { + mockBaseEnvironment(logger); + when(rankConfig.getSubSkillUnlockLevel(SubSkillType.EXCAVATION_ARCHAEOLOGY, 1)).thenReturn(1); + when(rankConfig.getSubSkillUnlockLevel(SubSkillType.EXCAVATION_GIGA_DRILL_BREAKER, 1)).thenReturn(1); + + // wire advanced config + + when(RankUtils.getRankUnlockLevel(SubSkillType.EXCAVATION_ARCHAEOLOGY, 1)).thenReturn(1); // needed? + when(RankUtils.getRankUnlockLevel(SubSkillType.EXCAVATION_GIGA_DRILL_BREAKER, 1)).thenReturn(1); // needed? + when(RankUtils.hasReachedRank(eq(1), any(Player.class), eq(SubSkillType.EXCAVATION_ARCHAEOLOGY))).thenReturn(true); + when(RankUtils.hasReachedRank(eq(1), any(Player.class), eq(SubSkillType.EXCAVATION_GIGA_DRILL_BREAKER))).thenReturn(true); + + // setup player and player related mocks after everything else + this.player = Mockito.mock(Player.class); + when(player.getUniqueId()).thenReturn(playerUUID); + + // wire inventory + this.playerInventory = Mockito.mock(PlayerInventory.class); + this.itemInMainHand = new ItemStack(Material.DIAMOND_SHOVEL); + when(player.getInventory()).thenReturn(playerInventory); + when(playerInventory.getItemInMainHand()).thenReturn(itemInMainHand); + + // Set up spy for Excavation Manager + + } + + @AfterEach + void tearDown() { + cleanupBaseEnvironment(); + } + + @Test + void excavationShouldHaveTreasureDrops() { + mmoPlayer.modifySkill(PrimarySkillType.EXCAVATION, 1000); + + // Wire block + BlockState blockState = Mockito.mock(BlockState.class); + BlockData blockData = Mockito.mock(BlockData.class); + Block block = Mockito.mock(Block.class); + when(blockState.getBlockData()).thenReturn(blockData); + when(blockState.getType()).thenReturn(Material.SAND); + when(blockData.getMaterial()).thenReturn(Material.SAND); + when(blockState.getBlock()).thenReturn(block); + when(blockState.getBlock().getDrops(any())).thenReturn(null); + + ExcavationManager excavationManager = Mockito.spy(new ExcavationManager(mmoPlayer)); + doReturn(getGuaranteedTreasureDrops()).when(excavationManager).getTreasures(blockState); + excavationManager.excavationBlockCheck(blockState); + + // verify ExcavationManager.processExcavationBonusesOnBlock was called + verify(excavationManager, atLeastOnce()).processExcavationBonusesOnBlock(any(BlockState.class), any(ExcavationTreasure.class), any(Location.class)); + } + + @Test + void excavationShouldNotDropTreasure() { + mmoPlayer.modifySkill(PrimarySkillType.EXCAVATION, 1000); + + // Wire block + BlockState blockState = Mockito.mock(BlockState.class); + BlockData blockData = Mockito.mock(BlockData.class); + Block block = Mockito.mock(Block.class); + when(blockState.getBlockData()).thenReturn(blockData); + when(blockState.getType()).thenReturn(Material.SAND); + when(blockData.getMaterial()).thenReturn(Material.SAND); + when(blockState.getBlock()).thenReturn(block); + when(blockState.getBlock().getDrops(any())).thenReturn(null); + + ExcavationManager excavationManager = Mockito.spy(new ExcavationManager(mmoPlayer)); + doReturn(getImpossibleTreasureDrops()).when(excavationManager).getTreasures(blockState); + excavationManager.excavationBlockCheck(blockState); + + // verify ExcavationManager.processExcavationBonusesOnBlock was called + verify(excavationManager, never()).processExcavationBonusesOnBlock(any(BlockState.class), any(ExcavationTreasure.class), any(Location.class)); + } + + private List getGuaranteedTreasureDrops() { + List treasures = new ArrayList<>();; + treasures.add(new ExcavationTreasure(new ItemStack(Material.CAKE), 1, 100, 1)); + return treasures; + } + + private List getImpossibleTreasureDrops() { + List treasures = new ArrayList<>();; + treasures.add(new ExcavationTreasure(new ItemStack(Material.CAKE), 1, 0, 1)); + return treasures; + } +} diff --git a/src/test/java/com/gmail/nossr50/skills/tridents/TridentsTest.java b/src/test/java/com/gmail/nossr50/skills/tridents/TridentsTest.java new file mode 100644 index 000000000..199c8e554 --- /dev/null +++ b/src/test/java/com/gmail/nossr50/skills/tridents/TridentsTest.java @@ -0,0 +1,39 @@ +package com.gmail.nossr50.skills.tridents; + +import com.gmail.nossr50.MMOTestEnvironment; +import com.gmail.nossr50.api.exceptions.InvalidSkillException; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; + +class TridentsTest extends MMOTestEnvironment { + private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(TridentsTest.class.getName()); + + TridentsManager tridentsManager; + ItemStack trident; + @BeforeEach + void setUp() throws InvalidSkillException { + mockBaseEnvironment(logger); + + // setup player and player related mocks after everything else + this.player = Mockito.mock(Player.class); + Mockito.when(player.getUniqueId()).thenReturn(playerUUID); + + // wire inventory + this.playerInventory = Mockito.mock(PlayerInventory.class); + this.trident = new ItemStack(Material.TRIDENT); + Mockito.when(playerInventory.getItemInMainHand()).thenReturn(trident); + + // Set up spy for manager + tridentsManager = Mockito.spy(new TridentsManager(mmoPlayer)); + } + + @AfterEach + void tearDown() { + cleanupBaseEnvironment(); + } +} diff --git a/src/test/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingTest.java b/src/test/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingTest.java new file mode 100644 index 000000000..6e533fbe8 --- /dev/null +++ b/src/test/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingTest.java @@ -0,0 +1,109 @@ +package com.gmail.nossr50.skills.woodcutting; + +import com.gmail.nossr50.MMOTestEnvironment; +import com.gmail.nossr50.api.exceptions.InvalidSkillException; +import com.gmail.nossr50.config.experience.ExperienceConfig; +import com.gmail.nossr50.datatypes.skills.PrimarySkillType; +import com.gmail.nossr50.datatypes.skills.SubSkillType; +import com.gmail.nossr50.util.skills.RankUtils; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; + +class WoodcuttingTest extends MMOTestEnvironment { + private static final java.util.logging.Logger logger = java.util.logging.Logger.getLogger(WoodcuttingTest.class.getName()); + + WoodcuttingManager woodcuttingManager; + @BeforeEach + void setUp() throws InvalidSkillException { + mockBaseEnvironment(logger); + Mockito.when(rankConfig.getSubSkillUnlockLevel(SubSkillType.WOODCUTTING_HARVEST_LUMBER, 1)).thenReturn(1); + + // wire advanced config + Mockito.when(advancedConfig.getMaximumProbability(SubSkillType.WOODCUTTING_HARVEST_LUMBER)).thenReturn(100D); + Mockito.when(advancedConfig.getMaximumProbability(SubSkillType.WOODCUTTING_CLEAN_CUTS)).thenReturn(10D); + Mockito.when(advancedConfig.getMaxBonusLevel(SubSkillType.WOODCUTTING_HARVEST_LUMBER)).thenReturn(1000); + Mockito.when(advancedConfig.getMaxBonusLevel(SubSkillType.WOODCUTTING_CLEAN_CUTS)).thenReturn(10000); + + Mockito.when(RankUtils.getRankUnlockLevel(SubSkillType.WOODCUTTING_HARVEST_LUMBER, 1)).thenReturn(1); // needed? + Mockito.when(RankUtils.getRankUnlockLevel(SubSkillType.WOODCUTTING_CLEAN_CUTS, 1)).thenReturn(1000); // needed? + Mockito.when(RankUtils.hasReachedRank(eq(1), any(Player.class), eq(SubSkillType.WOODCUTTING_HARVEST_LUMBER))).thenReturn(true); + Mockito.when(RankUtils.hasReachedRank(eq(1), any(Player.class), eq(SubSkillType.WOODCUTTING_CLEAN_CUTS))).thenReturn(true); + + // setup player and player related mocks after everything else + this.player = Mockito.mock(Player.class); + Mockito.when(player.getUniqueId()).thenReturn(playerUUID); + + // wire inventory + this.playerInventory = Mockito.mock(PlayerInventory.class); + this.itemInMainHand = new ItemStack(Material.DIAMOND_AXE); + Mockito.when(player.getInventory()).thenReturn(playerInventory); + Mockito.when(playerInventory.getItemInMainHand()).thenReturn(itemInMainHand); + + // Set up spy for WoodcuttingManager + woodcuttingManager = Mockito.spy(new WoodcuttingManager(mmoPlayer)); + } + + @AfterEach + void tearDown() { + cleanupBaseEnvironment(); + } + + @Test + void harvestLumberShouldDoubleDrop() { + mmoPlayer.modifySkill(PrimarySkillType.WOODCUTTING, 1000); + + BlockState blockState = Mockito.mock(BlockState.class); + Block block = Mockito.mock(Block.class); + // wire block + Mockito.when(blockState.getBlock()).thenReturn(block); + + Mockito.when(blockState.getBlock().getDrops(any())).thenReturn(null); + Mockito.when(blockState.getType()).thenReturn(Material.OAK_LOG); + woodcuttingManager.processBonusDropCheck(blockState); + + // verify bonus drops were spawned + // TODO: using at least once since triple drops can also happen + // TODO: Change the test env to disallow triple drop in the future + Mockito.verify(woodcuttingManager, Mockito.atLeastOnce()).spawnHarvestLumberBonusDrops(blockState); + } + + @Test + void harvestLumberShouldNotDoubleDrop() { + mmoPlayer.modifySkill(PrimarySkillType.WOODCUTTING, 0); + + BlockState blockState = Mockito.mock(BlockState.class); + Block block = Mockito.mock(Block.class); + // wire block + Mockito.when(blockState.getBlock()).thenReturn(block); + + Mockito.when(blockState.getBlock().getDrops(any())).thenReturn(null); + Mockito.when(blockState.getType()).thenReturn(Material.OAK_LOG); + woodcuttingManager.processBonusDropCheck(blockState); + + // verify bonus drops were not spawned + Mockito.verify(woodcuttingManager, Mockito.times(0)).spawnHarvestLumberBonusDrops(blockState); + } + + @Test + void testProcessWoodcuttingBlockXP() { + BlockState targetBlock = Mockito.mock(BlockState.class); + Mockito.when(targetBlock.getType()).thenReturn(Material.OAK_LOG); + // wire XP + Mockito.when(ExperienceConfig.getInstance().getXp(PrimarySkillType.WOODCUTTING, Material.OAK_LOG)).thenReturn(5); + + // Verify XP increased by 5 when processing XP + woodcuttingManager.processWoodcuttingBlockXP(targetBlock); + Mockito.verify(mmoPlayer, Mockito.times(1)).beginXpGain(eq(PrimarySkillType.WOODCUTTING), eq(5F), any(), any()); + } +} diff --git a/src/test/java/com/gmail/nossr50/util/random/ProbabilityTest.java b/src/test/java/com/gmail/nossr50/util/random/ProbabilityTest.java new file mode 100644 index 000000000..e114313ba --- /dev/null +++ b/src/test/java/com/gmail/nossr50/util/random/ProbabilityTest.java @@ -0,0 +1,105 @@ +package com.gmail.nossr50.util.random; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class ProbabilityTest { + + private static Stream provideProbabilitiesForWithinExpectations() { + return Stream.of( + // static probability, % of time for success + Arguments.of(new ProbabilityImpl(.05), 5), + Arguments.of(new ProbabilityImpl(.10), 10), + Arguments.of(new ProbabilityImpl(.15), 15), + Arguments.of(new ProbabilityImpl(.20), 20), + Arguments.of(new ProbabilityImpl(.25), 25), + Arguments.of(new ProbabilityImpl(.50), 50), + Arguments.of(new ProbabilityImpl(.75), 75), + Arguments.of(new ProbabilityImpl(.90), 90), + Arguments.of(new ProbabilityImpl(.999), 99.9), + Arguments.of(new ProbabilityImpl(0.0005), 0.05), + Arguments.of(new ProbabilityImpl(0.001), 0.1), + Arguments.of(new ProbabilityImpl(50.0), 100), + Arguments.of(new ProbabilityImpl(100.0), 100) + ); + } + + private static Stream provideOfPercentageProbabilitiesForWithinExpectations() { + return Stream.of( + // static probability, % of time for success + Arguments.of(Probability.ofPercent(5), 5), + Arguments.of(Probability.ofPercent(10), 10), + Arguments.of(Probability.ofPercent(15), 15), + Arguments.of(Probability.ofPercent(20), 20), + Arguments.of(Probability.ofPercent(25), 25), + Arguments.of(Probability.ofPercent(50), 50), + Arguments.of(Probability.ofPercent(75), 75), + Arguments.of(Probability.ofPercent(90), 90), + Arguments.of(Probability.ofPercent(99.9), 99.9), + Arguments.of(Probability.ofPercent(0.05), 0.05), + Arguments.of(Probability.ofPercent(0.1), 0.1), + Arguments.of(Probability.ofPercent(500), 100), + Arguments.of(Probability.ofPercent(1000), 100) + ); + } + @Test + void testAlwaysWinConstructor() { + for (int i = 0; i < 100000; i++) { + assertTrue(new ProbabilityImpl(100).evaluate()); + } + } + + @Test + void testAlwaysLoseConstructor() { + for (int i = 0; i < 100000; i++) { + assertFalse(new ProbabilityImpl(0).evaluate()); + } + } + + @Test + void testAlwaysWinOfPercent() { + for (int i = 0; i < 100000; i++) { + assertTrue(Probability.ofPercent(100).evaluate()); + } + } + + @Test + void testAlwaysLoseOfPercent() { + for (int i = 0; i < 100000; i++) { + assertFalse(Probability.ofPercent(0).evaluate()); + } + } + + @ParameterizedTest + @MethodSource("provideProbabilitiesForWithinExpectations") + void testOddsExpectationsConstructor(Probability probability, double expectedWinPercent) { + assertExpectations(probability, expectedWinPercent); + } + + @ParameterizedTest + @MethodSource("provideOfPercentageProbabilitiesForWithinExpectations") + void testOddsExpectationsOfPercent(Probability probability, double expectedWinPercent) { + assertExpectations(probability, expectedWinPercent); + } + + private static void assertExpectations(Probability probability, double expectedWinPercent) { + double iterations = 2.0e7; + double winCount = 0; + + for (int i = 0; i < iterations; i++) { + if(probability.evaluate()) { + winCount++; + } + } + + double successPercent = (winCount / iterations) * 100; + System.out.println(successPercent + ", " + expectedWinPercent); + assertEquals(expectedWinPercent, successPercent, 0.05D); + } +} diff --git a/src/test/java/com/gmail/nossr50/util/random/ProbabilityTestUtils.java b/src/test/java/com/gmail/nossr50/util/random/ProbabilityTestUtils.java new file mode 100644 index 000000000..e97119be6 --- /dev/null +++ b/src/test/java/com/gmail/nossr50/util/random/ProbabilityTestUtils.java @@ -0,0 +1,22 @@ +package com.gmail.nossr50.util.random; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ProbabilityTestUtils { + public static void assertProbabilityExpectations(double expectedWinPercent, Probability probability) { + double iterations = 2.0e7; //20 million + double winCount = 0; + for (int i = 0; i < iterations; i++) { + if(probability.evaluate()) { + winCount++; + } + } + + double successPercent = (winCount / iterations) * 100; + System.out.println("Wins: " + winCount); + System.out.println("Fails: " + (iterations - winCount)); + System.out.println("Percentage succeeded: " + successPercent + ", Expected: " + expectedWinPercent); + assertEquals(expectedWinPercent, successPercent, 0.025D); + System.out.println("Variance is within tolerance levels!"); + } +} diff --git a/src/test/java/com/gmail/nossr50/util/random/ProbabilityUtilTest.java b/src/test/java/com/gmail/nossr50/util/random/ProbabilityUtilTest.java new file mode 100644 index 000000000..4994f8053 --- /dev/null +++ b/src/test/java/com/gmail/nossr50/util/random/ProbabilityUtilTest.java @@ -0,0 +1,75 @@ +package com.gmail.nossr50.util.random; + +import com.gmail.nossr50.MMOTestEnvironment; +import com.gmail.nossr50.datatypes.skills.SubSkillType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.logging.Logger; +import java.util.stream.Stream; + +import static com.gmail.nossr50.datatypes.skills.SubSkillType.*; +import static com.gmail.nossr50.util.random.ProbabilityTestUtils.assertProbabilityExpectations; +import static com.gmail.nossr50.util.random.ProbabilityUtil.calculateCurrentSkillProbability; +import static java.util.logging.Logger.getLogger; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +class ProbabilityUtilTest extends MMOTestEnvironment { + private static final Logger logger = getLogger(ProbabilityUtilTest.class.getName()); + + final static double impactChance = 11D; + final static double greaterImpactChance = 0.007D; + final static double fastFoodChance = 45.5D; + + @BeforeEach + public void setupMocks() { + mockBaseEnvironment(logger); + when(advancedConfig.getImpactChance()).thenReturn(impactChance); + when(advancedConfig.getGreaterImpactChance()).thenReturn(greaterImpactChance); + when(advancedConfig.getFastFoodChance()).thenReturn(fastFoodChance); + } + + @AfterEach + public void tearDown() { + cleanupBaseEnvironment(); + } + + private static Stream staticChanceSkills() { + return Stream.of( + // static probability, % of time for success + Arguments.of(AXES_ARMOR_IMPACT, impactChance), + Arguments.of(AXES_GREATER_IMPACT, greaterImpactChance), + Arguments.of(TAMING_FAST_FOOD_SERVICE, fastFoodChance) + ); + } + + @ParameterizedTest + @MethodSource("staticChanceSkills") + void staticChanceSkillsShouldSucceedAsExpected(SubSkillType subSkillType, double expectedWinPercent) + throws InvalidStaticChance { + Probability staticRandomChance = ProbabilityUtil.getStaticRandomChance(subSkillType); + assertProbabilityExpectations(expectedWinPercent, staticRandomChance); + } + + @Test + public void isSkillRNGSuccessfulShouldBehaveAsExpected() { + // Given + when(advancedConfig.getMaximumProbability(UNARMED_ARROW_DEFLECT)).thenReturn(20D); + when(advancedConfig.getMaxBonusLevel(UNARMED_ARROW_DEFLECT)).thenReturn(0); + + final Probability probability = ProbabilityUtil.getSkillProbability(UNARMED_ARROW_DEFLECT, player); + assertEquals(0.2D, probability.getValue()); + assertProbabilityExpectations(20, probability); + } + + @Test + public void calculateCurrentSkillProbabilityShouldBeTwenty() { + final Probability probability = calculateCurrentSkillProbability(1000, 0, 20, 1000); + assertEquals(0.2D, probability.getValue()); + } +} diff --git a/src/test/java/com/gmail/nossr50/util/random/RandomChanceTest.java b/src/test/java/com/gmail/nossr50/util/random/RandomChanceTest.java deleted file mode 100644 index f28e7e842..000000000 --- a/src/test/java/com/gmail/nossr50/util/random/RandomChanceTest.java +++ /dev/null @@ -1,116 +0,0 @@ -//package com.gmail.nossr50.util.random; -// -//import com.gmail.nossr50.datatypes.player.McMMOPlayer; -//import com.gmail.nossr50.datatypes.skills.PrimarySkillType; -//import com.gmail.nossr50.datatypes.skills.SubSkillType; -//import com.gmail.nossr50.util.Permissions; -//import com.gmail.nossr50.util.player.UserManager; -//import org.bukkit.entity.Player; -//import org.jetbrains.annotations.NotNull; -//import org.junit.Assert; -//import org.junit.Before; -//import org.junit.Test; -//import org.junit.runner.RunWith; -//import org.mockito.Mockito; -//import org.powermock.api.mockito.PowerMockito; -//import org.powermock.core.classloader.annotations.PrepareForTest; -//import org.powermock.modules.junit4.PowerMockRunner; -// -//import static org.mockito.Mockito.mock; -// -////TODO: Rewrite the entire com.gmail.nossr50.util.random package, it was written in haste and it disgusts me -////TODO: Add more tests for the other types of random dice rolls -//@RunWith(PowerMockRunner.class) -//@PrepareForTest({RandomChanceUtil.class, UserManager.class}) -//public class RandomChanceTest { -// -// private Player luckyPlayer; -// private McMMOPlayer mmoPlayerLucky; -// -// private Player normalPlayer; -// private McMMOPlayer mmoPlayerNormal; -// -// private SubSkillType subSkillType; -// private PrimarySkillType primarySkillType; -// -// private final String testASCIIHeader = "---- mcMMO Tests ----"; -// -// @Before -// public void setUpMock() { -// primarySkillType = PrimarySkillType.HERBALISM; -// subSkillType = SubSkillType.HERBALISM_GREEN_THUMB; -// -// //TODO: Likely needs to be changed per skill if more tests were added -// PowerMockito.stub(PowerMockito.method(RandomChanceUtil.class, "getMaximumProbability", subSkillType.getClass())).toReturn(100D); -// PowerMockito.stub(PowerMockito.method(RandomChanceUtil.class, "getMaxBonusLevelCap", subSkillType.getClass())).toReturn(1000D); -// -// normalPlayer = mock(Player.class); -// luckyPlayer = mock(Player.class); -// -// mmoPlayerNormal = mock(McMMOPlayer.class); -// mmoPlayerLucky = mock(McMMOPlayer.class); -// -// PowerMockito.mockStatic(UserManager.class); -// Mockito.when(UserManager.getPlayer(normalPlayer)).thenReturn(mmoPlayerNormal); -// Mockito.when(UserManager.getPlayer(luckyPlayer)).thenReturn(mmoPlayerLucky); -// -// Mockito.when(mmoPlayerNormal.getPlayer()).thenReturn(normalPlayer); -// Mockito.when(mmoPlayerLucky.getPlayer()).thenReturn(luckyPlayer); -// -// //Lucky player has the lucky permission -// //Normal player doesn't have any lucky permission -// Mockito.when(Permissions.lucky(luckyPlayer, primarySkillType)).thenReturn(true); -// Mockito.when(Permissions.lucky(normalPlayer, primarySkillType)).thenReturn(false); -// -// Mockito.when(mmoPlayerNormal.getSkillLevel(primarySkillType)).thenReturn(800); -// Mockito.when(mmoPlayerLucky.getSkillLevel(primarySkillType)).thenReturn(800); -// } -// -// @Test -// public void testLuckyChance() { -// System.out.println(testASCIIHeader); -// System.out.println("Testing success odds to fall within expected values..."); -// assertEquals(80D, getSuccessChance(mmoPlayerNormal),0D); -// assertEquals(80D * RandomChanceUtil.LUCKY_MODIFIER, getSuccessChance(mmoPlayerLucky),0D); -// } -// -// @Test -// public void testNeverFailsSuccessLuckyPlayer() { -// System.out.println(testASCIIHeader); -// System.out.println("Test - Lucky Player with 80% base success should never fail (10,000 iterations)"); -// for(int x = 0; x < 10000; x++) { -// Assert.assertTrue(RandomChanceUtil.checkRandomChanceExecutionSuccess(luckyPlayer, SubSkillType.HERBALISM_GREEN_THUMB, true)); -// if(x == 10000-1) -// System.out.println("They never failed!"); -// } -// } -// -// @Test -// public void testFailsAboutExpected() { -// System.out.println(testASCIIHeader); -// System.out.println("Test - Player with 800 skill should fail about 20% of the time (100,000 iterations)"); -// double ratioDivisor = 1000; //1000 because we run the test 100,000 times -// double expectedFailRate = 20D; -// -// double win = 0, loss = 0; -// for(int x = 0; x < 100000; x++) { -// if(RandomChanceUtil.checkRandomChanceExecutionSuccess(normalPlayer, SubSkillType.HERBALISM_GREEN_THUMB, true)) { -// win++; -// } else { -// loss++; -// } -// } -// -// double lossRatio = (loss / ratioDivisor); -// Assert.assertEquals(lossRatio, expectedFailRate, 1D); -// } -// -// private double getSuccessChance(@NotNull McMMOPlayer mmoPlayer) { -// RandomChanceSkill randomChanceSkill = new RandomChanceSkill(mmoPlayer.getPlayer(), subSkillType, true); -// return RandomChanceUtil.calculateChanceOfSuccess(randomChanceSkill); -// } -// -// private void assertEquals(double expected, double actual, double delta) { -// Assert.assertEquals(expected, actual, delta); -// } -//} diff --git a/src/test/java/com/gmail/nossr50/util/skills/SkillToolsTest.java b/src/test/java/com/gmail/nossr50/util/skills/SkillToolsTest.java deleted file mode 100644 index 4a295d5d3..000000000 --- a/src/test/java/com/gmail/nossr50/util/skills/SkillToolsTest.java +++ /dev/null @@ -1,16 +0,0 @@ -//package com.gmail.nossr50.util.skills; -// -//import com.gmail.nossr50.datatypes.skills.PrimarySkillType; -//import com.google.common.collect.ImmutableList; -//import org.junit.Before; -//import org.junit.Test; -//import org.junit.runner.RunWith; -//import org.powermock.core.classloader.annotations.PrepareForTest; -//import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor; -//import org.powermock.modules.junit4.PowerMockRunner; -// -//@RunWith(PowerMockRunner.class) -//@PrepareForTest(SkillTools.class) -//public class SkillToolsTest { -// -//} \ No newline at end of file diff --git a/src/test/java/com/gmail/nossr50/util/text/TextUtilsTest.java b/src/test/java/com/gmail/nossr50/util/text/TextUtilsTest.java index cefbe7010..c58157179 100644 --- a/src/test/java/com/gmail/nossr50/util/text/TextUtilsTest.java +++ b/src/test/java/com/gmail/nossr50/util/text/TextUtilsTest.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; /** * This Unit Test checks if Adventure was set up correctly and works as expected. - * Normally we can rely on this to be the case. However sometimes our dependencies + * Normally, we can rely on this to be the case. However sometimes our dependencies * lack so far behind that things stop working correctly. * This test ensures that basic functionality is guaranteed to work as we would expect. * diff --git a/src/test/resources/healthydb.users b/src/test/resources/healthydb.users index 7ce5ccbad..79a2c7e70 100644 --- a/src/test/resources/healthydb.users +++ b/src/test/resources/healthydb.users @@ -1,3 +1,3 @@ -nossr50:1:IGNORED:IGNORED:10:2:20:3:4:5:6:7:8:9:10:30:40:50:60:70:80:90:100:IGNORED:11:110:111:222:333:444:555:666:777:IGNORED:12:120:888:IGNORED:HEARTS:13:130:588fe472-1c82-4c4e-9aa1-7eefccb277e3:1111:999:2020: -mrfloris:2420:::0:2452:0:1983:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:631e3896-da2a-4077-974b-d047859d76bc:5:1600906906:3030: -powerless:0:::0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0::0:0:0:0:0:0:0:0:0::0:0:0:1337:HEARTS:0:0:e0d07db8-f7e8-43c7-9ded-864dfc6f3b7c:5:1600906906:4040: \ No newline at end of file +nossr50:1:IGNORED:IGNORED:10:2:20:3:4:5:6:7:8:9:10:30:40:50:60:70:80:90:100:IGNORED:11:110:111:222:333:444:555:666:777:IGNORED:12:120:888:IGNORED:HEARTS:13:130:588fe472-1c82-4c4e-9aa1-7eefccb277e3:1111:999:2020:140:14:150:15:1111:2222:3333: +mrfloris:2420:::0:2452:0:1983:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:631e3896-da2a-4077-974b-d047859d76bc:5:1600906906:3030:0:0:0:0:0:0:0: +powerless:0:::0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0::0:0:0:0:0:0:0:0:0::0:0:0:1337:HEARTS:0:0:e0d07db8-f7e8-43c7-9ded-864dfc6f3b7c:5:1600906906:4040:0:0:0:0:0:0:0: \ No newline at end of file diff --git a/src/test/resources/olderdb.users b/src/test/resources/olderdb.users new file mode 100644 index 000000000..7ce5ccbad --- /dev/null +++ b/src/test/resources/olderdb.users @@ -0,0 +1,3 @@ +nossr50:1:IGNORED:IGNORED:10:2:20:3:4:5:6:7:8:9:10:30:40:50:60:70:80:90:100:IGNORED:11:110:111:222:333:444:555:666:777:IGNORED:12:120:888:IGNORED:HEARTS:13:130:588fe472-1c82-4c4e-9aa1-7eefccb277e3:1111:999:2020: +mrfloris:2420:::0:2452:0:1983:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:631e3896-da2a-4077-974b-d047859d76bc:5:1600906906:3030: +powerless:0:::0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0::0:0:0:0:0:0:0:0:0::0:0:0:1337:HEARTS:0:0:e0d07db8-f7e8-43c7-9ded-864dfc6f3b7c:5:1600906906:4040: \ No newline at end of file