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.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.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.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; } 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)).append("
  • "); } markerDescription.append("
"); } //TODO: Get information about the planner (repeatable and/or limited) 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 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) { //TODO: Get the currency from Vault rewardInfo.append("
  • ").append(reward.getMoney()).append(" money").append("
  • "); } if (reward.getExp() > 0) { rewardInfo.append("
  • ").append(reward.getMoney()).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 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 //TODO: See if there is anything to do against overlapping markers } /** * 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); } } } }