diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..2e16cba --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,33 @@ +pipeline { + agent any + tools { + jdk 'JDK17' + } + stages { + stage('Build') { + steps { + echo 'Building...' + sh 'mvn clean & mvn validate & mvn compile' + } + } + stage('Test') { + steps { + echo 'Testing...' + sh 'mvn test' + } + } + stage('Verify') { + steps { + echo 'Verifying...' + sh 'mvn verify -Dmaven.test.skip=true' + } + } + stage('Deploy') { + steps { + echo 'Deploying...' + sh 'mvn deploy -Dmaven.install.skip=true -Dmaven.test.skip=true' + archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true + } + } + } +} \ No newline at end of file diff --git a/README.MD b/README.MD index 0bb4429..610ba4e 100644 --- a/README.MD +++ b/README.MD @@ -14,4 +14,8 @@ basis, and possibly choosing exactly what information to display on each trait's - [Blacksmith](https://www.spigotmc.org/resources/blacksmith.105938/) - [Quests](https://www.spigotmc.org/resources/quests.3711/) - [Sentinel](https://www.spigotmc.org/resources/sentinel.22017/) -- [Minstrel](https://git.knarcraft.net/EpicKnarvik97/Minstrel) \ No newline at end of file +- [Minstrel](https://git.knarcraft.net/EpicKnarvik97/Minstrel) + +## Somewhat supported NPC traits, because the developers have no API + +- [Trader](https://www.spigotmc.org/resources/dtltraders.35890/) \ No newline at end of file diff --git a/pom.xml b/pom.xml index ca25666..e3393cf 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ A plugin for displaying citizens on the dynmap map - 17 + 16 UTF-8 @@ -24,8 +24,8 @@ maven-compiler-plugin 3.8.1 - 17 - 17 + ${java.version} + ${java.version} @@ -40,6 +40,20 @@ false + + + net.knarcraft:knarlib + + net/knarcraft/knarlib/** + + + + + *.MF + *.yml + + + @@ -54,6 +68,10 @@ + + knarcraft-repo + https://git.knarcraft.net/api/packages/EpicKnarvik97/maven + codemc-repo https://repo.codemc.io/repository/maven-public/ @@ -72,19 +90,29 @@ citizens-repo - http://repo.citizensnpcs.co/ + https://repo.citizensnpcs.co/ - vault-repo - http://nexus.hc.to/content/repositories/pub_releases + jitpack.io + https://jitpack.io + + + knarcraft-repo + https://git.knarcraft.net/api/packages/EpicKnarvik97/maven + + + knarcraft-repo + https://git.knarcraft.net/api/packages/EpicKnarvik97/maven + + org.spigotmc spigot-api - 1.19.2-R0.1-SNAPSHOT + 1.19.4-R0.1-SNAPSHOT provided @@ -103,13 +131,13 @@ me.blackvein.quests quests-api - 4.6.0 + 4.8.1 provided me.blackvein.quests quests-core - 4.6.0 + 4.8.1 provided @@ -121,7 +149,7 @@ net.knarcraft blacksmith - 1.0.1-SNAPSHOT + 1.0.4-SNAPSHOT provided @@ -131,11 +159,17 @@ provided - net.milkbowl.vault - Vault - 1.7.3 + com.github.MilkBowl + VaultAPI + 1.7 provided + + net.knarcraft + knarlib + 1.2.3 + compile + org.mcmonkey sentinel diff --git a/src/main/java/net/knarcraft/dynmapcitizens/DynmapCitizens.java b/src/main/java/net/knarcraft/dynmapcitizens/DynmapCitizens.java index 44a9cb6..265f09d 100644 --- a/src/main/java/net/knarcraft/dynmapcitizens/DynmapCitizens.java +++ b/src/main/java/net/knarcraft/dynmapcitizens/DynmapCitizens.java @@ -1,12 +1,18 @@ package net.knarcraft.dynmapcitizens; +import net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage; +import net.knarcraft.dynmapcitizens.formatting.SentinelTranslatableMessage; import net.knarcraft.dynmapcitizens.handler.VaultHandler; import net.knarcraft.dynmapcitizens.handler.trait.BlacksmithHandler; import net.knarcraft.dynmapcitizens.handler.trait.CitizensTraitHandler; +import net.knarcraft.dynmapcitizens.handler.trait.DTLTradersHandler; import net.knarcraft.dynmapcitizens.handler.trait.MinstrelHandler; import net.knarcraft.dynmapcitizens.handler.trait.SentinelHandler; import net.knarcraft.dynmapcitizens.handler.trait.quests.QuestsHandler; import net.knarcraft.dynmapcitizens.settings.GlobalSettings; +import net.knarcraft.knarlib.formatting.StringFormatter; +import net.knarcraft.knarlib.formatting.TranslatableTimeUnit; +import net.knarcraft.knarlib.formatting.Translator; import org.bukkit.Bukkit; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.plugin.Plugin; @@ -18,11 +24,16 @@ import java.util.ArrayList; import java.util.List; import java.util.logging.Level; +/** + * DynmapCitizens' main class + */ @SuppressWarnings("unused") public final class DynmapCitizens extends JavaPlugin { private static DynmapCitizens instance; - private DynmapAPI dynmapAPI; + private static Translator translator; + private static StringFormatter stringFormatter; + private DynmapAPI dynmapAPIInstance; private VaultHandler vaultHandler; private GlobalSettings globalSettings; private List traitHandlers; @@ -30,6 +41,7 @@ public final class DynmapCitizens extends JavaPlugin { @Override public void onEnable() { DynmapCitizens.instance = this; + //Initialize quest and dynmap APIs PluginManager pluginManager = Bukkit.getPluginManager(); Plugin dynmapPlugin = pluginManager.getPlugin("dynmap"); @@ -38,7 +50,7 @@ public final class DynmapCitizens extends JavaPlugin { this.onDisable(); return; } - this.dynmapAPI = dynmapAPI; + this.dynmapAPIInstance = dynmapAPI; this.globalSettings = new GlobalSettings(); FileConfiguration configuration = this.getConfig(); @@ -49,6 +61,14 @@ public final class DynmapCitizens extends JavaPlugin { configuration = this.getConfig(); this.globalSettings.load(configuration); + //Load all messages + translator = new Translator(); + translator.registerMessageCategory(TranslatableTimeUnit.UNIT_SECOND); + translator.registerMessageCategory(QuestsTranslatableMessage.QUESTS_REQUIREMENTS_FORMAT); + translator.registerMessageCategory(SentinelTranslatableMessage.SENTINEL_DESCRIPTION); + translator.loadLanguages(this.getDataFolder(), "en", "en"); + stringFormatter = new StringFormatter(this.getDescription().getName(), translator); + //Initialize all enabled traits initializeTraitHandlers(configuration); @@ -69,6 +89,24 @@ public final class DynmapCitizens extends JavaPlugin { //TODO: Perhaps remove icons, just in case? } + /** + * Gets the translator to use for translation + * + * @return

The translator to use

+ */ + public static Translator getTranslator() { + return translator; + } + + /** + * Gets the string formatter to use for formatting + * + * @return

The string formatter to use

+ */ + public static StringFormatter getFormatter() { + return stringFormatter; + } + /** * Gets the global settings for this plugin * @@ -93,7 +131,7 @@ public final class DynmapCitizens extends JavaPlugin { * @return

A reference to the Dynmap API

*/ public DynmapAPI getDynmapAPI() { - return this.dynmapAPI; + return this.dynmapAPIInstance; } /** @@ -117,6 +155,7 @@ public final class DynmapCitizens extends JavaPlugin { this.traitHandlers.add(new QuestsHandler()); this.traitHandlers.add(new SentinelHandler()); this.traitHandlers.add(new MinstrelHandler()); + this.traitHandlers.add(new DTLTradersHandler()); //Load and initialize all enabled trait handlers for (CitizensTraitHandler handler : this.traitHandlers) { diff --git a/src/main/java/net/knarcraft/dynmapcitizens/formatting/QuestsTranslatableMessage.java b/src/main/java/net/knarcraft/dynmapcitizens/formatting/QuestsTranslatableMessage.java new file mode 100644 index 0000000..1b56b61 --- /dev/null +++ b/src/main/java/net/knarcraft/dynmapcitizens/formatting/QuestsTranslatableMessage.java @@ -0,0 +1,169 @@ +package net.knarcraft.dynmapcitizens.formatting; + +import net.knarcraft.knarlib.formatting.TranslatableMessage; + +/** + * An enum describing all of DynmapCitizens' translatable messages + */ +public enum QuestsTranslatableMessage implements TranslatableMessage { + + /** + * The format for a quest's planner description + * + *

Placeholders: {questCoolDown}, {questFrom}, {questUntil}, {questRepeat}

+ */ + QUESTS_PLANNER_DESCRIPTION, + + /** + * The format for a quest's cool-down + * + *

Placeholders: {coolDown}

+ */ + QUESTS_PLANNER_COOL_DOWN, + + /** + * The text to display if a quest cannot be repeated + */ + QUESTS_PLANNER_UNREPEATABLE, + + /** + * The format for a quest's first availability date + * + *

Placeholders: {startDate}

+ */ + QUESTS_PLANNER_FROM, + + /** + * The format for a quest's last availability date + * + *

Placeholders: {endDate}

+ */ + QUESTS_PLANNER_UNTIL, + + /** + * The format for a quest's repeat delay + * + *

Placeholders: {repeatDelay}

+ */ + QUEST_PLANNER_REPEAT, + + /** + * The format for a quest's requirements + * + *

Placeholders: {requirementQuestPoints}, {requirementExp}, {requirementBlockedByQuests}, + * {requirementRequiredQuests}, {requirementRequiredItems}, {requirementMCMMOSkills}, {requirementPermissions}, + * {requirementCustom}

+ */ + QUESTS_REQUIREMENTS_FORMAT, + + /** + * The format for a quest's quest point requirement + * + *

Placeholders: {questPoints}

+ */ + QUESTS_REQUIREMENTS_QUEST_POINTS, + + /** + * The format for a quest's exp requirement + * + *

Placeholders: {exp}

+ */ + QUESTS_REQUIREMENTS_EXP, + + /** + * The format for a quest's blocking quests + * + *

Placeholders: {blockingQuests}

+ */ + QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_FORMAT, + + /** + * The format for one of a quest's blocking quests + * + *

Placeholders: {questName}

+ */ + QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_ITEM, + + /** + * The format for a quest's required quests + * + *

Placeholders: {requiredQuests}

+ */ + QUESTS_REQUIREMENTS_REQUIRED_QUEST_FORMAT, + + /** + * The format for one of a quest's required quests + * + *

Placeholders: {questName}

+ */ + QUESTS_REQUIREMENTS_REQUIRED_QUEST_ITEM, + + /** + * The format for a quest's required items + * + *

Placeholders: {requiredItems}

+ */ + QUESTS_REQUIREMENTS_REQUIRED_ITEM_FORMAT, + + /** + * The format for one of a quest's required items + * + *

Placeholders: {itemName}

+ */ + QUESTS_REQUIREMENTS_REQUIRED_ITEM_ITEM, + + /** + * The format for a quest's mcMMO skill requirement + * + *

Placeholders: {skill}, {level}

+ */ + QUESTS_REQUIREMENTS_MC_MMO_SKILL, + + /** + * The format for a quest's required permissions + * + *

Placeholders: {permissions}

+ */ + QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_FORMAT, + + /** + * The format for one of a quest's required permissions + * + *

Placeholders: {permission}

+ */ + QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_ITEM, + + /** + * The format for a quest's reach area name + * + *

Placeholders: {name}

+ */ + QUESTS_REACH_AREA_NAME_FORMAT, + + /** + * The format for a quest's reach area's description + * + *

Placeholders: {areaName}, {questName}

+ */ + QUESTS_REACH_AREA_DESCRIPTION_FORMAT, + + /** + * The format for a quest's kill area name + * + *

Placeholders: {name}

+ */ + QUESTS_KILL_AREA_NAME_FORMAT, + + /** + * The format for a quest's kill area description + * + *

Placeholders: {areaName}, {questName}, {mobName}, {mobAmount}

+ */ + QUESTS_KILL_AREA_DESCRIPTION_FORMAT, + ; + + @Override + public TranslatableMessage[] getAllMessages() { + return QuestsTranslatableMessage.values(); + } +} diff --git a/src/main/java/net/knarcraft/dynmapcitizens/formatting/SentinelTranslatableMessage.java b/src/main/java/net/knarcraft/dynmapcitizens/formatting/SentinelTranslatableMessage.java new file mode 100644 index 0000000..27898cc --- /dev/null +++ b/src/main/java/net/knarcraft/dynmapcitizens/formatting/SentinelTranslatableMessage.java @@ -0,0 +1,31 @@ +package net.knarcraft.dynmapcitizens.formatting; + +import net.knarcraft.knarlib.formatting.TranslatableMessage; + +/** + * An enum describing all translatable messages for sentinels + */ +public enum SentinelTranslatableMessage implements TranslatableMessage { + + /** + * The format for the basic description of any sentinel + * + *

Placeholders: {name}, {squad}, {sentinelDetails}

+ */ + SENTINEL_DESCRIPTION, + + /** + * The format for the detailed description of any sentinel + * + *

Placeholders: {invincible}, {armor}, {health}, {accuracy}, {damage}, {speed}, {allowKnockback}, {range}, + * {reach}, {targets}, {avoids}, {ignores}

+ */ + SENTINEL_DETAILS, + ; + + @Override + public TranslatableMessage[] getAllMessages() { + return SentinelTranslatableMessage.values(); + } + +} diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/DTLTradersHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/DTLTradersHandler.java new file mode 100644 index 0000000..9657fda --- /dev/null +++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/DTLTradersHandler.java @@ -0,0 +1,53 @@ +package net.knarcraft.dynmapcitizens.handler.trait; + +import net.citizensnpcs.api.CitizensAPI; +import net.citizensnpcs.api.npc.NPC; +import net.citizensnpcs.api.trait.Trait; +import net.knarcraft.dynmapcitizens.DynmapCitizens; +import net.knarcraft.dynmapcitizens.property.Icon; +import net.knarcraft.dynmapcitizens.settings.DTLTradersSettings; +import net.knarcraft.dynmapcitizens.settings.TraitSettings; +import org.dynmap.markers.GenericMarker; + +/** + * A handler class for the minstrel trait + */ +public class DTLTradersHandler extends AbstractTraitHandler { + + private final DTLTradersSettings settings = new DTLTradersSettings(); + + @Override + public void initialize() { + super.isEnabled = false; + CitizensAPI.getTraitFactory().getRegisteredTraits().forEach(traitInfo -> { + if (traitInfo.getTraitName().equals("trader")) { + super.isEnabled = true; + } + }); + + if (this.isEnabled) { + super.initializeMarkerSet(); + } + } + + @Override + public TraitSettings getSettings() { + return this.settings; + } + + @Override + public void updateMarkers() { + //Remove existing markers + super.markerSet.getMarkers().forEach(GenericMarker::deleteMarker); + + Class traderTrait = CitizensAPI.getTraitFactory().getTraitClass("trader"); + for (NPC npc : CitizensAPI.getNPCRegistry()) { + if (npc.hasTrait(traderTrait)) { + String description = "

" + npc.getName() + "

"; + addNPCMarker(npc.getUniqueId(), "Trader NPC: ", description, + DynmapCitizens.getInstance().getGlobalSettings().getMarkerIcons().get(Icon.TRADER), super.markerSet); + } + } + } + +} diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/SentinelHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/SentinelHandler.java index 28a7993..dc0938e 100644 --- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/SentinelHandler.java +++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/SentinelHandler.java @@ -47,12 +47,14 @@ public class SentinelHandler extends AbstractTraitHandler { description += "
Squad: " + trait.squad; } if (settings.displaySentinelStats()) { - description += "
Invincible: " + trait.invincible + "
Armor: " + - trait.armor + "
Health: " + trait.health + "
Accuracy: " + trait.accuracy + - "
Damage: " + trait.damage + "
Allow knockback: " + trait.allowKnockback; + description += "
Invincible: " + trait.invincible + "
Armor: " + trait.armor; + description += "
Health: " + trait.health + "
Accuracy: " + trait.accuracy; + description += "
Damage: " + trait.damage + "
Speed: " + trait.speed; + description += "
Allow knockback: " + trait.allowKnockback; description += "
Range: " + trait.range + "
Reach: " + trait.reach; - description += "
Targets: " + trait.allTargets.toAllInOneString() + "
Avoids: " + - trait.allAvoids.toAllInOneString() + "
Ignores: " + trait.allIgnores.toAllInOneString(); + description += "
Targets: " + trait.allTargets.toAllInOneString(); + description += "
Avoids: " + trait.allAvoids.toAllInOneString(); + description += "
Ignores: " + trait.allIgnores.toAllInOneString(); } addNPCMarker(npc.getUniqueId(), "Sentinel NPC: ", description, DynmapCitizens.getInstance().getGlobalSettings().getMarkerIcons().get(Icon.SENTINEL), super.markerSet); diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestAreaHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestAreaHandler.java index 25cf000..adf80b8 100644 --- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestAreaHandler.java +++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestAreaHandler.java @@ -3,9 +3,13 @@ package net.knarcraft.dynmapcitizens.handler.trait.quests; import me.blackvein.quests.QuestsAPI; import me.blackvein.quests.quests.IQuest; import me.blackvein.quests.quests.IStage; +import net.knarcraft.dynmapcitizens.DynmapCitizens; import net.knarcraft.dynmapcitizens.settings.QuestsSettings; import net.knarcraft.dynmapcitizens.util.DynmapHelper; import net.knarcraft.dynmapcitizens.util.QuestsHelper; +import net.knarcraft.knarlib.formatting.StringFormatter; +import net.knarcraft.knarlib.formatting.StringReplacer; +import net.knarcraft.knarlib.formatting.Translator; import org.bukkit.Location; import org.bukkit.entity.EntityType; import org.dynmap.DynmapAPI; @@ -14,6 +18,11 @@ import org.dynmap.markers.MarkerSet; import java.util.List; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_KILL_AREA_DESCRIPTION_FORMAT; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_KILL_AREA_NAME_FORMAT; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REACH_AREA_DESCRIPTION_FORMAT; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REACH_AREA_NAME_FORMAT; + /** * A handler class for quest areas */ @@ -24,6 +33,7 @@ public class QuestAreaHandler { private final MarkerSet reachAreaMarkerSet; private final QuestsSettings settings; private final List unavailableQuests; + private final StringFormatter formatter; /** * Instantiates a new quest area handler @@ -38,6 +48,7 @@ public class QuestAreaHandler { this.questsAPI = questsAPI; this.settings = settings; this.unavailableQuests = unavailableQuests; + this.formatter = DynmapCitizens.getFormatter(); killAreaMarkerSet = DynmapHelper.initializeMarkerSet(dynmapAPI, settings.getKillAreaSettings()); reachAreaMarkerSet = DynmapHelper.initializeMarkerSet(dynmapAPI, settings.getReachAreaSettings()); } @@ -75,11 +86,16 @@ public class QuestAreaHandler { Location location = stage.getLocationsToReach().get(i); int radius = stage.getRadiiToReachWithin().get(i); String areaName = stage.getLocationNames().get(i); - String description = ""; + + String formattedAreaName; if (areaName != null) { - description += "" + areaName + "
"; + formattedAreaName = formatter.replacePlaceholder(QUESTS_REACH_AREA_NAME_FORMAT, "{name}", areaName); + } else { + formattedAreaName = ""; } - description += "Target location for " + quest.getName(); + + String description = formatter.replacePlaceholders(QUESTS_REACH_AREA_DESCRIPTION_FORMAT, + new String[]{"{areaName}", "{questName}"}, new String[]{formattedAreaName, quest.getName()}); DynmapHelper.markLocation(location, radius, description, reachAreaMarkerSet, settings.getReachAreaSettings()); } } @@ -101,13 +117,20 @@ public class QuestAreaHandler { int mobAmount = stage.getMobNumToKill().get(i); String areaName = stage.getKillNames().get(i); - String description = ""; + String formattedAreaName; if (areaName != null) { - description += "" + areaName + "
"; + formattedAreaName = formatter.replacePlaceholder(QUESTS_KILL_AREA_NAME_FORMAT, "{name}", areaName); + } else { + formattedAreaName = ""; } - description += "Kill location for " + quest.getName() + - "
Kill " + QuestsHelper.normalizeName(mob.name()) + " x " + mobAmount; - DynmapHelper.markLocation(location, radius, description, killAreaMarkerSet, settings.getKillAreaSettings()); + + Translator translator = DynmapCitizens.getTranslator(); + StringReplacer replacer = new StringReplacer(translator.getTranslatedMessage(QUESTS_KILL_AREA_DESCRIPTION_FORMAT)); + replacer.add("{areaName}", formattedAreaName); + replacer.add("{questName}", quest.getName()); + replacer.add("{mobName}", QuestsHelper.normalizeName(mob.name())); + replacer.add("{mobAmount}", String.valueOf(mobAmount)); + DynmapHelper.markLocation(location, radius, replacer.replace(), killAreaMarkerSet, settings.getKillAreaSettings()); } } diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestPlannerInfoGenerator.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestPlannerInfoGenerator.java index 3d6d6e5..d3bce69 100644 --- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestPlannerInfoGenerator.java +++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestPlannerInfoGenerator.java @@ -2,7 +2,12 @@ package net.knarcraft.dynmapcitizens.handler.trait.quests; import me.blackvein.quests.quests.IQuest; import me.blackvein.quests.quests.Planner; -import net.knarcraft.dynmapcitizens.util.TimeFormatter; +import net.knarcraft.dynmapcitizens.DynmapCitizens; +import net.knarcraft.knarlib.formatting.TimeFormatter; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; /** * A class to generate a string containing all information about a quest's planner info @@ -33,7 +38,8 @@ public class QuestPlannerInfoGenerator { //Quest can be repeated after a cool-down if (planner.hasCooldown()) { plannerInfo.append("
  • Quest repeatable after: "); - plannerInfo.append(TimeFormatter.getDurationString(planner.getCooldown() / 1000)); + plannerInfo.append(TimeFormatter.getDurationString(DynmapCitizens.getTranslator(), + planner.getCooldown() / 1000)); plannerInfo.append("
  • "); } else { plannerInfo.append("
  • Quest cannot be repeated!
  • "); @@ -42,23 +48,36 @@ public class QuestPlannerInfoGenerator { //Quest only becomes available after the start date if (planner.hasStart()) { plannerInfo.append("
  • Quest available from "); - plannerInfo.append(TimeFormatter.formatTimestamp(planner.getStartInMillis())).append("
  • "); + plannerInfo.append(formatTimestamp(planner.getStartInMillis())).append(""); } //Quest is only available until the end date if (planner.hasEnd()) { plannerInfo.append("
  • Quest available until "); - plannerInfo.append(TimeFormatter.formatTimestamp(planner.getEndInMillis())).append("
  • "); + plannerInfo.append(formatTimestamp(planner.getEndInMillis())).append(""); } //Quest availability repeats if (planner.hasRepeat()) { plannerInfo.append("
  • Quest will become available again after "); - plannerInfo.append(TimeFormatter.getDurationString(planner.getRepeat() / 1000)).append("
  • "); + plannerInfo.append(TimeFormatter.getDurationString(DynmapCitizens.getTranslator(), + planner.getRepeat() / 1000)).append(""); } plannerInfo.append(""); return plannerInfo.toString(); } + /** + * Gets a datetime string for the given timestamp + * + * @param timestamp

    A timestamp in milliseconds

    + * @return

    A datetime string

    + */ + private String formatTimestamp(long timestamp) { + DateFormat format = new SimpleDateFormat("dd MM yyyy HH:mm:ss"); + Date date = new Date(timestamp); + return format.format(date); + } + } diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRequirementsInfoGenerator.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRequirementsInfoGenerator.java index 426b2ff..03f2dc0 100644 --- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRequirementsInfoGenerator.java +++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRequirementsInfoGenerator.java @@ -1,27 +1,51 @@ package net.knarcraft.dynmapcitizens.handler.trait.quests; +import me.blackvein.quests.QuestsAPI; import me.blackvein.quests.quests.IQuest; import me.blackvein.quests.quests.Requirements; +import net.knarcraft.dynmapcitizens.DynmapCitizens; import net.knarcraft.dynmapcitizens.util.QuestsHelper; +import net.knarcraft.knarlib.formatting.StringFormatter; +import net.knarcraft.knarlib.formatting.StringReplacer; +import net.knarcraft.knarlib.formatting.TranslatableMessage; import org.bukkit.inventory.ItemStack; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_FORMAT; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_ITEM; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_EXP; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_FORMAT; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_MC_MMO_SKILL; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_QUEST_POINTS; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_ITEM_FORMAT; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_ITEM_ITEM; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_FORMAT; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_ITEM; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_QUEST_FORMAT; +import static net.knarcraft.dynmapcitizens.formatting.QuestsTranslatableMessage.QUESTS_REQUIREMENTS_REQUIRED_QUEST_ITEM; + /** * A class to generate a string containing all information about a quest's requirements */ public class QuestRequirementsInfoGenerator { + private final QuestsAPI questsAPI; private final IQuest quest; + private final StringFormatter formatter; /** * Instantiates a new quest requirement info generator * - * @param quest

    The quest to generate information about

    + * @param questsAPI

    The API to use for getting quest information

    + * @param quest

    The quest to generate information about

    */ - public QuestRequirementsInfoGenerator(IQuest quest) { + public QuestRequirementsInfoGenerator(QuestsAPI questsAPI, IQuest quest) { + this.questsAPI = questsAPI; this.quest = quest; + formatter = DynmapCitizens.getFormatter(); } /** @@ -31,78 +55,140 @@ public class QuestRequirementsInfoGenerator { */ public String getQuestRequirementsInfo() { Requirements requirements = quest.getRequirements(); - StringBuilder requirementInfo = new StringBuilder(); if (!requirements.hasRequirement()) { - return requirementInfo.toString(); + return ""; } - requirementInfo.append("Requirements:
      "); + StringReplacer replacer = new StringReplacer(DynmapCitizens.getTranslator().getTranslatedMessage( + QUESTS_REQUIREMENTS_FORMAT)); - if (requirements.getQuestPoints() > 0) { - requirementInfo.append("
    • ").append(requirements.getQuestPoints()).append(" quest points
    • "); - } + //Add info about quest point requirement + replacer.add("{requirementQuestPoints}", requirements.getQuestPoints() > 0 ? + formatter.replacePlaceholder(QUESTS_REQUIREMENTS_QUEST_POINTS, + "{questPoints}", String.valueOf(requirements.getQuestPoints())) : ""); - if (requirements.getExp() > 0) { - requirementInfo.append("
    • ").append(requirements.getExp()).append(" exp
    • "); - } + //Add info about exp requirement + replacer.add("{requirementExp}", requirements.getExp() > 0 ? formatter.replacePlaceholder( + QUESTS_REQUIREMENTS_EXP, "{exp}", String.valueOf(requirements.getExp())) : ""); - if (!requirements.getBlockQuests().isEmpty()) { - requirementInfo.append("
    • Blocked by quests:
        "); - for (IQuest blockQuest : requirements.getBlockQuests()) { - requirementInfo.append("
      • ").append(blockQuest.getName()).append("
      • "); - } - requirementInfo.append("
    • "); - } + //Add info about blocking quests + replacer.add("{requirementBlockedByQuests}", !requirements.getBlockQuestIds().isEmpty() ? + getRequirementList(getQuestNames(requirements.getBlockQuestIds()), + QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_FORMAT, "{blockingQuests}", + QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_ITEM, "{questName}") : ""); - if (!requirements.getNeededQuests().isEmpty()) { - requirementInfo.append("
    • Required quests:
        "); - for (IQuest neededQuest : requirements.getNeededQuests()) { - requirementInfo.append("
      • ").append(neededQuest.getName()).append("
      • "); - } - requirementInfo.append("
    • "); - } + //Add info about required quests + replacer.add("{requirementRequiredQuests}", !requirements.getBlockQuestIds().isEmpty() ? + getRequirementList(getQuestNames(requirements.getBlockQuestIds()), + QUESTS_REQUIREMENTS_REQUIRED_QUEST_FORMAT, "{requiredQuests}", + QUESTS_REQUIREMENTS_REQUIRED_QUEST_ITEM, "{questName}") : ""); - if (!requirements.getItems().isEmpty()) { - requirementInfo.append("
    • Required items:
        "); - for (ItemStack item : requirements.getItems()) { - requirementInfo.append("
      • ").append(QuestsHelper.getUpperCasedItemStackString(item)).append("
      • "); - } - requirementInfo.append("
    • "); - } + //Add info about required items + replacer.add("{requirementRequiredItems}", !requirements.getItems().isEmpty() ? + getRequirementList(getItemNames(requirements.getItems()), QUESTS_REQUIREMENTS_REQUIRED_ITEM_FORMAT, + "{requiredItems}", QUESTS_REQUIREMENTS_REQUIRED_ITEM_ITEM, "{itemName}") : ""); + //Add info about required mcMMO skills if (!requirements.getMcmmoSkills().isEmpty()) { List skills = requirements.getMcmmoSkills(); List amounts = requirements.getMcmmoAmounts(); + StringBuilder mcMMOSkillsBuilder = new StringBuilder(); for (int i = 0; i < skills.size(); i++) { - requirementInfo.append("
    • Requires mcMMO skill ").append(skills.get(i)).append(" at level "); - requirementInfo.append(amounts.get(i)).append("
    • "); + mcMMOSkillsBuilder.append(formatter.replacePlaceholders(QUESTS_REQUIREMENTS_MC_MMO_SKILL, new String[]{ + "{skill}", "{level}"}, new String[]{skills.get(i), String.valueOf(amounts.get(i))})); } + replacer.add("{requirementMCMMOSkills}", mcMMOSkillsBuilder.toString()); + } else { + replacer.add("{requirementMCMMOSkills}", ""); } - if (!requirements.getPermissions().isEmpty()) { - requirementInfo.append("
    • Required permissions:
        "); - for (String permission : requirements.getPermissions()) { - requirementInfo.append("
      • ").append(permission).append("
      • "); - } - requirementInfo.append("
    • "); - } + //Add info about required permissions + replacer.add("{requirementPermissions}", !requirements.getPermissions().isEmpty() ? + getRequirementList(requirements.getPermissions(), QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_FORMAT, + "{permissions}", QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_ITEM, "{permission}") : ""); Map> customRequirementPlugins = requirements.getCustomRequirements(); + StringBuilder customRequirementsBuilder = new StringBuilder(); for (String plugin : customRequirementPlugins.keySet()) { - requirementInfo.append("
    • ").append(plugin).append(":
        "); + customRequirementsBuilder.append("
      • ").append(plugin).append(":
          "); //Note: The format of custom requirements is kind of weird. First, you have the key for which plugin the // requirement belongs to. Getting the value of the key gives another map. The map contains as key, the type // of value, like "Skill Amount" or "Skill Type". The value is the actual value of whatever it is. Map customRequirementEntry = customRequirementPlugins.get(plugin); for (String requirementDescription : customRequirementEntry.keySet()) { - requirementInfo.append("
        • ").append(requirementDescription).append(" "); - requirementInfo.append(customRequirementEntry.get(requirementDescription)).append("
        • "); + customRequirementsBuilder.append("
        • ").append(requirementDescription).append(" "); + customRequirementsBuilder.append(customRequirementEntry.get(requirementDescription)).append("
        • "); } - requirementInfo.append("
      • "); + customRequirementsBuilder.append("
    • "); } + replacer.add("{requirementCustom}", customRequirementsBuilder.toString()); + return replacer.replace(); + } - requirementInfo.append("
    "); - return requirementInfo.toString(); + /** + * Gets a list of item names from the given list of items + * + * @param items

    The items to get the names of

    + * @return

    The names of the given items

    + */ + private List getItemNames(List items) { + List itemNames = new ArrayList<>(); + for (ItemStack itemStack : items) { + itemNames.add(QuestsHelper.getUpperCasedItemStackString(itemStack)); + } + return itemNames; + } + + /** + * Gets a list of the quest names for the given quests + * + * @param questIds

    The quests to get names for

    + * @return

    A list of quest names

    + */ + private List getQuestNames(List questIds) { + List questNames = new ArrayList<>(questIds.size()); + for (String questId : questIds) { + IQuest quest = getQuest(questId); + if (quest != null) { + questNames.add(quest.getName()); + } + } + return questNames; + } + + /** + * Gets the quest with the given id + * + * @param questId

    The id of the quest to get

    + * @return

    The quest, or null if not found

    + */ + private IQuest getQuest(String questId) { + for (IQuest quest : questsAPI.getLoadedQuests()) { + if (quest.getId().equals(questId)) { + return quest; + } + } + return null; + } + + /** + * Gets a string for the given list of requirements + * + * @param itemList

    The items to display in the list of requirements

    + * @param formatMessage

    The translatable message describing the list format

    + * @param formatPlaceholder

    The placeholder to replace with the list items

    + * @param itemMessage

    The translatable message describing each item's format

    + * @param itemPlaceholder

    The placeholder to replace with each item in the list

    + * @return

    The string corresponding to the given requirement list

    + */ + private String getRequirementList(List itemList, TranslatableMessage formatMessage, String formatPlaceholder, + TranslatableMessage itemMessage, String itemPlaceholder) { + StringBuilder blockedBuilder = new StringBuilder(); + for (Object requirements : itemList) { + blockedBuilder.append(formatter.replacePlaceholder(itemMessage, itemPlaceholder, + String.valueOf(requirements))); + } + return formatter.replacePlaceholder(formatMessage, formatPlaceholder, blockedBuilder.toString()); } } diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestStagesInfoGenerator.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestStagesInfoGenerator.java index 3b9d25a..f91575d 100644 --- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestStagesInfoGenerator.java +++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestStagesInfoGenerator.java @@ -94,6 +94,9 @@ public class QuestStagesInfoGenerator { for (UUID npcId : stage.getNpcsToKill()) { questInfo.append("
  • Kill NPC ").append(registry.getByUniqueId(npcId).getName()).append("
  • "); } + for (UUID npcId : stage.getNpcsToInteract()) { + questInfo.append("
  • Talk to ").append(registry.getByUniqueId(npcId).getName()).append("
  • "); + } questInfo.append(getQuestItemsTaskString(stage.getBlocksToBreak(), "
  • Break ")).append("
  • "); questInfo.append(getQuestItemsTaskString(stage.getBlocksToCut(), "
  • Cut ")).append("
  • "); diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestsHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestsHandler.java index 3b928a0..b68cf7c 100644 --- a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestsHandler.java +++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestsHandler.java @@ -163,7 +163,7 @@ public class QuestsHandler extends AbstractTraitHandler { stringBuilder.append(new QuestRewardsInfoGenerator(quest).getQuestRewardsInfo()); } if (settings.displayRequirementInfo()) { - stringBuilder.append(new QuestRequirementsInfoGenerator(quest).getQuestRequirementsInfo()); + stringBuilder.append(new QuestRequirementsInfoGenerator(questsAPI, quest).getQuestRequirementsInfo()); } if (settings.displayPlannerInfo()) { stringBuilder.append(new QuestPlannerInfoGenerator(quest).getQuestPlannerInfo()); diff --git a/src/main/java/net/knarcraft/dynmapcitizens/property/Icon.java b/src/main/java/net/knarcraft/dynmapcitizens/property/Icon.java index b0b5ee0..5f2f949 100644 --- a/src/main/java/net/knarcraft/dynmapcitizens/property/Icon.java +++ b/src/main/java/net/knarcraft/dynmapcitizens/property/Icon.java @@ -43,6 +43,11 @@ public enum Icon { /** * An icon representing a minstrel NPC */ - MINSTREL + MINSTREL, + + /** + * An icon representing a trader NPC + */ + TRADER, } diff --git a/src/main/java/net/knarcraft/dynmapcitizens/settings/DTLTradersSettings.java b/src/main/java/net/knarcraft/dynmapcitizens/settings/DTLTradersSettings.java new file mode 100644 index 0000000..80b3593 --- /dev/null +++ b/src/main/java/net/knarcraft/dynmapcitizens/settings/DTLTradersSettings.java @@ -0,0 +1,20 @@ +package net.knarcraft.dynmapcitizens.settings; + +import org.bukkit.configuration.file.FileConfiguration; + +/** + * All settings for the minstrel trait + */ +public class DTLTradersSettings extends AbstractTraitSettings { + + @Override + public void load(FileConfiguration configuration) { + super.load(configuration); + } + + @Override + protected String getTraitConfigRoot() { + return "traits.trader"; + } + +} diff --git a/src/main/java/net/knarcraft/dynmapcitizens/settings/GlobalSettings.java b/src/main/java/net/knarcraft/dynmapcitizens/settings/GlobalSettings.java index a7fe753..47f70bd 100644 --- a/src/main/java/net/knarcraft/dynmapcitizens/settings/GlobalSettings.java +++ b/src/main/java/net/knarcraft/dynmapcitizens/settings/GlobalSettings.java @@ -82,6 +82,7 @@ public class GlobalSettings { case BLACKSMITH -> "hammer"; case SENTINEL -> "shield"; case MINSTREL -> "theater"; + case TRADER -> "coins"; }; } diff --git a/src/main/java/net/knarcraft/dynmapcitizens/util/TimeFormatter.java b/src/main/java/net/knarcraft/dynmapcitizens/util/TimeFormatter.java deleted file mode 100644 index 5ebc8ba..0000000 --- a/src/main/java/net/knarcraft/dynmapcitizens/util/TimeFormatter.java +++ /dev/null @@ -1,98 +0,0 @@ -package net.knarcraft.dynmapcitizens.util; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static net.knarcraft.blacksmith.formatting.StringFormatter.replacePlaceholder; - -/** - * A helper class for time formatting - */ -public class TimeFormatter { - - /** - * Gets a datetime string for the given timestamp - * - * @param timestamp

    A timestamp in milliseconds

    - * @return

    A datetime string

    - */ - public static String formatTimestamp(long timestamp) { - DateFormat format = new SimpleDateFormat("dd MM yyyy HH:mm:ss"); - Date date = new Date(timestamp); - return format.format(date); - } - - /** - * Gets the string used for displaying this sign's duration - * - * @return

    The string used for displaying this sign's duration

    - */ - public static String getDurationString(long duration) { - if (duration == 0) { - return "immediately"; - } else { - double minute = 60; - double hour = minute * 60; - double day = hour * 24; - double week = day * 7; - double month = day * 30; - double year = day * 365; - double decade = year * 10; - - Map timeUnits = new HashMap<>(); - timeUnits.put(decade, new String[]{"decade", "decades"}); - timeUnits.put(year, new String[]{"year", "years"}); - timeUnits.put(month, new String[]{"month", "months"}); - timeUnits.put(week, new String[]{"week", "weeks"}); - timeUnits.put(day, new String[]{"day", "days"}); - timeUnits.put(hour, new String[]{"hour", "hours"}); - timeUnits.put(minute, new String[]{"minute", "minutes"}); - timeUnits.put(1D, new String[]{"second", "seconds"}); - - List sortedUnits = new ArrayList<>(timeUnits.keySet()); - Collections.sort(sortedUnits); - Collections.reverse(sortedUnits); - - for (Double unit : sortedUnits) { - if (duration / unit >= 1) { - double units = round(duration / unit); - return formatDurationString(units, timeUnits.get(unit)[units == 1 ? 0 : 1], - (units * 10) % 10 == 0); - } - } - return formatDurationString(duration, "seconds", false); - } - } - - /** - * Rounds a number to its last two digits - * - * @param number

    The number to round

    - * @return

    The rounded number

    - */ - private static double round(double number) { - return Math.round(number * 100.0) / 100.0; - } - - /** - * Formats a duration string - * - * @param duration

    The duration to display

    - * @param translatableMessage

    The time unit to display

    - * @param castToInt

    Whether to cast the duration to an int

    - * @return

    The formatted duration string

    - */ - private static String formatDurationString(double duration, String translatableMessage, boolean castToInt) { - String durationFormat = "{duration} {unit}"; - durationFormat = replacePlaceholder(durationFormat, "{unit}", translatableMessage); - return replacePlaceholder(durationFormat, "{duration}", castToInt ? String.valueOf((int) duration) : - String.valueOf(duration)); - } - -} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 9dd6bdc..95ef02b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -16,6 +16,8 @@ icon: SENTINEL: "shield" # The marker used for minstrels MINSTREL: "theater" + # The marker used for traders + TRADER: "coins" # Settings for how often markers will be updated timer: @@ -129,4 +131,15 @@ traits: # Whether to hide the minstrel icon layer by default markersHiddenByDefault: false # Whether to display the list of songs a minstrel is playing - displayMinstrelSongs: true \ No newline at end of file + displayMinstrelSongs: true + # Settings for the trader trait + trader: + enabled: true + # The priority of trader markers. Higher priority markers will display on top of lower priority ones + markerSetPriority: 1 + # The id of the trader marker set. Change if it overlaps with an existing set id + markerSetId: "traders" + # The name of the trader marker set. Change it if you want a cooler name + markerSetName: "Traders" + # Whether to hide the trader icon layer by default + markersHiddenByDefault: false \ No newline at end of file diff --git a/src/main/resources/strings.yml b/src/main/resources/strings.yml new file mode 100644 index 0000000..21803ac --- /dev/null +++ b/src/main/resources/strings.yml @@ -0,0 +1,76 @@ +en: + SENTINEL_DESCRIPTION: | +

    {name}

    +
    Squad: {squad} + {sentinelDetails} + SENTINEL_DETAILS: | +
      +
    • Invincible: {invincible}
    • +
    • Armor: {armor}
    • +
    • Health: {health}
    • +
    • Accuracy: {accuracy}
    • +
    • Damage: {damage}
    • +
    • Speed: {speed}
    • +
    • Allow knockback: {allowKnockback}
    • +
    • Range: {range}
    • +
    • Reach: {reach}
    • +
    • Targets: {targets}
    • +
    • Avoids: {avoids}
    • +
    • Ignores: {ignores}
    • +
    + QUESTS_PLANNER_DESCRIPTION: | + Planner:
      + {questCoolDown} + {questFrom} + {questUntil} + {questRepeat} +
    + QUESTS_PLANNER_COOL_DOWN: "
  • Quest repeatable after: {coolDown}
  • " + QUESTS_PLANNER_UNREPEATABLE: "
  • Quest cannot be repeated!
  • " + QUESTS_PLANNER_FROM: "
  • Quest available from {startDate}
  • " + QUESTS_PLANNER_UNTIL: "
  • Quest available until {endDate}
  • " + QUEST_PLANNER_REPEAT: "
  • Quest will become available again after {repeatDelay}
  • " + QUESTS_REQUIREMENTS_FORMAT: | + Requirements:
      + {requirementQuestPoints} + {requirementExp} + {requirementBlockedByQuests} + {requirementRequiredQuests} + {requirementRequiredItems} + {requirementMCMMOSkills} + {requirementPermissions} + {requirementCustom} +
    + QUESTS_REQUIREMENTS_QUEST_POINTS: "
  • {questPoints} quest points
  • " + QUESTS_REQUIREMENTS_EXP: "
  • {exp} exp
  • " + QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_FORMAT: "
  • Blocked by quests:
      {blockingQuests}
  • " + QUESTS_REQUIREMENTS_BLOCKED_BY_QUEST_ITEM: "
  • {questName}
  • " + QUESTS_REQUIREMENTS_REQUIRED_QUEST_FORMAT: "
  • Required quests:
      {requiredQuests}
  • " + QUESTS_REQUIREMENTS_REQUIRED_QUEST_ITEM: "
  • {questName}
  • " + QUESTS_REQUIREMENTS_REQUIRED_ITEM_FORMAT: "
  • Required items:
      {requiredItems}
  • " + QUESTS_REQUIREMENTS_REQUIRED_ITEM_ITEM: "
  • {itemName}
  • " + QUESTS_REQUIREMENTS_MC_MMO_SKILL: "
  • Requires mcMMO skill {skill} at level {level}
  • " + QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_FORMAT: "
  • Required permissions:
      {permissions}
  • " + QUESTS_REQUIREMENTS_REQUIRED_PERMISSION_ITEM: "
  • {permission}
  • " + QUESTS_REACH_AREA_NAME_FORMAT: "{name}
    " + QUESTS_REACH_AREA_DESCRIPTION_FORMAT: "{areaName}Target location for {questName}" + QUESTS_KILL_AREA_NAME_FORMAT: "{name}
    " + QUESTS_KILL_AREA_DESCRIPTION_FORMAT: "{areaName}Kill location for {questName}
    Kill {mobName} x {mobAmount}" + DURATION_FORMAT: "in {time} {unit}" + UNIT_NOW: "imminently" + UNIT_SECOND: "second" + UNIT_SECONDS: "seconds" + UNIT_MINUTE: "minute" + UNIT_MINUTES: "minutes" + UNIT_HOUR: "hour" + UNIT_HOURS: "hours" + UNIT_DAY: "day" + UNIT_DAYS: "days" + UNIT_WEEK: "week" + UNIT_WEEKS: "weeks" + UNIT_MONTH: "month" + UNIT_MONTHS: "months" + UNIT_YEAR: "year" + UNIT_YEARS: "years" + UNIT_DECADE: "decade" + UNIT_DECADES: "decades" \ No newline at end of file