diff --git a/src/main/java/net/knarcraft/dynmapcitizens/DynmapCitizens.java b/src/main/java/net/knarcraft/dynmapcitizens/DynmapCitizens.java
index 99afdf6..b7fd8cb 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/DynmapCitizens.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/DynmapCitizens.java
@@ -1,10 +1,12 @@
package net.knarcraft.dynmapcitizens;
-import net.knarcraft.dynmapcitizens.trait.BlacksmithHandler;
-import net.knarcraft.dynmapcitizens.trait.CitizensTraitHandler;
-import net.knarcraft.dynmapcitizens.trait.MinstrelHandler;
-import net.knarcraft.dynmapcitizens.trait.SentinelHandler;
-import net.knarcraft.dynmapcitizens.trait.quests.QuestsHandler;
+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.MinstrelHandler;
+import net.knarcraft.dynmapcitizens.handler.trait.SentinelHandler;
+import net.knarcraft.dynmapcitizens.handler.trait.quests.QuestsHandler;
+import net.knarcraft.dynmapcitizens.property.Icon;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/VaultHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/VaultHandler.java
similarity index 96%
rename from src/main/java/net/knarcraft/dynmapcitizens/VaultHandler.java
rename to src/main/java/net/knarcraft/dynmapcitizens/handler/VaultHandler.java
index 1ea5723..d082ba4 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/VaultHandler.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/VaultHandler.java
@@ -1,4 +1,4 @@
-package net.knarcraft.dynmapcitizens;
+package net.knarcraft.dynmapcitizens.handler;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.plugin.RegisteredServiceProvider;
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/trait/AbstractTraitHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/AbstractTraitHandler.java
similarity index 98%
rename from src/main/java/net/knarcraft/dynmapcitizens/trait/AbstractTraitHandler.java
rename to src/main/java/net/knarcraft/dynmapcitizens/handler/trait/AbstractTraitHandler.java
index 391d086..b129fdd 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/trait/AbstractTraitHandler.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/AbstractTraitHandler.java
@@ -1,4 +1,4 @@
-package net.knarcraft.dynmapcitizens.trait;
+package net.knarcraft.dynmapcitizens.handler.trait;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/trait/BlacksmithHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/BlacksmithHandler.java
similarity index 96%
rename from src/main/java/net/knarcraft/dynmapcitizens/trait/BlacksmithHandler.java
rename to src/main/java/net/knarcraft/dynmapcitizens/handler/trait/BlacksmithHandler.java
index b0055ef..6f1ba99 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/trait/BlacksmithHandler.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/BlacksmithHandler.java
@@ -1,4 +1,4 @@
-package net.knarcraft.dynmapcitizens.trait;
+package net.knarcraft.dynmapcitizens.handler.trait;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
@@ -7,8 +7,8 @@ import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.NPCSettings;
import net.knarcraft.blacksmith.trait.BlacksmithTrait;
import net.knarcraft.dynmapcitizens.DynmapCitizens;
-import net.knarcraft.dynmapcitizens.Icon;
-import net.knarcraft.dynmapcitizens.UpdateRate;
+import net.knarcraft.dynmapcitizens.property.Icon;
+import net.knarcraft.dynmapcitizens.property.UpdateRate;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.dynmap.DynmapAPI;
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/trait/CitizensTraitHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/CitizensTraitHandler.java
similarity index 85%
rename from src/main/java/net/knarcraft/dynmapcitizens/trait/CitizensTraitHandler.java
rename to src/main/java/net/knarcraft/dynmapcitizens/handler/trait/CitizensTraitHandler.java
index 90c7d9c..f3cb726 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/trait/CitizensTraitHandler.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/CitizensTraitHandler.java
@@ -1,6 +1,6 @@
-package net.knarcraft.dynmapcitizens.trait;
+package net.knarcraft.dynmapcitizens.handler.trait;
-import net.knarcraft.dynmapcitizens.UpdateRate;
+import net.knarcraft.dynmapcitizens.property.UpdateRate;
/**
* A handler which takes care of everything for one citizen trait
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/trait/MinstrelHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/MinstrelHandler.java
similarity index 94%
rename from src/main/java/net/knarcraft/dynmapcitizens/trait/MinstrelHandler.java
rename to src/main/java/net/knarcraft/dynmapcitizens/handler/trait/MinstrelHandler.java
index f5f834c..639e9b9 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/trait/MinstrelHandler.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/MinstrelHandler.java
@@ -1,11 +1,11 @@
-package net.knarcraft.dynmapcitizens.trait;
+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.Icon;
-import net.knarcraft.dynmapcitizens.UpdateRate;
+import net.knarcraft.dynmapcitizens.property.Icon;
+import net.knarcraft.dynmapcitizens.property.UpdateRate;
import net.knarcraft.minstrel.MinstrelPlugin;
import net.knarcraft.minstrel.music.Song;
import net.knarcraft.minstrel.trait.MinstrelTrait;
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/trait/SentinelHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/SentinelHandler.java
similarity index 92%
rename from src/main/java/net/knarcraft/dynmapcitizens/trait/SentinelHandler.java
rename to src/main/java/net/knarcraft/dynmapcitizens/handler/trait/SentinelHandler.java
index 7506784..aae0f44 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/trait/SentinelHandler.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/SentinelHandler.java
@@ -1,11 +1,11 @@
-package net.knarcraft.dynmapcitizens.trait;
+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.Icon;
-import net.knarcraft.dynmapcitizens.UpdateRate;
+import net.knarcraft.dynmapcitizens.property.Icon;
+import net.knarcraft.dynmapcitizens.property.UpdateRate;
import org.bukkit.Bukkit;
import org.dynmap.DynmapAPI;
import org.dynmap.markers.GenericMarker;
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/trait/quests/NPCQuestInfo.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/NPCQuestInfo.java
similarity index 97%
rename from src/main/java/net/knarcraft/dynmapcitizens/trait/quests/NPCQuestInfo.java
rename to src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/NPCQuestInfo.java
index 89ba09d..7082751 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/trait/quests/NPCQuestInfo.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/NPCQuestInfo.java
@@ -1,7 +1,7 @@
-package net.knarcraft.dynmapcitizens.trait.quests;
+package net.knarcraft.dynmapcitizens.handler.trait.quests;
import me.blackvein.quests.quests.IQuest;
-import net.knarcraft.dynmapcitizens.Icon;
+import net.knarcraft.dynmapcitizens.property.Icon;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/trait/quests/QuestNPCType.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestNPCType.java
similarity index 89%
rename from src/main/java/net/knarcraft/dynmapcitizens/trait/quests/QuestNPCType.java
rename to src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestNPCType.java
index 918bea6..ea90965 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/trait/quests/QuestNPCType.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestNPCType.java
@@ -1,4 +1,4 @@
-package net.knarcraft.dynmapcitizens.trait.quests;
+package net.knarcraft.dynmapcitizens.handler.trait.quests;
/**
* A specifier for a quest NPC's main type
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
new file mode 100644
index 0000000..3d6d6e5
--- /dev/null
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestPlannerInfoGenerator.java
@@ -0,0 +1,64 @@
+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;
+
+/**
+ * A class to generate a string containing all information about a quest's planner info
+ */
+public class QuestPlannerInfoGenerator {
+
+ private final IQuest quest;
+
+ /**
+ * Instantiates a new quest planner info generator
+ *
+ * @param quest
The quest to generate information about
+ */
+ public QuestPlannerInfoGenerator(IQuest quest) {
+ this.quest = quest;
+ }
+
+ /**
+ * Gets information about the time-availability of a quest
+ *
+ * @return Information about when the quest is available
+ */
+ public String getQuestPlannerInfo() {
+ Planner planner = quest.getPlanner();
+ StringBuilder plannerInfo = new StringBuilder();
+ plannerInfo.append("Planner:");
+
+ //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("
");
+ } else {
+ plannerInfo.append("- Quest cannot be repeated!
");
+ }
+
+ //Quest only becomes available after the start date
+ if (planner.hasStart()) {
+ plannerInfo.append("- Quest available from ");
+ plannerInfo.append(TimeFormatter.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("
");
+ }
+
+ //Quest availability repeats
+ if (planner.hasRepeat()) {
+ plannerInfo.append("- Quest will become available again after ");
+ plannerInfo.append(TimeFormatter.getDurationString(planner.getRepeat() / 1000)).append("
");
+ }
+
+ plannerInfo.append("
");
+ return plannerInfo.toString();
+ }
+
+}
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
new file mode 100644
index 0000000..426b2ff
--- /dev/null
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRequirementsInfoGenerator.java
@@ -0,0 +1,108 @@
+package net.knarcraft.dynmapcitizens.handler.trait.quests;
+
+import me.blackvein.quests.quests.IQuest;
+import me.blackvein.quests.quests.Requirements;
+import net.knarcraft.dynmapcitizens.util.QuestsHelper;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A class to generate a string containing all information about a quest's requirements
+ */
+public class QuestRequirementsInfoGenerator {
+
+ private final IQuest quest;
+
+ /**
+ * Instantiates a new quest requirement info generator
+ *
+ * @param quest The quest to generate information about
+ */
+ public QuestRequirementsInfoGenerator(IQuest quest) {
+ this.quest = quest;
+ }
+
+ /**
+ * Gets information about all requirements for the given quest
+ *
+ * @return Information about the quest's requirements
+ */
+ public String getQuestRequirementsInfo() {
+ Requirements requirements = quest.getRequirements();
+ StringBuilder requirementInfo = new StringBuilder();
+ if (!requirements.hasRequirement()) {
+ return requirementInfo.toString();
+ }
+
+ requirementInfo.append("Requirements: ");
+
+ if (requirements.getQuestPoints() > 0) {
+ requirementInfo.append("- ").append(requirements.getQuestPoints()).append(" quest points
");
+ }
+
+ if (requirements.getExp() > 0) {
+ requirementInfo.append("- ").append(requirements.getExp()).append(" exp
");
+ }
+
+ if (!requirements.getBlockQuests().isEmpty()) {
+ requirementInfo.append("- Blocked by quests:
");
+ for (IQuest blockQuest : requirements.getBlockQuests()) {
+ requirementInfo.append("- ").append(blockQuest.getName()).append("
");
+ }
+ requirementInfo.append("
");
+ }
+
+ if (!requirements.getNeededQuests().isEmpty()) {
+ requirementInfo.append("- Required quests:
");
+ for (IQuest neededQuest : requirements.getNeededQuests()) {
+ requirementInfo.append("- ").append(neededQuest.getName()).append("
");
+ }
+ requirementInfo.append("
");
+ }
+
+ if (!requirements.getItems().isEmpty()) {
+ requirementInfo.append("- Required items:
");
+ for (ItemStack item : requirements.getItems()) {
+ requirementInfo.append("- ").append(QuestsHelper.getUpperCasedItemStackString(item)).append("
");
+ }
+ requirementInfo.append("
");
+ }
+
+ if (!requirements.getMcmmoSkills().isEmpty()) {
+ List skills = requirements.getMcmmoSkills();
+ List amounts = requirements.getMcmmoAmounts();
+ 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("
");
+ }
+ }
+
+ if (!requirements.getPermissions().isEmpty()) {
+ requirementInfo.append("- Required permissions:
");
+ for (String permission : requirements.getPermissions()) {
+ requirementInfo.append("- ").append(permission).append("
");
+ }
+ requirementInfo.append("
");
+ }
+
+ Map> customRequirementPlugins = requirements.getCustomRequirements();
+ for (String plugin : customRequirementPlugins.keySet()) {
+ requirementInfo.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("
");
+ }
+ requirementInfo.append("
");
+ }
+
+ requirementInfo.append("
");
+ return requirementInfo.toString();
+ }
+
+}
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRewardsInfoGenerator.java b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRewardsInfoGenerator.java
new file mode 100644
index 0000000..383aa44
--- /dev/null
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestRewardsInfoGenerator.java
@@ -0,0 +1,58 @@
+package net.knarcraft.dynmapcitizens.handler.trait.quests;
+
+import me.blackvein.quests.quests.IQuest;
+import me.blackvein.quests.quests.Rewards;
+import net.knarcraft.dynmapcitizens.util.QuestsHelper;
+import org.bukkit.inventory.ItemStack;
+
+/**
+ * A class to generate a string containing all information about a quest's rewards
+ */
+public class QuestRewardsInfoGenerator {
+
+ private final IQuest quest;
+
+ /**
+ * Instantiates a new quest reward info generator
+ *
+ * @param quest The quest to generate information about
+ */
+ public QuestRewardsInfoGenerator(IQuest quest) {
+ this.quest = quest;
+ }
+
+ /**
+ * Gets information about all rewards for the given quest
+ *
+ * @return Information about the quest's rewards
+ */
+ public String getQuestRewardsInfo() {
+ Rewards reward = quest.getRewards();
+ StringBuilder rewardInfo = new StringBuilder();
+ rewardInfo.append("Rewards:");
+
+ if (reward.getMoney() > 0) {
+ rewardInfo.append("- ").append(reward.getMoney()).append(" ").append(QuestsHelper.getCurrency(reward.getMoney())).append("
");
+ }
+
+ if (reward.getExp() > 0) {
+ rewardInfo.append("- ").append(reward.getExp()).append(" exp").append("
");
+ }
+
+ for (String permission : reward.getPermissions()) {
+ rewardInfo.append("- ").append("Permission: ").append(permission).append("
");
+ }
+
+ for (ItemStack item : reward.getItems()) {
+ rewardInfo.append("- ").append(QuestsHelper.getUpperCasedItemStackString(item)).append("
");
+ }
+
+ for (String command : reward.getCommands()) {
+ rewardInfo.append("- Command: ").append(command).append("
");
+ }
+
+ rewardInfo.append("
");
+ return rewardInfo.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
new file mode 100644
index 0000000..dbea1c1
--- /dev/null
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestStagesInfoGenerator.java
@@ -0,0 +1,109 @@
+package net.knarcraft.dynmapcitizens.handler.trait.quests;
+
+import me.blackvein.quests.quests.IQuest;
+import me.blackvein.quests.quests.IStage;
+import net.citizensnpcs.api.CitizensAPI;
+import net.citizensnpcs.api.npc.NPC;
+import net.citizensnpcs.api.npc.NPCRegistry;
+import net.knarcraft.dynmapcitizens.util.QuestsHelper;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * A class to generate a string containing all information about a quest's stages
+ */
+public class QuestStagesInfoGenerator {
+
+ private final IQuest quest;
+
+ /**
+ * Instantiates a new quest stages info generator
+ *
+ * @param quest The quest to generate information about
+ */
+ public QuestStagesInfoGenerator(IQuest quest) {
+ this.quest = quest;
+ }
+
+ /**
+ * Gets information about a quest's stages
+ *
+ * @return A string with information about the quest's stages
+ */
+ public String getQuestStagesInfo() {
+ StringBuilder questInfo = new StringBuilder();
+ NPCRegistry registry = CitizensAPI.getNPCRegistry();
+ int stageCounter = 1;
+ questInfo.append("Stages:");
+ for (IStage stage : quest.getStages()) {
+ questInfo.append("- Stage ").append(stageCounter).append(" tasks:
");
+ int mobTypes = stage.getMobsToKill().size();
+ for (int i = 0; i < mobTypes; i++) {
+ questInfo.append("- Kill ").append(QuestsHelper.normalizeName(stage.getMobsToKill().get(i).name())).append(
+ " X ").append(stage.getMobNumToKill().get(i)).append("
");
+ }
+ int deliveries = stage.getItemDeliveryTargets().size();
+ for (int i = 0; i < deliveries; i++) {
+ NPC npc = registry.getByUniqueId(stage.getItemDeliveryTargets().get(i));
+ questInfo.append("- Deliver ").append(QuestsHelper.getItemStackString(stage.getItemsToDeliver().get(i))).append(
+ " to ").append(npc.getName()).append("
");
+ }
+ if (stage.getFishToCatch() != null) {
+ questInfo.append("- Catch ").append(stage.getFishToCatch()).append(" fish").append("
");
+ }
+ for (UUID npcId : stage.getNpcsToKill()) {
+ questInfo.append("- Kill NPC ").append(registry.getByUniqueId(npcId).getName()).append("
");
+ }
+
+ questInfo.append(getQuestItemsTaskString(stage.getBlocksToBreak(), "- Break ")).append("
");
+ questInfo.append(getQuestItemsTaskString(stage.getBlocksToCut(), "- Cut ")).append("
");
+ questInfo.append(getQuestItemsTaskString(stage.getBlocksToDamage(), "- Damage ")).append("
");
+ questInfo.append(getQuestItemsTaskString(stage.getBlocksToUse(), "- Use ")).append("
");
+ questInfo.append(getQuestItemsTaskString(stage.getBlocksToPlace(), "- Place ")).append("
");
+
+ questInfo.append(getQuestItemsTaskString(stage.getItemsToBrew(), "- Brew ")).append("
");
+ questInfo.append(getQuestItemsTaskString(stage.getItemsToConsume(), "- Consume ")).append("
");
+ questInfo.append(getQuestItemsTaskString(stage.getItemsToCraft(), "- Craft ")).append("
");
+ questInfo.append(getQuestItemsTaskString(stage.getItemsToEnchant(), "- Enchant ")).append("
");
+ questInfo.append(getQuestItemsTaskString(stage.getItemsToSmelt(), "- Smelt ")).append("
");
+
+ int sheepTypes = stage.getSheepToShear().size();
+ for (int i = 0; i < sheepTypes; i++) {
+ questInfo.append("- Shear ").append(stage.getSheepNumToShear().get(i)).append(" ").append(
+ QuestsHelper.normalizeName(stage.getSheepToShear().get(i).name())).append(" sheep").append("
");
+ }
+ if (stage.getCowsToMilk() != null) {
+ questInfo.append("- Milk ").append(stage.getCowsToMilk()).append(" cows").append("
");
+ }
+
+ int mobTamingEntries = stage.getMobsToTame().size();
+ for (int i = 0; i < mobTamingEntries; i++) {
+ questInfo.append("- Tame ").append(stage.getMobNumToTame().get(i)).append(" ").append(
+ QuestsHelper.normalizeName(stage.getMobsToTame().get(i).name())).append("
");
+ }
+
+ questInfo.append("
");
+ stageCounter++;
+ }
+ questInfo.append("
");
+ return questInfo.toString();
+ }
+
+ /**
+ * Gets a string to display a quest task involving some action on an item
+ *
+ * @param items The items that are part of the task
+ * @param explanation The explanation of what the player needs to do with the items
+ * @return A string describing the necessary tasks
+ */
+ private String getQuestItemsTaskString(List items, String explanation) {
+ StringBuilder questInfo = new StringBuilder();
+ for (ItemStack itemStack : items) {
+ questInfo.append(explanation).append(QuestsHelper.getItemStackString(itemStack));
+ }
+ return questInfo.toString();
+ }
+
+}
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
new file mode 100644
index 0000000..c9b482f
--- /dev/null
+++ b/src/main/java/net/knarcraft/dynmapcitizens/handler/trait/quests/QuestsHandler.java
@@ -0,0 +1,266 @@
+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.citizensnpcs.api.CitizensAPI;
+import net.citizensnpcs.api.npc.NPCRegistry;
+import net.knarcraft.dynmapcitizens.DynmapCitizens;
+import net.knarcraft.dynmapcitizens.handler.trait.AbstractTraitHandler;
+import net.knarcraft.dynmapcitizens.property.Icon;
+import net.knarcraft.dynmapcitizens.property.UpdateRate;
+import net.knarcraft.dynmapcitizens.util.QuestsHelper;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.dynmap.DynmapAPI;
+import org.dynmap.markers.CircleMarker;
+import org.dynmap.markers.GenericMarker;
+import org.dynmap.markers.MarkerIcon;
+import org.dynmap.markers.MarkerSet;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.logging.Level;
+
+/**
+ * A handler class for the quests trait
+ */
+public class QuestsHandler extends AbstractTraitHandler {
+
+ private QuestsAPI questsAPI;
+ private MarkerSet questMarkerSet;
+ private MarkerSet questAreaMarkerSet;
+ private Map markerIcons;
+ private Collection loadedQuests;
+ private Map questGiverInfo;
+
+ @Override
+ public void initialize() {
+ questsAPI = (QuestsAPI) Bukkit.getServer().getPluginManager().getPlugin("Quests");
+ DynmapAPI dynmapAPI = DynmapCitizens.getInstance().getDynmapAPI();
+ markerIcons = DynmapCitizens.getInstance().getMarkerIcons();
+ if (questsAPI != null) {
+ questMarkerSet = getMarkerSet(dynmapAPI, "quests", "Quests");
+ questAreaMarkerSet = getMarkerSet(dynmapAPI, "quest_areas", "Quest areas");
+ if (questMarkerSet != null && questAreaMarkerSet != null) {
+ questMarkerSet.setHideByDefault(false);
+ questAreaMarkerSet.setHideByDefault(true);
+ questMarkerSet.setLayerPriority(3);
+ questAreaMarkerSet.setLayerPriority(2);
+ isEnabled = true;
+ return;
+ }
+ }
+ isEnabled = false;
+ }
+
+ @Override
+ public UpdateRate getUpdateRate() {
+ return UpdateRate.VERY_SLOW;
+ }
+
+ @Override
+ public void updateMarkers() {
+ if (questsAPI.isLoading()) {
+ return;
+ }
+
+ //There is no point in updating if there has been no changes in quests
+ boolean questsChanged = loadedQuests == null || !loadedQuests.equals(questsAPI.getLoadedQuests());
+ loadedQuests = questsAPI.getLoadedQuests();
+
+ //Remove old quest markers
+ questMarkerSet.getMarkers().forEach(GenericMarker::deleteMarker);
+
+ //Updates all quest area markers
+ if (questsChanged) {
+ updateQuestAreas();
+ }
+
+ //Generate information about all NPCs involved in quests
+ generateQuestNPCInfo();
+
+ //Generate markers based on the generated info
+ generateAllMarkers();
+ }
+
+ /**
+ * Generates information about all NPCs involved in quests
+ */
+ private void generateQuestNPCInfo() {
+ //Clear any previous information
+ questGiverInfo = new HashMap<>();
+ //Generation information about NPC's parts in each quest
+ for (IQuest quest : questsAPI.getLoadedQuests()) {
+ if (quest.getNpcStart() != null) {
+ getInfo(quest.getNpcStart()).addQuestStart(quest);
+ }
+ for (IStage stage : quest.getStages()) {
+ for (UUID npcId : stage.getNpcsToKill()) {
+ getInfo(npcId).addQuestKill(quest);
+ }
+ for (UUID npcId : stage.getItemDeliveryTargets()) {
+ getInfo(npcId).addQuestDeliver(quest);
+ }
+ for (UUID npcId : stage.getNpcsToInteract()) {
+ getInfo(npcId).addQuestInteract(quest);
+ }
+ }
+ }
+ }
+
+ /**
+ * Generates all quest markers based on the previously generated quest NPC info
+ */
+ private void generateAllMarkers() {
+ NPCRegistry registry = CitizensAPI.getNPCRegistry();
+ //Add markers for each NPC detected as part of a quest
+ for (UUID npcId : questGiverInfo.keySet()) {
+ NPCQuestInfo info = questGiverInfo.get(npcId);
+ MarkerIcon icon = markerIcons.get(info.getNPCIcon());
+ List questStarts = info.getQuestStarts();
+ StringBuilder markerDescription = new StringBuilder();
+
+ markerDescription.append("").append(registry.getByUniqueId(npcId).getName()).append("
");
+
+ if (!questStarts.isEmpty()) {
+ markerDescription.append("Quests offered:
");
+ for (IQuest quest : questStarts) {
+ markerDescription.append("").append(quest.getName()).append("
- ");
+ markerDescription.append(quest.getDescription()).append("
").append(
+ new QuestStagesInfoGenerator(quest).getQuestStagesInfo());
+ markerDescription.append(new QuestRewardsInfoGenerator(quest).getQuestRewardsInfo());
+ markerDescription.append(new QuestRequirementsInfoGenerator(quest).getQuestRequirementsInfo());
+ markerDescription.append(new QuestPlannerInfoGenerator(quest).getQuestPlannerInfo()).append(" ");
+ }
+ markerDescription.append("
");
+ }
+
+ markerDescription.append(getInvolvedInQuestsString(info));
+
+ addNPCMarker(npcId, QuestsHelper.getMarkerTitle(info.getQuestNPCType()), markerDescription.toString(), icon, questMarkerSet);
+ }
+ }
+
+ /**
+ * Gets information about the quests an NPC is involved in
+ *
+ * @param info The NPC info to look through
+ * @return Information about an NPC's involvement in different quests
+ */
+ private String getInvolvedInQuestsString(NPCQuestInfo info) {
+ List questKills = info.getQuestKills();
+ List questInteractions = info.getQuestInteractions();
+ List questDeliveries = info.getQuestDeliveries();
+
+ if (questKills.isEmpty() && questInteractions.isEmpty() && questDeliveries.isEmpty()) {
+ return "";
+ }
+ StringBuilder markerDescription = new StringBuilder();
+ markerDescription.append("Involved in quests:
");
+ addInvolvedInString("Killed in", questKills, markerDescription);
+ addInvolvedInString("Delivery target in", questDeliveries, markerDescription);
+ addInvolvedInString("Interacted with in quest", questInteractions, markerDescription);
+ markerDescription.append("
");
+ return markerDescription.toString();
+ }
+
+ /**
+ * Adds string explaining how an NPC is involved with the given quests
+ *
+ * @param prefix The string explaining how the NPC is involved
+ * @param quests The quests the NPC is involved in
+ * @param builder The string builder to append to
+ */
+ private void addInvolvedInString(String prefix, List quests, StringBuilder builder) {
+ for (IQuest quest : new HashSet<>(quests)) {
+ builder.append("").append(prefix).append(": ").append(quest.getName()).append("");
+ }
+ }
+
+
+ /**
+ * Gets the info object for the given NPC
+ *
+ * @param npcId The id of the NPC to get information about
+ * @return The NPC's info object
+ */
+ private NPCQuestInfo getInfo(UUID npcId) {
+ if (questGiverInfo.get(npcId) == null) {
+ questGiverInfo.put(npcId, new NPCQuestInfo());
+ }
+ return questGiverInfo.get(npcId);
+ }
+
+ /**
+ * Updates all quest area markers
+ */
+ private void updateQuestAreas() {
+ questAreaMarkerSet.getCircleMarkers().forEach(GenericMarker::deleteMarker);
+ for (IQuest quest : questsAPI.getLoadedQuests()) {
+ for (IStage stage : quest.getStages()) {
+ markKillLocations(quest, stage);
+ markReachLocations(quest, stage);
+ }
+ }
+ //TODO: Mark WorldGuard areas part of quests. Requires WorldGuard integration
+ }
+
+ /**
+ * Marks any reach locations found in the given stage
+ *
+ * @param quest The quest the stage belongs to
+ * @param stage The stage to search for reach locations
+ */
+ private void markReachLocations(IQuest quest, IStage stage) {
+ markLocations(stage.getLocationsToReach(), stage.getRadiiToReachWithin(),
+ "Target location for: " + quest.getName());
+ }
+
+ /**
+ * Marks any kill locations found in the given stage
+ *
+ * @param quest The quest the stage belongs to
+ * @param stage The stage to search for kill locations
+ */
+ private void markKillLocations(IQuest quest, IStage stage) {
+ markLocations(stage.getLocationsToKillWithin(), stage.getRadiiToKillWithin(),
+ "Kill location for: " + quest.getName());
+ }
+
+ /**
+ * Marks the given locations on the dynamic map
+ *
+ * @param locations The locations to mark
+ * @param radii The radius of each location's circle
+ * @param description The description for what the location means
+ */
+ private void markLocations(List locations, List radii, String description) {
+ for (int i = 0; i < locations.size(); i++) {
+ Location location = locations.get(i);
+ int radius = radii.get(i);
+
+ //Skip if location is invalid
+ World world = location.getWorld();
+ if (world == null) {
+ continue;
+ }
+
+ CircleMarker circleMarker = questAreaMarkerSet.createCircleMarker(null, description, true,
+ world.getName(), location.getX(), location.getY(), location.getZ(), radius, radius, false);
+ if (circleMarker == null) {
+ DynmapCitizens.getInstance().getLogger().log(Level.WARNING, "Unable to create circle marker at " +
+ location + " with radius " + radius);
+ } else {
+ circleMarker.setFillStyle(0.3, 0x75AFD2);
+ circleMarker.setLineStyle(1, 1.0, 0x36c90e);
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/Icon.java b/src/main/java/net/knarcraft/dynmapcitizens/property/Icon.java
similarity index 94%
rename from src/main/java/net/knarcraft/dynmapcitizens/Icon.java
rename to src/main/java/net/knarcraft/dynmapcitizens/property/Icon.java
index 4487d96..b0b5ee0 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/Icon.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/property/Icon.java
@@ -1,4 +1,4 @@
-package net.knarcraft.dynmapcitizens;
+package net.knarcraft.dynmapcitizens.property;
/**
* Every icon type used
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/UpdateRate.java b/src/main/java/net/knarcraft/dynmapcitizens/property/UpdateRate.java
similarity index 94%
rename from src/main/java/net/knarcraft/dynmapcitizens/UpdateRate.java
rename to src/main/java/net/knarcraft/dynmapcitizens/property/UpdateRate.java
index b5f6d51..7d5e55d 100644
--- a/src/main/java/net/knarcraft/dynmapcitizens/UpdateRate.java
+++ b/src/main/java/net/knarcraft/dynmapcitizens/property/UpdateRate.java
@@ -1,4 +1,4 @@
-package net.knarcraft.dynmapcitizens;
+package net.knarcraft.dynmapcitizens.property;
/**
* An update rate for a group of icons
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/trait/quests/QuestsHandler.java b/src/main/java/net/knarcraft/dynmapcitizens/trait/quests/QuestsHandler.java
deleted file mode 100644
index 3e69e57..0000000
--- a/src/main/java/net/knarcraft/dynmapcitizens/trait/quests/QuestsHandler.java
+++ /dev/null
@@ -1,604 +0,0 @@
-package net.knarcraft.dynmapcitizens.trait.quests;
-
-import me.blackvein.quests.QuestsAPI;
-import me.blackvein.quests.quests.IQuest;
-import me.blackvein.quests.quests.IStage;
-import me.blackvein.quests.quests.Planner;
-import me.blackvein.quests.quests.Requirements;
-import me.blackvein.quests.quests.Rewards;
-import net.citizensnpcs.api.CitizensAPI;
-import net.citizensnpcs.api.npc.NPC;
-import net.citizensnpcs.api.npc.NPCRegistry;
-import net.knarcraft.dynmapcitizens.DynmapCitizens;
-import net.knarcraft.dynmapcitizens.Icon;
-import net.knarcraft.dynmapcitizens.UpdateRate;
-import net.knarcraft.dynmapcitizens.VaultHandler;
-import net.knarcraft.dynmapcitizens.trait.AbstractTraitHandler;
-import org.bukkit.Bukkit;
-import org.bukkit.Location;
-import org.bukkit.World;
-import org.bukkit.inventory.ItemStack;
-import org.dynmap.DynmapAPI;
-import org.dynmap.markers.CircleMarker;
-import org.dynmap.markers.GenericMarker;
-import org.dynmap.markers.MarkerIcon;
-import org.dynmap.markers.MarkerSet;
-
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-import java.util.logging.Level;
-
-import static net.knarcraft.blacksmith.formatting.StringFormatter.replacePlaceholder;
-
-/**
- * A handler class for the quests trait
- */
-public class QuestsHandler extends AbstractTraitHandler {
-
- private QuestsAPI questsAPI;
- private MarkerSet questMarkerSet;
- private MarkerSet questAreaMarkerSet;
- private Map markerIcons;
- private Collection loadedQuests;
- private Map questGiverInfo;
-
- @Override
- public void initialize() {
- questsAPI = (QuestsAPI) Bukkit.getServer().getPluginManager().getPlugin("Quests");
- DynmapAPI dynmapAPI = DynmapCitizens.getInstance().getDynmapAPI();
- markerIcons = DynmapCitizens.getInstance().getMarkerIcons();
- if (questsAPI != null) {
- questMarkerSet = getMarkerSet(dynmapAPI, "quests", "Quests");
- questAreaMarkerSet = getMarkerSet(dynmapAPI, "quest_areas", "Quest areas");
- if (questMarkerSet != null && questAreaMarkerSet != null) {
- questMarkerSet.setHideByDefault(false);
- questAreaMarkerSet.setHideByDefault(true);
- questMarkerSet.setLayerPriority(3);
- questAreaMarkerSet.setLayerPriority(2);
- isEnabled = true;
- return;
- }
- }
- isEnabled = false;
- }
-
- @Override
- public UpdateRate getUpdateRate() {
- return UpdateRate.VERY_SLOW;
- }
-
- @Override
- public void updateMarkers() {
- if (questsAPI.isLoading()) {
- return;
- }
-
- questGiverInfo = new HashMap<>();
-
- //There is no point in updating if there has been no changes in quests
- boolean questsChanged = loadedQuests == null || !loadedQuests.equals(questsAPI.getLoadedQuests());
- loadedQuests = questsAPI.getLoadedQuests();
-
- //Remove old quest markers
- questMarkerSet.getMarkers().forEach(GenericMarker::deleteMarker);
-
- //Updates all quest area markers
- if (questsChanged) {
- updateQuestAreas();
- }
-
- NPCRegistry registry = CitizensAPI.getNPCRegistry();
-
- //Generation information about NPC's parts in each quest
- for (IQuest quest : questsAPI.getLoadedQuests()) {
- if (quest.getNpcStart() != null) {
- getInfo(quest.getNpcStart()).addQuestStart(quest);
- }
- for (IStage stage : quest.getStages()) {
- for (UUID npcId : stage.getNpcsToKill()) {
- getInfo(npcId).addQuestKill(quest);
- }
- for (UUID npcId : stage.getItemDeliveryTargets()) {
- getInfo(npcId).addQuestDeliver(quest);
- }
- for (UUID npcId : stage.getNpcsToInteract()) {
- getInfo(npcId).addQuestInteract(quest);
- }
- }
- }
-
- //Add markers for each NPC detected as part of a quest
- for (UUID npcId : questGiverInfo.keySet()) {
- NPCQuestInfo info = questGiverInfo.get(npcId);
- MarkerIcon icon = markerIcons.get(info.getNPCIcon());
- List questStarts = info.getQuestStarts();
- List questKills = info.getQuestKills();
- List questInteractions = info.getQuestInteractions();
- List questDeliveries = info.getQuestDeliveries();
- StringBuilder markerDescription = new StringBuilder();
-
- markerDescription.append("").append(registry.getByUniqueId(npcId).getName()).append("
");
-
- if (!questStarts.isEmpty()) {
- markerDescription.append("Quests offered:
");
- for (IQuest quest : questStarts) {
- markerDescription.append("").append(quest.getName()).append("
- ");
- markerDescription.append(quest.getDescription()).append("
").append(getQuestStagesInfo(quest));
- markerDescription.append(getQuestRewardsInfo(quest)).append(getQuestRequirementsInfo(quest));
- markerDescription.append(getQuestPlannerInfo(quest)).append(" ");
- }
- markerDescription.append("
");
- }
-
- if (!questKills.isEmpty() || !questInteractions.isEmpty() || !questDeliveries.isEmpty()) {
- markerDescription.append("Involved in quests:
");
-
- for (IQuest quest : new HashSet<>(questKills)) {
- markerDescription.append("- Killed in: ").append(quest.getName()).append("
");
- }
- for (IQuest quest : new HashSet<>(questDeliveries)) {
- markerDescription.append("- Delivery target in: ").append(quest.getName()).append("
");
- }
- for (IQuest quest : new HashSet<>(questInteractions)) {
- markerDescription.append("- Interacted with in quest: ").append(quest.getName()).append("
");
- }
-
- markerDescription.append("
");
- }
-
- addNPCMarker(npcId, getMarkerTitle(info.getQuestNPCType()), markerDescription.toString(), icon, questMarkerSet);
- }
- }
-
- /**
- * Gets information about the time-availability of a quest
- *
- * @param quest The quest to get planner info from
- * @return Information about when the quest is available
- */
- private String getQuestPlannerInfo(IQuest quest) {
- Planner planner = quest.getPlanner();
- StringBuilder plannerInfo = new StringBuilder();
- plannerInfo.append("Planner:");
-
- //Quest can be repeated after a cool-down
- if (planner.hasCooldown()) {
- plannerInfo.append("- Quest repeatable after: ").append(getDurationString(planner.getCooldown() / 1000));
- plannerInfo.append("
");
- } else {
- plannerInfo.append("- Quest cannot be repeated!
");
- }
-
- //Quest only becomes available after the start date
- if (planner.hasStart()) {
- DateFormat format = new SimpleDateFormat("dd MM yyyy HH:mm:ss");
- Date date = new Date(planner.getStartInMillis());
- plannerInfo.append("- Quest available from ").append(format.format(date)).append("
");
- }
-
- //Quest is only available until the end date
- if (planner.hasEnd()) {
- DateFormat format = new SimpleDateFormat("dd MM yyyy HH:mm:ss");
- Date date = new Date(planner.getEndInMillis());
- plannerInfo.append("- Quest available until ").append(format.format(date)).append("
");
- }
-
- //Quest availability repeats
- if (planner.hasRepeat()) {
- plannerInfo.append("- Quest will become available again after ");
- plannerInfo.append(getDurationString(planner.getRepeat() / 1000)).append("
");
- }
-
- plannerInfo.append("
");
- return plannerInfo.toString();
- }
-
- /**
- * Gets the string used for displaying this sign's duration
- *
- * @return The string used for displaying this sign's duration
- */
- public 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 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 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));
- }
-
- /**
- * Gets information about all requirements for the given quest
- *
- * @param quest The quest to get requirements for
- * @return Information about the quest's requirements
- */
- private String getQuestRequirementsInfo(IQuest quest) {
- Requirements requirements = quest.getRequirements();
- StringBuilder requirementInfo = new StringBuilder();
- if (!requirements.hasRequirement()) {
- return requirementInfo.toString();
- }
-
- requirementInfo.append("Requirements: ");
-
- if (requirements.getQuestPoints() > 0) {
- requirementInfo.append("- ").append(requirements.getQuestPoints()).append(" quest points
");
- }
-
- if (requirements.getExp() > 0) {
- requirementInfo.append("- ").append(requirements.getExp()).append(" exp
");
- }
-
- if (!requirements.getBlockQuests().isEmpty()) {
- requirementInfo.append("- Blocked by quests:
");
- for (IQuest blockQuest : requirements.getBlockQuests()) {
- requirementInfo.append("- ").append(blockQuest.getName()).append("
");
- }
- requirementInfo.append("
");
- }
-
- if (!requirements.getNeededQuests().isEmpty()) {
- requirementInfo.append("- Required quests:
");
- for (IQuest neededQuest : requirements.getNeededQuests()) {
- requirementInfo.append("- ").append(neededQuest.getName()).append("
");
- }
- requirementInfo.append("
");
- }
-
- if (!requirements.getItems().isEmpty()) {
- requirementInfo.append("- Required items:
");
- for (ItemStack item : requirements.getItems()) {
- requirementInfo.append("- ").append(uppercaseFirst(getItemStackString(item))).append("
");
- }
- requirementInfo.append("
");
- }
-
- if (!requirements.getMcmmoSkills().isEmpty()) {
- List skills = requirements.getMcmmoSkills();
- List amounts = requirements.getMcmmoAmounts();
- 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("
");
- }
- }
-
- if (!requirements.getPermissions().isEmpty()) {
- requirementInfo.append("- Required permissions:
");
- for (String permission : requirements.getPermissions()) {
- requirementInfo.append("- ").append(permission).append("
");
- }
- requirementInfo.append("
");
- }
-
- Map> customRequirementPlugins = requirements.getCustomRequirements();
- for (String plugin : customRequirementPlugins.keySet()) {
- requirementInfo.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(" ").append(customRequirementEntry.get(requirementDescription)).append("
");
- }
- requirementInfo.append("
");
- }
-
- requirementInfo.append("
");
- return requirementInfo.toString();
- }
-
- /**
- * Gets information about all rewards for the given quest
- *
- * @param quest The quest to get reward information for
- * @return Information about the quest's rewards
- */
- private String getQuestRewardsInfo(IQuest quest) {
- Rewards reward = quest.getRewards();
- StringBuilder rewardInfo = new StringBuilder();
- rewardInfo.append("Rewards:");
-
- if (reward.getMoney() > 0) {
- rewardInfo.append("- ").append(reward.getMoney()).append(" ").append(getCurrency(reward.getMoney())).append("
");
- }
-
- if (reward.getExp() > 0) {
- rewardInfo.append("- ").append(reward.getExp()).append(" exp").append("
");
- }
-
- for (String permission : reward.getPermissions()) {
- rewardInfo.append("- ").append("Permission: ").append(permission).append("
");
- }
-
- for (ItemStack item : reward.getItems()) {
- rewardInfo.append("- ").append(uppercaseFirst(getItemStackString(item))).append("
");
- }
-
- for (String command : reward.getCommands()) {
- rewardInfo.append("- Command: ").append(command).append("
");
- }
-
- rewardInfo.append("
");
- return rewardInfo.toString();
- }
-
- /**
- * Gets the currency to print for the given amount of money
- *
- * @param money The amount to pay/use
- * @return The currency name to use
- */
- private String getCurrency(double money) {
- VaultHandler vaultHandler = DynmapCitizens.getInstance().getVaultHandler();
- if (vaultHandler.isEnabled()) {
- return vaultHandler.getCurrency(money != 1);
- } else {
- return "money";
- }
- }
-
- /**
- * Gets the marker title to use for the given quest NPC type
- *
- * @param type The type of marker to get the title for
- * @return The title to use for the marker
- */
- private String getMarkerTitle(QuestNPCType type) {
- return switch (type) {
- case GIVER -> "Quest Start NPC: ";
- case INTERACT -> "Quest Interact NPC: ";
- case DELIVER -> "Quest Deliver NPC: ";
- case KILL -> "Quest Kill NPC: ";
- case CHAIN -> "Quest Chain NPC: ";
- };
- }
-
- /**
- * Gets the info object for the given NPC
- *
- * @param npcId The id of the NPC to get information about
- * @return The NPC's info object
- */
- private NPCQuestInfo getInfo(UUID npcId) {
- if (questGiverInfo.get(npcId) == null) {
- questGiverInfo.put(npcId, new NPCQuestInfo());
- }
- return questGiverInfo.get(npcId);
- }
-
- /**
- * Updates all quest area markers
- */
- private void updateQuestAreas() {
- questAreaMarkerSet.getCircleMarkers().forEach(GenericMarker::deleteMarker);
- for (IQuest quest : questsAPI.getLoadedQuests()) {
- for (IStage stage : quest.getStages()) {
- markKillLocations(quest, stage);
- markReachLocations(quest, stage);
- }
- }
- //TODO: Mark WorldGuard areas part of quests. Requires WorldGuard integration
- }
-
- /**
- * Gets information about a quest's stages
- *
- * @param quest The quest to get information about
- * @return A string with information about the quest's stages
- */
- private String getQuestStagesInfo(IQuest quest) {
- StringBuilder questInfo = new StringBuilder();
- NPCRegistry registry = CitizensAPI.getNPCRegistry();
- for (IStage stage : quest.getStages()) {
- questInfo.append("Tasks:");
- int mobTypes = stage.getMobsToKill().size();
- for (int i = 0; i < mobTypes; i++) {
- questInfo.append("- Kill ").append(normalizeName(stage.getMobsToKill().get(i).name())).append(
- " X ").append(stage.getMobNumToKill().get(i)).append("
");
- }
- int deliveries = stage.getItemDeliveryTargets().size();
- for (int i = 0; i < deliveries; i++) {
- NPC npc = registry.getByUniqueId(stage.getItemDeliveryTargets().get(i));
- questInfo.append("- Deliver ").append(getItemStackString(stage.getItemsToDeliver().get(i))).append(
- " to ").append(npc.getName()).append("
");
- }
- if (stage.getFishToCatch() != null) {
- questInfo.append("- Catch ").append(stage.getFishToCatch()).append(" fish").append("
");
- }
- for (UUID npcId : stage.getNpcsToKill()) {
- questInfo.append("- Kill NPC ").append(registry.getByUniqueId(npcId).getName()).append("
");
- }
-
- questInfo.append(getQuestItemsTaskString(stage.getBlocksToBreak(), "- Break ")).append("
");
- questInfo.append(getQuestItemsTaskString(stage.getBlocksToCut(), "- Cut ")).append("
");
- questInfo.append(getQuestItemsTaskString(stage.getBlocksToDamage(), "- Damage ")).append("
");
- questInfo.append(getQuestItemsTaskString(stage.getBlocksToUse(), "- Use ")).append("
");
- questInfo.append(getQuestItemsTaskString(stage.getBlocksToPlace(), "- Place ")).append("
");
-
- questInfo.append(getQuestItemsTaskString(stage.getItemsToBrew(), "- Brew ")).append("
");
- questInfo.append(getQuestItemsTaskString(stage.getItemsToConsume(), "- Consume ")).append("
");
- questInfo.append(getQuestItemsTaskString(stage.getItemsToCraft(), "- Craft ")).append("
");
- questInfo.append(getQuestItemsTaskString(stage.getItemsToEnchant(), "- Enchant ")).append("
");
- questInfo.append(getQuestItemsTaskString(stage.getItemsToSmelt(), "- Smelt ")).append("
");
-
- int sheepTypes = stage.getSheepToShear().size();
- for (int i = 0; i < sheepTypes; i++) {
- questInfo.append("- Shear ").append(stage.getSheepNumToShear().get(i)).append(" ").append(
- normalizeName(stage.getSheepToShear().get(i).name())).append(" sheep").append("
");
- }
- if (stage.getCowsToMilk() != null) {
- questInfo.append("- Milk ").append(stage.getCowsToMilk()).append(" cows").append("
");
- }
-
- int mobTamingEntries = stage.getMobsToTame().size();
- for (int i = 0; i < mobTamingEntries; i++) {
- questInfo.append("- Tame ").append(stage.getMobNumToTame().get(i)).append(" ").append(
- normalizeName(stage.getMobsToTame().get(i).name())).append("
");
- }
-
- questInfo.append("
");
- }
- return questInfo.toString();
- }
-
- /**
- * Gets a string to display a quest task involving some action on an item
- *
- * @param items The items that are part of the task
- * @param explanation The explanation of what the player needs to do with the items
- * @return A string describing the necessary tasks
- */
- private String getQuestItemsTaskString(List items, String explanation) {
- StringBuilder questInfo = new StringBuilder();
- for (ItemStack itemStack : items) {
- questInfo.append(explanation).append(getItemStackString(itemStack));
- }
- return questInfo.toString();
- }
-
- /**
- * Gets the proper string representation of an item stack
- *
- * @param itemStack The item stack to print
- * @return The string representation of the item stack
- */
- private String getItemStackString(ItemStack itemStack) {
- return normalizeName(itemStack.getType().name()) + " X " + itemStack.getAmount();
- }
-
- /**
- * Makes the first character in a string uppercase
- *
- * @param string The string to run on
- * @return The same string, with the first character converted to uppercase
- */
- private String uppercaseFirst(String string) {
- return string.substring(0, 1).toUpperCase() + string.substring(1);
- }
-
- /**
- * Normalizes an internal name to make it human-readable
- *
- * @param name The name to normalize
- * @return The normalized name
- */
- private String normalizeName(String name) {
- return name.toLowerCase().replace("_", " ");
- }
-
- /**
- * Marks any reach locations found in the given stage
- *
- * @param quest The quest the stage belongs to
- * @param stage The stage to search for reach locations
- */
- private void markReachLocations(IQuest quest, IStage stage) {
- markLocations(stage.getLocationsToReach(), stage.getRadiiToReachWithin(),
- "Target location for: " + quest.getName());
- }
-
- /**
- * Marks any kill locations found in the given stage
- *
- * @param quest The quest the stage belongs to
- * @param stage The stage to search for kill locations
- */
- private void markKillLocations(IQuest quest, IStage stage) {
- markLocations(stage.getLocationsToKillWithin(), stage.getRadiiToKillWithin(),
- "Kill location for: " + quest.getName());
- }
-
- /**
- * Marks the given locations on the dynamic map
- *
- * @param locations The locations to mark
- * @param radii The radius of each location's circle
- * @param description The description for what the location means
- */
- private void markLocations(List locations, List radii, String description) {
- for (int i = 0; i < locations.size(); i++) {
- Location location = locations.get(i);
- int radius = radii.get(i);
-
- //Skip if location is invalid
- World world = location.getWorld();
- if (world == null) {
- continue;
- }
-
- CircleMarker circleMarker = questAreaMarkerSet.createCircleMarker(null, description, true,
- world.getName(), location.getX(), location.getY(), location.getZ(), radius, radius, false);
- if (circleMarker == null) {
- DynmapCitizens.getInstance().getLogger().log(Level.WARNING, "Unable to create circle marker at " +
- location + " with radius " + radius);
- } else {
- circleMarker.setFillStyle(0.3, 0x75AFD2);
- circleMarker.setLineStyle(1, 1.0, 0x36c90e);
- }
- }
- }
-
-}
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/util/QuestsHelper.java b/src/main/java/net/knarcraft/dynmapcitizens/util/QuestsHelper.java
new file mode 100644
index 0000000..21b06e3
--- /dev/null
+++ b/src/main/java/net/knarcraft/dynmapcitizens/util/QuestsHelper.java
@@ -0,0 +1,84 @@
+package net.knarcraft.dynmapcitizens.util;
+
+import net.knarcraft.dynmapcitizens.DynmapCitizens;
+import net.knarcraft.dynmapcitizens.handler.VaultHandler;
+import net.knarcraft.dynmapcitizens.handler.trait.quests.QuestNPCType;
+import org.bukkit.inventory.ItemStack;
+
+/**
+ * A helper class for various quests-related tasks
+ */
+public class QuestsHelper {
+
+ /**
+ * Gets an item stack string with the first character upper-cased
+ *
+ * @param itemStack The item stack to get the string for
+ * @return A string describing the item stack
+ */
+ public static String getUpperCasedItemStackString(ItemStack itemStack) {
+ return uppercaseFirst(getItemStackString(itemStack));
+ }
+
+ /**
+ * Gets the proper string representation of an item stack
+ *
+ * @param itemStack The item stack to print
+ * @return The string representation of the item stack
+ */
+ public static String getItemStackString(ItemStack itemStack) {
+ return normalizeName(itemStack.getType().name()) + " x " + itemStack.getAmount();
+ }
+
+ /**
+ * Makes the first character in a string uppercase
+ *
+ * @param string The string to run on
+ * @return The same string, with the first character converted to uppercase
+ */
+ private static String uppercaseFirst(String string) {
+ return string.substring(0, 1).toUpperCase() + string.substring(1);
+ }
+
+ /**
+ * Normalizes an internal name to make it human-readable
+ *
+ * @param name The name to normalize
+ * @return The normalized name
+ */
+ public static String normalizeName(String name) {
+ return name.toLowerCase().replace("_", " ");
+ }
+
+ /**
+ * Gets the currency to print for the given amount of money
+ *
+ * @param money The amount to pay/use
+ * @return The currency name to use
+ */
+ public static String getCurrency(double money) {
+ VaultHandler vaultHandler = DynmapCitizens.getInstance().getVaultHandler();
+ if (vaultHandler.isEnabled()) {
+ return vaultHandler.getCurrency(money != 1);
+ } else {
+ return "money";
+ }
+ }
+
+ /**
+ * Gets the marker title to use for the given quest NPC type
+ *
+ * @param type The type of marker to get the title for
+ * @return The title to use for the marker
+ */
+ public static String getMarkerTitle(QuestNPCType type) {
+ return switch (type) {
+ case GIVER -> "Quest Start NPC: ";
+ case INTERACT -> "Quest Interact NPC: ";
+ case DELIVER -> "Quest Deliver NPC: ";
+ case KILL -> "Quest Kill NPC: ";
+ case CHAIN -> "Quest Chain NPC: ";
+ };
+ }
+
+}
diff --git a/src/main/java/net/knarcraft/dynmapcitizens/util/TimeFormatter.java b/src/main/java/net/knarcraft/dynmapcitizens/util/TimeFormatter.java
new file mode 100644
index 0000000..5ebc8ba
--- /dev/null
+++ b/src/main/java/net/knarcraft/dynmapcitizens/util/TimeFormatter.java
@@ -0,0 +1,98 @@
+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));
+ }
+
+}