mirror of
https://gitlab.com/phoenix-dvpmt/mmocore.git
synced 2025-11-18 06:24:17 +01:00
Skill trees now cache node&path shapes&states to reduce impact on performance
This commit is contained in:
parent
d7dbdfca6e
commit
5225e97042
@ -1,6 +1,7 @@
|
||||
package net.Indyuce.mmocore.api;
|
||||
|
||||
import io.lumine.mythic.lib.MythicLib;
|
||||
import io.lumine.mythic.lib.message.actionbar.ActionBarPriority;
|
||||
import io.lumine.mythic.lib.util.lang3.Validate;
|
||||
import net.Indyuce.mmocore.MMOCore;
|
||||
import net.Indyuce.mmocore.api.player.PlayerData;
|
||||
@ -162,7 +163,7 @@ public class ConfigMessage {
|
||||
|
||||
// Handle special case with player data + action bar
|
||||
if (playerData != null && playerData.isOnline() && actionbar) {
|
||||
playerData.displayActionBar(rawMessage, raw);
|
||||
playerData.getMMOPlayerData().getActionBar().show(ActionBarPriority.NORMAL, rawMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -51,8 +51,8 @@ import net.Indyuce.mmocore.skill.binding.BoundSkillInfo;
|
||||
import net.Indyuce.mmocore.skill.binding.SkillSlot;
|
||||
import net.Indyuce.mmocore.skill.cast.SkillCastingInstance;
|
||||
import net.Indyuce.mmocore.skill.cast.SkillCastingMode;
|
||||
import net.Indyuce.mmocore.skilltree.NodeState;
|
||||
import net.Indyuce.mmocore.skilltree.SkillTreeNode;
|
||||
import net.Indyuce.mmocore.skilltree.*;
|
||||
import net.Indyuce.mmocore.skilltree.display.PathState;
|
||||
import net.Indyuce.mmocore.skilltree.tree.SkillTree;
|
||||
import net.Indyuce.mmocore.util.Language;
|
||||
import net.Indyuce.mmocore.waypoint.Waypoint;
|
||||
@ -112,25 +112,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
private final Map<PlayerActivity, Long> lastActivity = new HashMap<>();
|
||||
private final CombatHandler combat = new CombatHandler(this);
|
||||
|
||||
/**
|
||||
* Cached data
|
||||
* <p>
|
||||
* Current state of each node. This does not get saved in the player database
|
||||
* as it can be inferred from the skill tree node levels map.
|
||||
*/
|
||||
private final Map<SkillTreeNode, NodeState> nodeStates = new HashMap<>();
|
||||
|
||||
private final Map<SkillTreeNode, Integer> nodeLevels = new HashMap<>();
|
||||
private final Map<String, Integer> skillTreePoints = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Cached data
|
||||
* <p>
|
||||
* Amount of points spent in each tree. This does not get saved in the
|
||||
* player database as it can be inferred from the skill tree node levels map.
|
||||
*/
|
||||
private final Map<SkillTree, Integer> skillTreePointsSpent = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Saves the NSK's of the items that have been unlocked in the format "namespace:key".
|
||||
* This is used for:
|
||||
@ -228,13 +209,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
node.getExperienceTable().applyTemporaryTriggers(this, node);
|
||||
}
|
||||
|
||||
private void setupSkillTrees() {
|
||||
|
||||
// Node states setup
|
||||
for (SkillTree skillTree : getProfess().getSkillTrees())
|
||||
skillTree.setupNodeStates(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSessionReady() {
|
||||
|
||||
@ -268,6 +242,58 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
setStellium(lastStellium, PlayerResourceUpdateEvent.UpdateReason.CHOOSE_CLASS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Integer, String> mapBoundSkills() {
|
||||
Map<Integer, String> result = new HashMap<>();
|
||||
for (int slot : boundSkills.keySet())
|
||||
result.put(slot, boundSkills.get(slot).getClassSkill().getSkill().getHandler().getId());
|
||||
return result;
|
||||
}
|
||||
|
||||
public void resetTriggerStats() {
|
||||
getMMOPlayerData().getStatMap().getInstances().forEach(statInstance -> statInstance.removeIf(Trigger.STAT_MODIFIER_KEY::equals));
|
||||
getMMOPlayerData().getSkillModifierMap().removeModifiers(Trigger.STAT_MODIFIER_KEY);
|
||||
}
|
||||
|
||||
//region Skill trees
|
||||
|
||||
/**
|
||||
* Current state of each node. This does not get saved in the player database
|
||||
* as it can be inferred from the skill tree node levels map. This is used
|
||||
* mainly for displaying the skill tree in the GUI.
|
||||
*/
|
||||
private final Map<SkillTreeNode, NodeState> nodeStates = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Current state of each path. This does not get saved in the player database
|
||||
* as it can be inferred from the skill tree node levels map. This is used
|
||||
* mainly for displaying the skill tree in the GUI.
|
||||
*/
|
||||
private final Map<ParentInformation, PathState> edgeStates = new HashMap<>();
|
||||
|
||||
private final Map<SkillTreeNode, Integer> nodeLevels = new HashMap<>();
|
||||
private final Map<String, Integer> skillTreePoints = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Cached data
|
||||
* <p>
|
||||
* Amount of points spent in each tree. This does not get saved in the
|
||||
* player database as it can be inferred from the skill tree node levels map.
|
||||
*/
|
||||
private final Map<SkillTree, Integer> skillTreePointsSpent = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public int getSkillTreeReallocationPoints() {
|
||||
return skillTreeReallocationPoints;
|
||||
}
|
||||
|
||||
private void setupSkillTrees() {
|
||||
|
||||
// Node states setup
|
||||
for (SkillTree skillTree : getProfess().getSkillTrees())
|
||||
skillTree.resolveStates(this);
|
||||
}
|
||||
|
||||
public int getPointsSpent(@NotNull SkillTree skillTree) {
|
||||
return skillTreePointsSpent.getOrDefault(skillTree, 0);
|
||||
}
|
||||
@ -276,11 +302,11 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
private static String asKey(@Nullable SkillTree skillTree) {
|
||||
return skillTree == null ? Arguments.SKILL_TREE_GLOBAL_KEY : skillTree.getId();
|
||||
}
|
||||
|
||||
|
||||
public void setSkillTreePoints(@NotNull SkillTree skillTree, int points) {
|
||||
setSkillTreePoints(asKey(skillTree), points);
|
||||
}
|
||||
|
||||
|
||||
public void setSkillTreePoints(@NotNull String skillTreeId, int points) {
|
||||
if (points <= 0) skillTreePoints.remove(skillTreeId);
|
||||
else skillTreePoints.put(skillTreeId, points);
|
||||
@ -294,15 +320,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
skillTreePoints.merge(id, Math.max(0, val), (points, ignored) -> Math.max(0, points + val));
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of points spent in that skill tree
|
||||
* @deprecated
|
||||
* @see #getPointsSpent(SkillTree)
|
||||
*/
|
||||
public int countSkillTreePoints(@NotNull SkillTree skillTree) {
|
||||
return nodeLevels.keySet().stream().filter(node -> node.getTree().equals(skillTree)).mapToInt(node -> nodeLevels.get(node) * node.getPointConsumption()).sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a copy to make sure that the object
|
||||
* created is independent of the state of playerData.
|
||||
@ -312,14 +329,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
return new HashMap<>(skillTreePoints);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Integer, String> mapBoundSkills() {
|
||||
Map<Integer, String> result = new HashMap<>();
|
||||
for (int slot : boundSkills.keySet())
|
||||
result.put(slot, boundSkills.get(slot).getClassSkill().getSkill().getHandler().getId());
|
||||
return result;
|
||||
}
|
||||
|
||||
public Set<Map.Entry<String, Integer>> getNodeLevelsEntrySet() {
|
||||
HashMap<String, Integer> nodeLevelsString = new HashMap<>();
|
||||
for (SkillTreeNode node : nodeLevels.keySet())
|
||||
@ -327,11 +336,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
return nodeLevelsString.entrySet();
|
||||
}
|
||||
|
||||
public void resetTriggerStats() {
|
||||
getMMOPlayerData().getStatMap().getInstances().forEach(statInstance -> statInstance.removeIf(Trigger.STAT_MODIFIER_KEY::equals));
|
||||
getMMOPlayerData().getSkillModifierMap().removeModifiers(Trigger.STAT_MODIFIER_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Integer> getNodeLevels() {
|
||||
final Map<String, Integer> mapped = new HashMap<>();
|
||||
@ -349,6 +353,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
// Skill trees progress
|
||||
nodeLevels.clear();
|
||||
nodeStates.clear(); // Cache data
|
||||
edgeStates.clear();
|
||||
skillTreePointsSpent.clear();
|
||||
tableItemClaims.keySet().removeIf(s -> s.startsWith(SkillTreeNode.KEY_PREFIX)); // Clear node claim count
|
||||
|
||||
@ -360,13 +365,14 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
setupSkillTrees();
|
||||
}
|
||||
|
||||
public void clearNodeStates(@NotNull SkillTree skillTree) {
|
||||
for (SkillTreeNode node : skillTree.getNodes()) nodeStates.remove(node);
|
||||
public void clearStates(@NotNull SkillTree skillTree) {
|
||||
for (var node : skillTree.getNodes()) nodeStates.remove(node);
|
||||
// TODO clear path states.
|
||||
}
|
||||
|
||||
// TODO move to UI...?
|
||||
@NotNull
|
||||
public NodeIncrementResult canIncrementNodeLevel(@NotNull SkillTreeNode node) {
|
||||
// TODO move to UI...?
|
||||
final var nodeState = nodeStates.get(node);
|
||||
|
||||
// Node is maxed out
|
||||
@ -395,10 +401,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
final int newLevel = addNodeLevels(node, 1);
|
||||
node.updateAdvancement(this, newLevel); // Claim the node exp table
|
||||
|
||||
// Update node state
|
||||
// TODO check/remove: USELESS LINE? since node states are updated afterwards
|
||||
// nodeStates.compute(node, (key, status) -> status == NodeState.UNLOCKABLE ? NodeState.UNLOCKED : status);
|
||||
|
||||
// Consume skill tree points
|
||||
final AtomicInteger cost = new AtomicInteger(node.getPointConsumption());
|
||||
skillTreePoints.computeIfPresent(node.getTree().getId(), (key, points) -> {
|
||||
@ -408,9 +410,8 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
});
|
||||
if (cost.get() > 0) withdrawSkillTreePoints("global", cost.get());
|
||||
|
||||
// Reload node states from full skill tree
|
||||
clearNodeStates(node.getTree());
|
||||
node.getTree().setupNodeStates(this);
|
||||
// Reload node/path states from full skill tree
|
||||
node.getTree().resolveStates(this);
|
||||
}
|
||||
|
||||
public int getSkillTreePoints(@Nullable SkillTree skillTree) {
|
||||
@ -426,14 +427,22 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
skillTreePoints.computeIfPresent(treeId, (ignored, points) -> cost >= points ? null : points - cost);
|
||||
}
|
||||
|
||||
public void setNodeState(SkillTreeNode node, NodeState nodeState) {
|
||||
public void setNodeState(@NotNull SkillTreeNode node, @NotNull NodeState nodeState) {
|
||||
nodeStates.put(node, nodeState);
|
||||
}
|
||||
|
||||
public NodeState getNodeState(SkillTreeNode node) {
|
||||
public void setPathState(@NotNull ParentInformation edge, @NotNull PathState pathState) {
|
||||
edgeStates.put(edge, pathState);
|
||||
}
|
||||
|
||||
public NodeState getNodeState(@NotNull SkillTreeNode node) {
|
||||
return nodeStates.get(node);
|
||||
}
|
||||
|
||||
public PathState getPathState(@NotNull ParentInformation path) {
|
||||
return edgeStates.get(path);
|
||||
}
|
||||
|
||||
public boolean hasNodeState(@NotNull SkillTreeNode node) {
|
||||
return nodeStates.containsKey(node);
|
||||
}
|
||||
@ -461,9 +470,8 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
for (SkillTreeNode node : skillTree.getNodes()) {
|
||||
node.resetAdvancement(this, true);
|
||||
setNodeLevel(node, 0);
|
||||
nodeStates.remove(node);
|
||||
}
|
||||
skillTree.setupNodeStates(this);
|
||||
skillTree.resolveStates(this);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -475,6 +483,8 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
return !nodeStates.isEmpty();
|
||||
}
|
||||
|
||||
//endregion Skill trees
|
||||
|
||||
@Override
|
||||
public Map<String, Integer> getNodeTimesClaimed() {
|
||||
Map<String, Integer> result = new HashMap<>();
|
||||
@ -650,11 +660,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
return attributeReallocationPoints;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSkillTreeReallocationPoints() {
|
||||
return skillTreeReallocationPoints;
|
||||
}
|
||||
|
||||
public int getClaims(@NotNull ExperienceObject object, @NotNull ExperienceItem item) {
|
||||
final ExperienceTable table = object.getExperienceTable();
|
||||
return getClaims(object.getKey() + "." + table.getId() + "." + item.getId());
|
||||
@ -698,11 +703,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
return guild != null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setLevel(int level) {
|
||||
setLevel(level, PlayerLevelChangeEvent.Reason.UNKNOWN);
|
||||
}
|
||||
|
||||
public void setLevel(int level, @NotNull PlayerLevelChangeEvent.Reason reason) {
|
||||
|
||||
final var oldLevel = this.level;
|
||||
@ -718,11 +718,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void takeLevels(int value) {
|
||||
setLevel(level - value, PlayerLevelChangeEvent.Reason.UNKNOWN);
|
||||
}
|
||||
|
||||
public void giveLevels(int value, @NotNull EXPSource source) {
|
||||
long equivalentExp = 0;
|
||||
while (value-- > 0) equivalentExp += getProfess().getExpCurve().getExperience(getLevel() + value + 1);
|
||||
@ -1398,16 +1393,43 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
|
||||
//region Deprecated
|
||||
|
||||
@Deprecated
|
||||
public void takeLevels(int value) {
|
||||
setLevel(level - value, PlayerLevelChangeEvent.Reason.UNKNOWN);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setLevel(int level) {
|
||||
setLevel(level, PlayerLevelChangeEvent.Reason.UNKNOWN);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void clearNodeStates(@NotNull SkillTree skillTree) {
|
||||
clearStates(skillTree);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setMana(double, PlayerResourceUpdateEvent.UpdateReason)
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setMana(double amount) {
|
||||
setMana(amount, PlayerResourceUpdateEvent.UpdateReason.UNKNOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setStamina(double, PlayerResourceUpdateEvent.UpdateReason)
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setStamina(double amount) {
|
||||
setStamina(amount, PlayerResourceUpdateEvent.UpdateReason.UNKNOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #setStellium(double, PlayerResourceUpdateEvent.UpdateReason)
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setStellium(double amount) {
|
||||
setStellium(amount, PlayerResourceUpdateEvent.UpdateReason.UNKNOWN);
|
||||
@ -1539,11 +1561,19 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #hasUnlockedLevel(ClassSkill)
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean hasSkillUnlocked(RegisteredSkill skill) {
|
||||
return getProfess().hasSkill(skill.getHandler().getId()) && hasSkillUnlocked(getProfess().getSkill(skill.getHandler().getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #hasUnlockedLevel(ClassSkill)
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean hasSkillUnlocked(ClassSkill skill) {
|
||||
return hasUnlockedLevel(skill);
|
||||
@ -1552,6 +1582,9 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
/**
|
||||
* Everytime a player does a combat action, like taking
|
||||
* or dealing damage to an entity, this method is called.
|
||||
*
|
||||
* @see #getCombat()
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void updateCombat() {
|
||||
@ -1560,7 +1593,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
|
||||
@Deprecated
|
||||
public void displayActionBar(@NotNull String message) {
|
||||
displayActionBar(message, false);
|
||||
getMMOPlayerData().getActionBar().show(ActionBarPriority.NORMAL, message);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@ -1580,15 +1613,34 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #getAttributes()
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setAttribute(PlayerAttribute attribute, int value) {
|
||||
setAttribute(attribute.getId(), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #getAttributes()
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public void setAttribute(String id, int value) {
|
||||
attributes.getInstance(id).setBase(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of points spent in that skill tree
|
||||
*
|
||||
* @see #getPointsSpent(SkillTree)
|
||||
* @deprecated
|
||||
*/
|
||||
@Deprecated
|
||||
public int countSkillTreePoints(@NotNull SkillTree skillTree) {
|
||||
return nodeLevels.keySet().stream().filter(node -> node.getTree().equals(skillTree)).mapToInt(node -> nodeLevels.get(node) * node.getPointConsumption()).sum();
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
||||
|
||||
@ -332,7 +332,7 @@ public class SavedClassInformation implements ClassDataContainer {
|
||||
for (SkillTreeNode node : skillTree.getNodes())
|
||||
player.setNodeLevel(node, nodeLevels.getOrDefault(node.getFullId(), 0));
|
||||
|
||||
skillTree.setupNodeStates(player);
|
||||
skillTree.resolveStates(player);
|
||||
}
|
||||
|
||||
// Add the values to the times claimed table and claims the corresponding stat triggers.
|
||||
|
||||
@ -13,7 +13,7 @@ import io.lumine.mythic.lib.gui.util.IconOptions;
|
||||
import net.Indyuce.mmocore.MMOCore;
|
||||
import net.Indyuce.mmocore.api.player.PlayerData;
|
||||
import net.Indyuce.mmocore.player.Message;
|
||||
import net.Indyuce.mmocore.skilltree.IntegerCoordinates;
|
||||
import net.Indyuce.mmocore.skilltree.IntCoords;
|
||||
import net.Indyuce.mmocore.skilltree.NodeState;
|
||||
import net.Indyuce.mmocore.skilltree.ParentType;
|
||||
import net.Indyuce.mmocore.skilltree.SkillTreeNode;
|
||||
@ -22,7 +22,6 @@ import net.Indyuce.mmocore.skilltree.display.NodeDisplayInfo;
|
||||
import net.Indyuce.mmocore.skilltree.display.PathDisplayInfo;
|
||||
import net.Indyuce.mmocore.skilltree.tree.SkillTree;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.event.inventory.ClickType;
|
||||
@ -127,11 +126,10 @@ public class SkillTreeViewer extends EditableInventory {
|
||||
}
|
||||
|
||||
int reallocated = inv.playerData.getPointsSpent(inv.skillTree);
|
||||
// Remove all the nodeStates progress
|
||||
inv.playerData.giveSkillTreePoints(inv.skillTree.getId(), reallocated);
|
||||
inv.playerData.giveSkillTreeReallocationPoints(-1);
|
||||
inv.playerData.resetSkillTree(inv.skillTree);
|
||||
inv.skillTree.setupNodeStates(inv.playerData);
|
||||
inv.skillTree.resolveStates(inv.playerData);
|
||||
Message.SKILL_TREE_REALLOCATE.send(inv.playerData, "points", inv.playerData.getSkillTreePoints(inv.skillTree.getId()), "skill-tree", inv.skillTree.getName());
|
||||
inv.open();
|
||||
}
|
||||
@ -304,7 +302,7 @@ public class SkillTreeViewer extends EditableInventory {
|
||||
|
||||
@Override
|
||||
public void preprocessLore(@NotNull SkillTreeInventory inv, int n, @NotNull List<String> lore) {
|
||||
IntegerCoordinates coordinates = inv.getCoordinates(n);
|
||||
IntCoords coordinates = inv.getCoordinates(n);
|
||||
if (!inv.getSkillTree().isNode(coordinates)) return;
|
||||
|
||||
SkillTreeNode node = inv.getSkillTree().getNode(coordinates);
|
||||
@ -346,10 +344,11 @@ public class SkillTreeViewer extends EditableInventory {
|
||||
*/
|
||||
@Override
|
||||
public ItemStack getDisplayedItem(SkillTreeInventory inv, int n) {
|
||||
IntegerCoordinates coordinates = inv.getCoordinates(n);
|
||||
if (!inv.getSkillTree().isPathOrNode(coordinates)) return new ItemStack(Material.AIR);
|
||||
IntCoords coordinates = inv.getCoordinates(n);
|
||||
|
||||
IconOptions icon = inv.computeIcon(coordinates);
|
||||
if (icon == null) return null; // Neither a path nor a node
|
||||
|
||||
IconOptions icon = inv.getIcon(coordinates);
|
||||
ItemStack item = super.getDisplayedItem(inv, new ItemOptions(n, icon));
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
|
||||
@ -377,6 +376,7 @@ public class SkillTreeViewer extends EditableInventory {
|
||||
* Soft&Strong children lore for the node
|
||||
*/
|
||||
public List<String> getParentsLore(SkillTreeInventory inv, SkillTreeNode node, Collection<SkillTreeNode> parents) {
|
||||
// TODO why is this hardcoded >:(
|
||||
List<String> lore = new ArrayList<>();
|
||||
for (SkillTreeNode parent : parents) {
|
||||
int level = inv.playerData.getNodeLevel(parent);
|
||||
@ -387,7 +387,7 @@ public class SkillTreeViewer extends EditableInventory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Placeholders getPlaceholders(SkillTreeInventory inv, int n) {
|
||||
public @NotNull Placeholders getPlaceholders(SkillTreeInventory inv, int n) {
|
||||
Placeholders holders = new Placeholders();
|
||||
holders.register("skill-tree", inv.getSkillTree().getName());
|
||||
boolean isNode = inv.getSkillTree().isNode(inv.getCoordinates(n));
|
||||
@ -400,10 +400,10 @@ public class SkillTreeViewer extends EditableInventory {
|
||||
holders.register("name", node.getName());
|
||||
holders.register("max-children", node.getMaxChildren());
|
||||
holders.register("point-consumed", node.getPointConsumption());
|
||||
holders.register("display-type", node.getNodeType());
|
||||
} else {
|
||||
//holders.register("display-type", node.getNodeType());
|
||||
} /*else {
|
||||
holders.register("display-type", inv.skillTree.getPath(inv.getCoordinates(n)).getPathType());
|
||||
}
|
||||
}*/
|
||||
int maxPointSpent = inv.getSkillTree().getMaxPointSpent();
|
||||
holders.register("max-point-spent", maxPointSpent == Integer.MAX_VALUE ? "∞" : maxPointSpent);
|
||||
holders.register("point-spent", inv.playerData.getPointsSpent(inv.getSkillTree()));
|
||||
@ -420,10 +420,10 @@ public class SkillTreeViewer extends EditableInventory {
|
||||
final PersistentDataContainer container = event.getCurrentItem().getItemMeta().getPersistentDataContainer();
|
||||
final int x = container.get(new NamespacedKey(MMOCore.plugin, "coordinates.x"), PersistentDataType.INTEGER);
|
||||
final int y = container.get(new NamespacedKey(MMOCore.plugin, "coordinates.y"), PersistentDataType.INTEGER);
|
||||
if (!inv.skillTree.isNode(new IntegerCoordinates(x, y))) return;
|
||||
if (!inv.skillTree.isNode(new IntCoords(x, y))) return;
|
||||
|
||||
// Higher number of points spent in SKILL TREE (not node)
|
||||
final SkillTreeNode node = inv.skillTree.getNode(new IntegerCoordinates(x, y));
|
||||
final SkillTreeNode node = inv.skillTree.getNode(new IntCoords(x, y));
|
||||
if (inv.playerData.getPointsSpent(inv.skillTree) >= inv.skillTree.getMaxPointSpent()) {
|
||||
Message.SKILL_TREE_MAX_POINTS_SPENT.send(inv.playerData);
|
||||
return;
|
||||
@ -518,24 +518,39 @@ public class SkillTreeViewer extends EditableInventory {
|
||||
return playerData;
|
||||
}
|
||||
|
||||
public IconOptions getIcon(@NotNull IntegerCoordinates coordinates) {
|
||||
if (skillTree.isNode(coordinates)) {
|
||||
var node = skillTree.getNode(coordinates);
|
||||
var nodeShape = node.getNodeType();
|
||||
var nodeState = playerData.getNodeState(node);
|
||||
@Deprecated
|
||||
public IconOptions getIcon(IntCoords coordinates) {
|
||||
return computeIcon(coordinates);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IconOptions computeIcon(@NotNull IntCoords coordinates) {
|
||||
|
||||
// Is this a node?
|
||||
final var node = skillTree.getNodeOrNull(coordinates);
|
||||
if (node != null) {
|
||||
final var nodeShape = skillTree.getNodeShape(node);
|
||||
final var nodeState = playerData.getNodeState(node);
|
||||
var displayInfo = new NodeDisplayInfo(nodeShape, nodeState);
|
||||
|
||||
// Node > skill tree > skill tree UI
|
||||
var icon = DisplayMap.getIcon(displayInfo, node.getIcons(), skillTree.getIcons(), icons);
|
||||
if (icon == null && nodeState == NodeState.MAXED_OUT) {
|
||||
// Fallback to UNLOCKED if no MAXED_OUT icon found
|
||||
displayInfo = new NodeDisplayInfo(nodeShape, NodeState.UNLOCKED);
|
||||
icon = DisplayMap.getIcon(displayInfo, node.getIcons(), skillTree.getIcons(), icons);
|
||||
}
|
||||
if (icon == null) icon = DisplayMap.DEFAULT_ICON;
|
||||
//Validate.notNull(icon, "Node " + node.getFullId() + " has no icon for shape " + nodeShape + " and state " + nodeState);
|
||||
|
||||
return icon;
|
||||
} else {
|
||||
var path = skillTree.getPath(coordinates);
|
||||
var pathShape = path.getPathType();
|
||||
var pathStatus = path.getStatus(playerData);
|
||||
var displayInfo = new PathDisplayInfo(pathShape, pathStatus);
|
||||
}
|
||||
|
||||
final var edge = skillTree.getPath(coordinates);
|
||||
if (edge != null) {
|
||||
final var pathState = playerData.getPathState(edge);
|
||||
final var pathShape = edge.getShape(coordinates);
|
||||
final var displayInfo = new PathDisplayInfo(pathShape, pathState);
|
||||
|
||||
// Skill tree > Skill tree UI
|
||||
var icon = DisplayMap.getIcon(displayInfo, skillTree.getIcons(), icons);
|
||||
@ -544,6 +559,9 @@ public class SkillTreeViewer extends EditableInventory {
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
// Neither a node or a path
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -552,11 +570,11 @@ public class SkillTreeViewer extends EditableInventory {
|
||||
return guiName.replace("{skill-tree-name}", skillTree.getName()).replace("{skill-tree-id}", skillTree.getId());
|
||||
}
|
||||
|
||||
public IntegerCoordinates getCoordinates(int n) {
|
||||
public IntCoords getCoordinates(int n) {
|
||||
int slot = slots.get(n);
|
||||
int deltaX = (slot - minSlot) % 9;
|
||||
int deltaY = (slot - minSlot) / 9;
|
||||
IntegerCoordinates coordinates = new IntegerCoordinates(getX() + deltaX, getY() + deltaY);
|
||||
IntCoords coordinates = new IntCoords(getX() + deltaX, getY() + deltaY);
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
|
||||
@ -55,14 +55,7 @@ public class SkillTreeManager extends MMOCoreRegister<SkillTree> {
|
||||
@NotNull
|
||||
public SkillTree loadSkillTree(@NotNull ConfigurationSection config) {
|
||||
Validate.notNull(config, "Config cannot be null");
|
||||
|
||||
final SkillTreeType type;
|
||||
try {
|
||||
type = SkillTreeType.valueOf(UtilityMethods.enumName(config.getString("type", "custom")));
|
||||
} catch (RuntimeException exception) {
|
||||
throw new IllegalArgumentException("Not a valid skill tree type");
|
||||
}
|
||||
|
||||
final var type = UtilityMethods.prettyValueOf(SkillTreeType::valueOf, config.getString("type", "custom"), "No skill tree type '%s'");
|
||||
return type.construct(config);
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ package net.Indyuce.mmocore.skill.cast.handler;
|
||||
import io.lumine.mythic.lib.api.event.skill.PlayerCastSkillEvent;
|
||||
import io.lumine.mythic.lib.api.player.EquipmentSlot;
|
||||
import io.lumine.mythic.lib.gui.editable.placeholder.Placeholders;
|
||||
import io.lumine.mythic.lib.message.actionbar.ActionBarPriority;
|
||||
import io.lumine.mythic.lib.player.PlayerMetadata;
|
||||
import io.lumine.mythic.lib.skill.result.SkillResult;
|
||||
import io.lumine.mythic.lib.skill.trigger.TriggerMetadata;
|
||||
@ -195,7 +196,11 @@ public class KeyCombos extends SkillCastingHandler {
|
||||
public void onTick() {
|
||||
if (actionBarOptions != null) if (actionBarOptions.isSubtitle)
|
||||
getCaster().getPlayer().sendTitle(" ", actionBarOptions.format(this), 0, 20, 0);
|
||||
else getCaster().displayActionBar(actionBarOptions.format(this));
|
||||
else {
|
||||
var handler = caster.getMMOPlayerData().getActionBar();
|
||||
if (!handler.canShow(ActionBarPriority.NORMAL)) return;
|
||||
handler.show(ActionBarPriority.NORMAL, actionBarOptions.format(this));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -3,6 +3,7 @@ package net.Indyuce.mmocore.skill.cast.handler;
|
||||
import io.lumine.mythic.lib.MythicLib;
|
||||
import io.lumine.mythic.lib.UtilityMethods;
|
||||
import io.lumine.mythic.lib.api.player.EquipmentSlot;
|
||||
import io.lumine.mythic.lib.message.actionbar.ActionBarPriority;
|
||||
import io.lumine.mythic.lib.skill.trigger.TriggerMetadata;
|
||||
import io.lumine.mythic.lib.util.SoundObject;
|
||||
import net.Indyuce.mmocore.MMOCore;
|
||||
@ -127,7 +128,10 @@ public class SkillScroller extends SkillCastingHandler {
|
||||
public void onTick() {
|
||||
final String skillName = getSelected().getSkill().getName();
|
||||
final String actionBarFormat = MythicLib.plugin.getPlaceholderParser().parse(getCaster().getPlayer(), SkillScroller.this.actionBarFormat.replace("{selected}", skillName));
|
||||
getCaster().displayActionBar(actionBarFormat);
|
||||
|
||||
var handler = caster.getMMOPlayerData().getActionBar();
|
||||
if (!handler.canShow(ActionBarPriority.NORMAL)) return;
|
||||
handler.show(ActionBarPriority.NORMAL, actionBarFormat);
|
||||
}
|
||||
|
||||
public ClassSkill getSelected() {
|
||||
|
||||
@ -8,22 +8,27 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public class IntegerCoordinates {
|
||||
public class IntCoords {
|
||||
private final int x, y;
|
||||
|
||||
public IntegerCoordinates(int x, int y) {
|
||||
public IntCoords(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public IntegerCoordinates(String str) {
|
||||
public IntCoords(String str) {
|
||||
String[] split = str.split("\\.");
|
||||
Validate.isTrue(split.length == 2, "Invalid format");
|
||||
x = Integer.parseInt(split[0]);
|
||||
y = Integer.parseInt(split[1]);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public IntCoords offset(int x, int y) {
|
||||
return new IntCoords(this.x + x, this.y + y);
|
||||
}
|
||||
|
||||
public int getX() {
|
||||
return x;
|
||||
}
|
||||
@ -33,15 +38,15 @@ public class IntegerCoordinates {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public IntegerCoordinates add(@NotNull IntegerCoordinates other) {
|
||||
return new IntegerCoordinates(x + other.x, y + other.y);
|
||||
public IntCoords add(@NotNull IntCoords other) {
|
||||
return new IntCoords(x + other.x, y + other.y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
IntegerCoordinates that = (IntegerCoordinates) o;
|
||||
IntCoords that = (IntCoords) o;
|
||||
return x == that.x && y == that.y;
|
||||
}
|
||||
|
||||
@ -56,18 +61,18 @@ public class IntegerCoordinates {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static IntegerCoordinates from(@Nullable Object object) {
|
||||
public static IntCoords from(@Nullable Object object) {
|
||||
Validate.notNull(object, "Could not read coordinates");
|
||||
|
||||
if (object instanceof ConfigurationSection) {
|
||||
final ConfigurationSection config = (ConfigurationSection) object;
|
||||
return new IntegerCoordinates(config.getInt("x"), config.getInt("y"));
|
||||
return new IntCoords(config.getInt("x"), config.getInt("y"));
|
||||
}
|
||||
|
||||
if (object instanceof String) {
|
||||
final String[] split = ((String) object).split("[:.,]");
|
||||
Validate.isTrue(split.length > 1, "Must provide two coordinates, X and Y, got " + Arrays.asList(split));
|
||||
return new IntegerCoordinates(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
|
||||
return new IntCoords(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
|
||||
}
|
||||
|
||||
throw new RuntimeException("Needs either a string or configuration section");
|
||||
@ -1,23 +1,138 @@
|
||||
package net.Indyuce.mmocore.skilltree;
|
||||
|
||||
import net.Indyuce.mmocore.skilltree.display.PathShape;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Holds information about a parent/child node in a skill tree.
|
||||
* `relative` can hold either a parent or child node. There is
|
||||
* always one `parent` counterpart for every `child` instance of this class.
|
||||
* <p>
|
||||
* If we represent the skill tree by a graph where edges are skill tree
|
||||
* "nodes", then this class represents an edge in that graph.
|
||||
*
|
||||
* @author jules
|
||||
*/
|
||||
public class ParentInformation {
|
||||
private final SkillTreeNode node;
|
||||
private final SkillTreeNode child, parent;
|
||||
private final ParentType type;
|
||||
private final int level;
|
||||
private final int minLevel;
|
||||
private final boolean reciprocal;
|
||||
|
||||
public ParentInformation(SkillTreeNode node, ParentType type, int level) {
|
||||
this.node = node;
|
||||
private final Map<IntCoords, PathShape> elements = new HashMap<>();
|
||||
|
||||
public ParentInformation(@NotNull SkillTreeNode child, @NotNull SkillTreeNode parent) {
|
||||
this(child, parent, ParentType.SOFT, false, 1);
|
||||
}
|
||||
|
||||
public ParentInformation(@NotNull SkillTreeNode child,
|
||||
@NotNull SkillTreeNode parent,
|
||||
@NotNull ParentType type,
|
||||
boolean reciprocal,
|
||||
int minLevel) {
|
||||
this.child = child;
|
||||
this.parent = parent;
|
||||
this.type = type;
|
||||
this.level = level;
|
||||
this.reciprocal = reciprocal;
|
||||
this.minLevel = Math.max(1, minLevel);
|
||||
}
|
||||
|
||||
public ParentInformation(@NotNull SkillTreeNode child,
|
||||
@NotNull SkillTreeNode parent,
|
||||
@NotNull ParentType type,
|
||||
@NotNull ConfigurationSection config) {
|
||||
this.child = child;
|
||||
this.parent = parent;
|
||||
this.type = type;
|
||||
this.reciprocal = false;
|
||||
this.minLevel = Math.max(1, config.getInt("level"));
|
||||
|
||||
// Read paths
|
||||
if (config.contains("paths")) {
|
||||
final var pathListRaw = config.getStringList("paths");
|
||||
pathListRaw.forEach(string -> this.elements.put(IntCoords.from(string), null));
|
||||
}
|
||||
|
||||
// All paths are loaded => cache their shapes
|
||||
for (var element : this.elements.keySet()) {
|
||||
final var previousValue = this.elements.put(element, computePathShape(element));
|
||||
Validate.isTrue(previousValue == null, "Path shape already computed?");
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public SkillTreeNode getNode() {
|
||||
return node;
|
||||
public PathShape getShape(@NotNull IntCoords coordinates) {
|
||||
final var shape = this.elements.get(coordinates);
|
||||
Validate.notNull(shape, "No path element at " + coordinates);
|
||||
return shape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the method for computing the shape of a path element ie
|
||||
* whether it goes up, right, up-right, etc. based on the presence
|
||||
* of other path elements around it.
|
||||
*
|
||||
* @param coordinates Coordinates of the path element to compute the shape for
|
||||
* @return Shape of the path element
|
||||
*/
|
||||
@NotNull
|
||||
private PathShape computePathShape(@NotNull IntCoords coordinates) {
|
||||
|
||||
final var upCoords = new IntCoords(coordinates.getX(), coordinates.getY() - 1);
|
||||
final var downCoords = new IntCoords(coordinates.getX(), coordinates.getY() + 1);
|
||||
final var rightCoords = new IntCoords(coordinates.getX() + 1, coordinates.getY());
|
||||
final var leftCoords = new IntCoords(coordinates.getX() - 1, coordinates.getY());
|
||||
|
||||
final var hasUp = this.elements.containsKey(upCoords) || upCoords.equals(parent.getCoordinates()) || upCoords.equals(child.getCoordinates());
|
||||
final var hasDown = this.elements.containsKey(downCoords) || downCoords.equals(parent.getCoordinates()) || downCoords.equals(child.getCoordinates());
|
||||
final var hasRight = this.elements.containsKey(rightCoords) || rightCoords.equals(parent.getCoordinates()) || rightCoords.equals(child.getCoordinates());
|
||||
final var hasLeft = this.elements.containsKey(leftCoords) || leftCoords.equals(parent.getCoordinates()) || leftCoords.equals(child.getCoordinates());
|
||||
|
||||
if ((hasUp || hasDown) && !hasLeft && !hasRight) return PathShape.UP;
|
||||
else if ((hasRight || hasLeft) && !hasUp && !hasDown) return PathShape.RIGHT;
|
||||
else if (hasUp && hasRight) return PathShape.UP_RIGHT;
|
||||
else if (hasUp && hasLeft) return PathShape.UP_LEFT;
|
||||
else if (hasDown && hasRight) return PathShape.DOWN_RIGHT;
|
||||
else if (hasDown && hasLeft) return PathShape.DOWN_LEFT;
|
||||
|
||||
return PathShape.DEFAULT;
|
||||
}
|
||||
|
||||
public boolean isSymmetrical() {
|
||||
return reciprocal;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Set<IntCoords> getElements() {
|
||||
return elements.keySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ParentInformation{" +
|
||||
"child=" + child +
|
||||
", parent=" + parent +
|
||||
", type=" + type +
|
||||
", minLevel=" + minLevel +
|
||||
", elements=" + elements +
|
||||
'}';
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public SkillTreeNode getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public SkillTreeNode getChild() {
|
||||
return child;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -25,8 +140,12 @@ public class ParentInformation {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Minimum level of parent node required
|
||||
* for the child node to be reachable.
|
||||
*/
|
||||
public int getLevel() {
|
||||
return level;
|
||||
return minLevel;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -34,11 +153,33 @@ public class ParentInformation {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ParentInformation that = (ParentInformation) o;
|
||||
return Objects.equals(node, that.node) && type == that.type;
|
||||
// Big Hypothesis = there are NO two edges with the same child, parent and type.
|
||||
return Objects.equals(child, that.child) && Objects.equals(parent, that.parent) && type == that.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(node, type);
|
||||
return Objects.hash(child, parent, type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static ParentInformation fromConfig(@NotNull SkillTreeNode child,
|
||||
@NotNull SkillTreeNode parent,
|
||||
@NotNull ParentType parentType,
|
||||
@NotNull Object configObject) {
|
||||
Validate.notNull(configObject, "Cannot load parent info from null object");
|
||||
|
||||
// From simple int, no path.
|
||||
if (configObject instanceof Integer) {
|
||||
// TODO try to infer paths from 'paths' config for backwards compatibility
|
||||
return new ParentInformation(child, parent, parentType, false, (Integer) configObject);
|
||||
}
|
||||
|
||||
// From config section
|
||||
if (configObject instanceof ConfigurationSection) {
|
||||
return new ParentInformation(child, parent, parentType, (ConfigurationSection) configObject);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Cannot load parent from " + configObject.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
package net.Indyuce.mmocore.skilltree;
|
||||
|
||||
import io.lumine.mythic.lib.MythicLib;
|
||||
import io.lumine.mythic.lib.UtilityMethods;
|
||||
import io.lumine.mythic.lib.gui.editable.placeholder.Placeholders;
|
||||
import io.lumine.mythic.lib.gui.util.IconOptions;
|
||||
import io.lumine.mythic.lib.util.PostLoadAction;
|
||||
import io.lumine.mythic.lib.util.lang3.Validate;
|
||||
import net.Indyuce.mmocore.MMOCore;
|
||||
import net.Indyuce.mmocore.api.player.PlayerData;
|
||||
@ -23,13 +25,14 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
// We must use generics to get the type of the corresponding tree
|
||||
|
||||
public class SkillTreeNode implements ExperienceObject {
|
||||
private final SkillTree tree;
|
||||
private final String name, id;
|
||||
private final String permissionRequired;
|
||||
private final int pointConsumption;
|
||||
private final DisplayMap icons;
|
||||
private final IntegerCoordinates coordinates;
|
||||
private final IntCoords coordinates;
|
||||
private final int maxLevel, maxChildren;
|
||||
private final ExperienceTable experienceTable;
|
||||
private final List<ParentInformation> children = new ArrayList<>();
|
||||
@ -38,11 +41,42 @@ public class SkillTreeNode implements ExperienceObject {
|
||||
|
||||
private boolean root;
|
||||
|
||||
public SkillTreeNode(SkillTree tree, ConfigurationSection config) {
|
||||
private final PostLoadAction postLoadAction = new PostLoadAction(config -> {
|
||||
|
||||
// Load children
|
||||
// Requires other nodes to be loaded first
|
||||
loadRelatives(true, config);
|
||||
|
||||
// Load parents. Both work, one way or the other
|
||||
// Requires other nodes to be loaded first
|
||||
loadRelatives(false, config);
|
||||
});
|
||||
|
||||
private void loadRelatives(boolean nodeIsParent, @NotNull ConfigurationSection config) {
|
||||
final var configPath = nodeIsParent ? "children" : "parents";
|
||||
|
||||
if (config.isConfigurationSection(configPath))
|
||||
for (var parentTypeRaw : config.getConfigurationSection(configPath).getKeys(false)) {
|
||||
final var section = config.getConfigurationSection(configPath + "." + parentTypeRaw);
|
||||
Validate.notNull(section, "Could not read " + configPath + " of type '" + parentTypeRaw + "'");
|
||||
final ParentType parentType = UtilityMethods.prettyValueOf(ParentType::valueOf, parentTypeRaw, "No parent type called '%s'");
|
||||
|
||||
for (var relativeId : section.getKeys(false)) {
|
||||
final var relative = SkillTreeNode.this.tree.getNode(relativeId);
|
||||
final var child = nodeIsParent ? relative : this;
|
||||
final var parent = nodeIsParent ? this : relative;
|
||||
child.addParent(ParentInformation.fromConfig(child, parent, parentType, section.get(relativeId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SkillTreeNode(@NotNull SkillTree tree, @NotNull ConfigurationSection config) {
|
||||
Validate.notNull(config, "Config cannot be null");
|
||||
this.id = config.getName();
|
||||
this.tree = tree;
|
||||
|
||||
postLoadAction.cacheConfig(config);
|
||||
|
||||
// Load icons for node states
|
||||
this.icons = DisplayMap.from(config.getConfigurationSection("display"));
|
||||
|
||||
@ -70,7 +104,7 @@ public class SkillTreeNode implements ExperienceObject {
|
||||
Validate.isTrue(maxLevel > 0, "Max level must be positive");
|
||||
maxChildren = config.getInt("max-children", 0);
|
||||
Validate.isTrue(maxChildren >= 0, "Max children must positive or zero");
|
||||
coordinates = IntegerCoordinates.from(config.get("coordinates"));
|
||||
coordinates = IntCoords.from(config.get("coordinates"));
|
||||
}
|
||||
|
||||
public SkillTree getTree() {
|
||||
@ -81,12 +115,23 @@ public class SkillTreeNode implements ExperienceObject {
|
||||
return root;
|
||||
}
|
||||
|
||||
public void addParent(@NotNull SkillTreeNode parent, @NotNull ParentType parentType, int requiredLevel) {
|
||||
parents.add(new ParentInformation(parent, parentType, requiredLevel));
|
||||
@NotNull
|
||||
public PostLoadAction getPostLoadAction() {
|
||||
return postLoadAction;
|
||||
}
|
||||
|
||||
public void addChild(@NotNull SkillTreeNode child, @NotNull ParentType parentType, int requiredLevel) {
|
||||
children.add(new ParentInformation(child, parentType, requiredLevel));
|
||||
/**
|
||||
* Registers this relation both as a parent and child relation in the right
|
||||
* registers of the parent and child nodes.
|
||||
* <p>
|
||||
* Note that the {@link #children} and {@link #parents} maps are only
|
||||
* modified through this method.
|
||||
*/
|
||||
public void addParent(@NotNull ParentInformation parentInfo) {
|
||||
Validate.isTrue(parentInfo.getChild().equals(this), "#addParent(..) must be called on child node");
|
||||
|
||||
parents.add(parentInfo);
|
||||
parentInfo.getParent().children.add(parentInfo);
|
||||
}
|
||||
|
||||
public void setRoot() {
|
||||
@ -97,24 +142,9 @@ public class SkillTreeNode implements ExperienceObject {
|
||||
return pointConsumption;
|
||||
}
|
||||
|
||||
public int getParentNeededLevel(SkillTreeNode parent) {
|
||||
for (ParentInformation entry : parents)
|
||||
if (entry.getNode().equals(parent))
|
||||
return entry.getLevel();
|
||||
throw new RuntimeException("Could not find parent " + parent.getId() + " for node " + id);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public int getParentNeededLevel(SkillTreeNode parent, ParentType parentType) {
|
||||
for (ParentInformation entry : parents)
|
||||
if (entry.getNode().equals(parent) && entry.getType() == parentType)
|
||||
return entry.getLevel();
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean hasParent(SkillTreeNode parent) {
|
||||
for (ParentInformation entry : parents)
|
||||
if (entry.getNode() == parent) return true;
|
||||
for (var edge : parents)
|
||||
if (edge.getParent().equals(parent)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -135,12 +165,6 @@ public class SkillTreeNode implements ExperienceObject {
|
||||
return parents;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Deprecated
|
||||
public List<SkillTreeNode> getParents(ParentType parentType) {
|
||||
return parents.stream().filter(integer -> integer.getType() == parentType).map(ParentInformation::getNode).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<ParentInformation> getChildren() {
|
||||
return children;
|
||||
@ -155,7 +179,7 @@ public class SkillTreeNode implements ExperienceObject {
|
||||
|
||||
/**
|
||||
* @return Full node identifier, containing both the node identifier AND
|
||||
* the skill tree identifier, like "combat_extra_strength"
|
||||
* the skill tree identifier, like "combat_extra_strength"
|
||||
*/
|
||||
@NotNull
|
||||
public String getFullId() {
|
||||
@ -168,7 +192,7 @@ public class SkillTreeNode implements ExperienceObject {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public IntegerCoordinates getCoordinates() {
|
||||
public IntCoords getCoordinates() {
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
@ -195,30 +219,6 @@ public class SkillTreeNode implements ExperienceObject {
|
||||
return experienceTable != null;
|
||||
}
|
||||
|
||||
public NodeShape getNodeType() {
|
||||
boolean up = tree.isPathOrNode(new IntegerCoordinates(coordinates.getX(), coordinates.getY() - 1));
|
||||
boolean down = tree.isPathOrNode(new IntegerCoordinates(coordinates.getX(), coordinates.getY() + 1));
|
||||
boolean right = tree.isPathOrNode(new IntegerCoordinates(coordinates.getX() + 1, coordinates.getY()));
|
||||
boolean left = tree.isPathOrNode(new IntegerCoordinates(coordinates.getX() - 1, coordinates.getY()));
|
||||
|
||||
if (up && right && down && left) return NodeShape.UP_RIGHT_DOWN_LEFT;
|
||||
else if (up && right && down) return NodeShape.UP_RIGHT_DOWN;
|
||||
else if (up && right && left) return NodeShape.UP_RIGHT_LEFT;
|
||||
else if (up && down && left) return NodeShape.UP_DOWN_LEFT;
|
||||
else if (down && right && left) return NodeShape.DOWN_RIGHT_LEFT;
|
||||
else if (up && right) return NodeShape.UP_RIGHT;
|
||||
else if (up && down) return NodeShape.UP_DOWN;
|
||||
else if (up && left) return NodeShape.UP_LEFT;
|
||||
else if (down && right) return NodeShape.DOWN_RIGHT;
|
||||
else if (down && left) return NodeShape.DOWN_LEFT;
|
||||
else if (right && left) return NodeShape.RIGHT_LEFT;
|
||||
else if (up) return NodeShape.UP;
|
||||
else if (down) return NodeShape.DOWN;
|
||||
else if (right) return NodeShape.RIGHT;
|
||||
else if (left) return NodeShape.LEFT;
|
||||
return NodeShape.NO_PATH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
@ -277,6 +277,26 @@ public class SkillTreeNode implements ExperienceObject {
|
||||
|
||||
//region Deprecated
|
||||
|
||||
@Deprecated
|
||||
public int getParentNeededLevel(SkillTreeNode parent) {
|
||||
for (var edge : parents)
|
||||
if (edge.getParent().equals(parent)) return edge.getLevel();
|
||||
throw new RuntimeException("Could not find parent " + parent.getId() + " for node " + id);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Deprecated
|
||||
public List<SkillTreeNode> getParents(ParentType parentType) {
|
||||
return parents.stream().filter(integer -> integer.getType() == parentType).map(ParentInformation::getParent).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public int getParentNeededLevel(SkillTreeNode parent, ParentType parentType) {
|
||||
for (var edge : parents)
|
||||
if (edge.getParent().equals(parent) && edge.getType() == parentType) return edge.getLevel();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public boolean hasIcon(NodeState status) {
|
||||
return getIcon(status) != null;
|
||||
|
||||
@ -1,67 +0,0 @@
|
||||
package net.Indyuce.mmocore.skilltree;
|
||||
|
||||
import net.Indyuce.mmocore.api.player.PlayerData;
|
||||
import net.Indyuce.mmocore.skilltree.display.PathShape;
|
||||
import net.Indyuce.mmocore.skilltree.tree.SkillTree;
|
||||
|
||||
public class SkillTreePath {
|
||||
private final SkillTree tree;
|
||||
private final IntegerCoordinates coordinates;
|
||||
private final SkillTreeNode from;
|
||||
private final SkillTreeNode to;
|
||||
|
||||
public SkillTreePath(SkillTree tree, IntegerCoordinates coordinates, SkillTreeNode from, SkillTreeNode skillTreeNode) {
|
||||
this.tree = tree;
|
||||
this.coordinates = coordinates;
|
||||
this.from = from;
|
||||
to = skillTreeNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the status of a path between two nodes, which is determined
|
||||
* by the pair of states of the two nodes.
|
||||
*/
|
||||
public PathState getStatus(PlayerData playerData) {
|
||||
var from = playerData.getNodeState(this.from);
|
||||
var to = playerData.getNodeState(this.to);
|
||||
|
||||
// Either one is fully locked => gray out path
|
||||
if (from == NodeState.FULLY_LOCKED || to == NodeState.FULLY_LOCKED) return PathState.FULLY_LOCKED;
|
||||
|
||||
// Both are unlocked => path is taken, unlocked
|
||||
if (from.isUnlocked() && to.isUnlocked()) return PathState.UNLOCKED;
|
||||
|
||||
// One of them is unlocked, other one is unlockable => path is not taken yet, but can be
|
||||
if ((from == NodeState.UNLOCKABLE && to.isUnlocked()) || (from.isUnlocked() && to == NodeState.UNLOCKABLE))
|
||||
return PathState.UNLOCKABLE;
|
||||
|
||||
// Otherwise, locked path
|
||||
return PathState.LOCKED;
|
||||
}
|
||||
|
||||
public PathShape getPathType() {
|
||||
IntegerCoordinates upCoor = new IntegerCoordinates(coordinates.getX(), coordinates.getY() - 1);
|
||||
IntegerCoordinates downCoor = new IntegerCoordinates(coordinates.getX(), coordinates.getY() + 1);
|
||||
IntegerCoordinates rightCoor = new IntegerCoordinates(coordinates.getX() + 1, coordinates.getY());
|
||||
IntegerCoordinates leftCoor = new IntegerCoordinates(coordinates.getX() - 1, coordinates.getY());
|
||||
boolean hasUp = tree.isPath(upCoor) || upCoor.equals(from.getCoordinates()) || upCoor.equals(to.getCoordinates());
|
||||
boolean hasDown = tree.isPath(downCoor) || downCoor.equals(from.getCoordinates()) || downCoor.equals(to.getCoordinates());
|
||||
boolean hasRight = tree.isPath(rightCoor) || rightCoor.equals(from.getCoordinates()) || rightCoor.equals(to.getCoordinates());
|
||||
boolean hasLeft = tree.isPath(leftCoor) || leftCoor.equals(from.getCoordinates()) || leftCoor.equals(to.getCoordinates());
|
||||
|
||||
if ((hasUp || hasDown) && !hasLeft && !hasRight) {
|
||||
return PathShape.UP;
|
||||
} else if ((hasRight || hasLeft) && !hasUp && !hasDown) {
|
||||
return PathShape.RIGHT;
|
||||
} else if (hasUp && hasRight) {
|
||||
return PathShape.UP_RIGHT;
|
||||
} else if (hasUp && hasLeft) {
|
||||
return PathShape.UP_LEFT;
|
||||
} else if (hasDown && hasRight) {
|
||||
return PathShape.DOWN_RIGHT;
|
||||
} else if (hasDown && hasLeft) {
|
||||
return PathShape.DOWN_LEFT;
|
||||
}
|
||||
return PathShape.DEFAULT;
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ package net.Indyuce.mmocore.skilltree.display;
|
||||
import io.lumine.mythic.lib.UtilityMethods;
|
||||
import io.lumine.mythic.lib.gui.util.IconOptions;
|
||||
import net.Indyuce.mmocore.skilltree.NodeState;
|
||||
import net.Indyuce.mmocore.skilltree.PathState;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package net.Indyuce.mmocore.skilltree.display;
|
||||
|
||||
import net.Indyuce.mmocore.skilltree.PathState;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
@ -15,7 +14,7 @@ public class PathDisplayInfo {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public PathState getStatus() {
|
||||
public PathState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
package net.Indyuce.mmocore.skilltree;
|
||||
package net.Indyuce.mmocore.skilltree.display;
|
||||
|
||||
import net.Indyuce.mmocore.api.player.PlayerData;
|
||||
|
||||
/**
|
||||
* @see SkillTreePath#getStatus(PlayerData)
|
||||
*/
|
||||
public enum PathState {
|
||||
|
||||
/**
|
||||
@ -1,6 +1,7 @@
|
||||
package net.Indyuce.mmocore.skilltree.tree;
|
||||
|
||||
import net.Indyuce.mmocore.skilltree.IntegerCoordinates;
|
||||
import net.Indyuce.mmocore.skilltree.IntCoords;
|
||||
import net.Indyuce.mmocore.skilltree.ParentInformation;
|
||||
import net.Indyuce.mmocore.skilltree.ParentType;
|
||||
import net.Indyuce.mmocore.skilltree.SkillTreeNode;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
@ -10,20 +11,20 @@ public class ProximitySkillTree extends SkillTree {
|
||||
super(config);
|
||||
|
||||
// Neighbors are marked as soft parents
|
||||
for (SkillTreeNode node : nodes.values())
|
||||
for (IntegerCoordinates relative : RELATIVES) {
|
||||
for (var node : nodes.values())
|
||||
for (var relative : RELATIVES) {
|
||||
final SkillTreeNode neighbor = this.getNodeOrNull(node.getCoordinates().add(relative));
|
||||
if (neighbor != null) {
|
||||
node.addParent(neighbor, ParentType.SOFT, 1);
|
||||
neighbor.addChild(node, ParentType.SOFT, 1);
|
||||
final var parentInfo = new ParentInformation(node, neighbor, ParentType.SOFT, true,1);
|
||||
node.addParent(parentInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final IntegerCoordinates[] RELATIVES = {
|
||||
new IntegerCoordinates(1, 0),
|
||||
new IntegerCoordinates(-1, 0),
|
||||
new IntegerCoordinates(0, 1),
|
||||
new IntegerCoordinates(0, -1)
|
||||
private static final IntCoords[] RELATIVES = {
|
||||
new IntCoords(1, 0),
|
||||
new IntCoords(-1, 0),
|
||||
new IntCoords(0, 1),
|
||||
new IntCoords(0, -1)
|
||||
};
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ import net.Indyuce.mmocore.api.player.PlayerData;
|
||||
import net.Indyuce.mmocore.manager.registry.RegisteredObject;
|
||||
import net.Indyuce.mmocore.skilltree.*;
|
||||
import net.Indyuce.mmocore.skilltree.display.DisplayMap;
|
||||
import net.Indyuce.mmocore.skilltree.display.NodeShape;
|
||||
import net.Indyuce.mmocore.skilltree.display.PathState;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@ -41,9 +43,6 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
protected final List<SkillTreeNode> roots = new ArrayList<>();
|
||||
protected final DisplayMap icons;
|
||||
|
||||
protected final Map<IntegerCoordinates, SkillTreeNode> coordNodes = new HashMap<>();
|
||||
protected final Map<IntegerCoordinates, SkillTreePath> coordPaths = new HashMap<>();
|
||||
|
||||
public SkillTree(@NotNull ConfigurationSection config) {
|
||||
this.id = Objects.requireNonNull(config.getString("id"), "Could not find skill tree id");
|
||||
this.name = MythicLib.plugin.parseColors(Objects.requireNonNull(config.getString("name"), "Could not find skill tree name"));
|
||||
@ -59,54 +58,28 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
ConfigurationSection section = config.getConfigurationSection("nodes." + key);
|
||||
SkillTreeNode node = new SkillTreeNode(this, section);
|
||||
nodes.put(node.getId(), node);
|
||||
coordNodes.put(node.getCoordinates(), node);
|
||||
nodeByCoordinate.put(node.getCoordinates(), node);
|
||||
|
||||
if (node.isRoot()) roots.add(node);
|
||||
} catch (Exception e) {
|
||||
MMOCore.log("Couldn't load skill tree node " + id + "." + key + ": " + e.getMessage());
|
||||
} catch (Exception exception) {
|
||||
MMOCore.log(Level.WARNING, "Couldn't load skill tree node " + id + "." + key + ": " + exception.getMessage());
|
||||
}
|
||||
|
||||
// Load paths
|
||||
for (String from : config.getConfigurationSection("nodes").getKeys(false)) {
|
||||
ConfigurationSection section = config.getConfigurationSection("nodes." + from);
|
||||
if (section.contains("paths")) {
|
||||
for (String to : section.getConfigurationSection("paths").getKeys(false)) {
|
||||
SkillTreeNode node1 = nodes.get(to);
|
||||
if (node1 == null) {
|
||||
MMOCore.log("Couldn't find node " + to + " for path in node " + from + ".");
|
||||
continue;
|
||||
}
|
||||
for (String pathKey : section.getConfigurationSection("paths." + to).getKeys(false)) {
|
||||
IntegerCoordinates coordinates = IntegerCoordinates.from(section.get("paths." + to + "." + pathKey));
|
||||
coordPaths.put(coordinates, new SkillTreePath(this, coordinates, nodes.get(from), node1));
|
||||
}
|
||||
}
|
||||
// Post load all nodes
|
||||
for (var node : nodes.values())
|
||||
try {
|
||||
node.getPostLoadAction().performAction();
|
||||
node.getParents().forEach(parentInfo -> parentInfo.getElements().forEach(coords -> this.pathByCoordinate.put(coords, parentInfo)));
|
||||
} catch (Exception exception) {
|
||||
MMOCore.log(Level.WARNING, "Couldn't post-load skill tree node " + id + "." + node.getId() + ": " + exception.getMessage());
|
||||
exception.printStackTrace(); // TODO remove
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve node shapes
|
||||
resolveNodeShapes();
|
||||
|
||||
// Load icons
|
||||
this.icons = DisplayMap.from(config.getConfigurationSection("display"));
|
||||
|
||||
// Setup children and parents for each node
|
||||
for (SkillTreeNode node : nodes.values())
|
||||
try {
|
||||
if (config.isConfigurationSection("nodes." + node.getId() + ".parents"))
|
||||
for (String key : config.getConfigurationSection("nodes." + node.getId() + ".parents").getKeys(false)) {
|
||||
final ConfigurationSection section = config.getConfigurationSection("nodes." + node.getId() + ".parents." + key);
|
||||
if (section != null) {
|
||||
final ParentType parentType = ParentType.valueOf(UtilityMethods.enumName(key));
|
||||
|
||||
for (String parentId : section.getKeys(false)) {
|
||||
final SkillTreeNode parent = getNode(parentId);
|
||||
final int level = section.getInt(parentId);
|
||||
node.addParent(parent, parentType, level);
|
||||
parent.addChild(node, parentType, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
MMOCore.plugin.getLogger().log(Level.WARNING, "Could not load parents of skill tree node '" + node.getId() + "': " + exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getLore() {
|
||||
@ -130,21 +103,46 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
return roots;
|
||||
}
|
||||
|
||||
//region Resolving states and shapes
|
||||
|
||||
private final Map<SkillTreeNode, NodeShape> nodeShapes = new HashMap<>();
|
||||
|
||||
@NotNull
|
||||
public NodeShape getNodeShape(@NotNull SkillTreeNode node) {
|
||||
return Objects.requireNonNull(nodeShapes.get(node), "Missing node shape");
|
||||
}
|
||||
|
||||
private void resolveNodeShapes() {
|
||||
for (var node : nodes.values()) nodeShapes.put(node, resolveNodeShape(node));
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO Write documentation
|
||||
* Resets all states saved for the nodes and edges of
|
||||
* this skill tree and recomputes them from scratch solely
|
||||
* based on the nodes unlocked (node level map) by the
|
||||
* player and points spent in each of them.
|
||||
* <p>
|
||||
* TODO Use some collection and progressively filter out the nodes to avoid useless iterations
|
||||
* <p>
|
||||
* Let:
|
||||
* - A the number of path elements (not edges)
|
||||
* - V denote the number of nodes in the skill tree
|
||||
* - P the number of parents any node, has at most
|
||||
* - P the number of parents any node has, at most
|
||||
* - C the number of children any node has, at most
|
||||
* <p>
|
||||
* This algorithm runs in O(V * P * C)
|
||||
* This algorithm runs in O(V * P * C). In Minecraft nodes
|
||||
* can't have more than 4 children or parents which makes
|
||||
* it O(V).
|
||||
*
|
||||
* @author jules
|
||||
*/
|
||||
public void setupNodeStates(@NotNull PlayerData playerData) {
|
||||
public void resolveStates(@NotNull PlayerData playerData) {
|
||||
playerData.clearStates(this);
|
||||
resolveNodeStates(playerData);
|
||||
resolvePathStates(playerData);
|
||||
}
|
||||
|
||||
// Reinitialization
|
||||
playerData.clearNodeStates(this);
|
||||
private void resolveNodeStates(@NotNull PlayerData playerData) {
|
||||
|
||||
// If the player has already spent the maximum amount of points in this skill tree.
|
||||
final boolean skillTreeLocked = playerData.getPointsSpent(this) >= this.maxPointSpent;
|
||||
@ -153,25 +151,27 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
// PASS 1
|
||||
//
|
||||
// Initialization. Mark all nodes either locked or unlocked
|
||||
for (SkillTreeNode node : nodes.values())
|
||||
playerData.setNodeState(node, playerData.getNodeLevel(node) > 0 ? NodeState.UNLOCKED : lockState);
|
||||
// Mark nodes as "Maxed out" if maximum level is reached.
|
||||
for (var node : nodes.values()) {
|
||||
final var nodeLevel = playerData.getNodeLevel(node);
|
||||
playerData.setNodeState(node, nodeLevel == 0 ? lockState : nodeLevel == node.getMaxLevel() ? NodeState.MAXED_OUT : NodeState.UNLOCKED);
|
||||
}
|
||||
|
||||
if (skillTreeLocked) return;
|
||||
|
||||
// PASS 2
|
||||
//
|
||||
// Apply basic unreachability rules in O(V * [C + P])
|
||||
// Apply level 1-unreachability rules in O(V * [C + P])
|
||||
// It has to differ from pass 1 because it uses results from pass 1.
|
||||
final Stack<SkillTreeNode> unreachable = new Stack<>();
|
||||
final Set<SkillTreeNode> updated = new HashSet<>();
|
||||
final var unreachable = new Stack<SkillTreeNode>();
|
||||
|
||||
for (SkillTreeNode node : nodes.values()) {
|
||||
for (var node : nodes.values()) {
|
||||
|
||||
// INCOMPATIBILITY RULES
|
||||
//
|
||||
// Any node with an unlocked incompatible parent is made unreachable.
|
||||
for (ParentInformation parent : node.getParents())
|
||||
if (parent.getType() == ParentType.INCOMPATIBLE && playerData.getNodeState(parent.getNode()) == NodeState.UNLOCKED) {
|
||||
for (var edge : node.getParents())
|
||||
if (edge.getType() == ParentType.INCOMPATIBLE && playerData.getNodeState(edge.getParent()).isUnlocked()) {
|
||||
unreachable.add(node);
|
||||
break;
|
||||
}
|
||||
@ -184,13 +184,14 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
if (maxChildren > 0) {
|
||||
|
||||
int unlocked = 0;
|
||||
final List<SkillTreeNode> locked = new ArrayList<>();
|
||||
final var locked = new ArrayList<SkillTreeNode>();
|
||||
|
||||
for (ParentInformation child : node.getChildren())
|
||||
switch (playerData.getNodeState(child.getNode())) {
|
||||
for (var edge : node.getChildren())
|
||||
switch (playerData.getNodeState(edge.getChild())) {
|
||||
case LOCKED:
|
||||
locked.add(child.getNode());
|
||||
locked.add(edge.getChild());
|
||||
break;
|
||||
case MAXED_OUT:
|
||||
case UNLOCKED:
|
||||
unlocked++;
|
||||
break;
|
||||
@ -202,17 +203,18 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
|
||||
// PASS 3
|
||||
//
|
||||
// Propagate unreachability in O(V * C * P)
|
||||
// Propagate level 1-unreachability in O(V * C * P)
|
||||
// Unreachability is transitive, if one node is unreachable, all subsequent
|
||||
// child nodes are all unreachable.
|
||||
final var unreachableCheck = new HashSet<SkillTreeNode>();
|
||||
while (!unreachable.empty()) {
|
||||
final SkillTreeNode node = unreachable.pop();
|
||||
final var node = unreachable.pop();
|
||||
|
||||
updated.add(node);
|
||||
unreachableCheck.add(node);
|
||||
playerData.setNodeState(node, NodeState.FULLY_LOCKED);
|
||||
for (ParentInformation child : node.getChildren()) // Propagate
|
||||
if (!updated.contains(child.getNode()) && isUnreachable(child.getNode(), playerData))
|
||||
unreachable.push(child.getNode());
|
||||
for (var edge : node.getChildren()) // Propagate
|
||||
if (!unreachableCheck.contains(edge.getChild()) && isUnreachable(edge.getChild(), playerData))
|
||||
unreachable.push(edge.getChild());
|
||||
}
|
||||
|
||||
// PASS 4
|
||||
@ -221,7 +223,7 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
// because the distance between the set of all unlocked nodes and the set
|
||||
// of all unlockable nodes is at most 1 (unlockability is not "transitive")
|
||||
pass4:
|
||||
for (SkillTreeNode node : nodes.values()) {
|
||||
for (var node : nodes.values()) {
|
||||
if (playerData.getNodeState(node) != NodeState.LOCKED) continue;
|
||||
|
||||
// ROOT NODES
|
||||
@ -239,12 +241,12 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
// One soft parent of any node must be unlocked for the node to be unlockable.
|
||||
boolean soft = false, hasSoft = false;
|
||||
|
||||
for (ParentInformation parent : node.getParents()) {
|
||||
if (parent.getType() == ParentType.STRONG && playerData.getNodeLevel(parent.getNode()) < parent.getLevel())
|
||||
for (var edge : node.getParents()) {
|
||||
if (edge.getType() == ParentType.STRONG && playerData.getNodeLevel(edge.getParent()) < edge.getLevel())
|
||||
continue pass4; // Keep the node locked
|
||||
else if (!soft && parent.getType() == ParentType.SOFT) {
|
||||
else if (!soft && edge.getType() == ParentType.SOFT) {
|
||||
hasSoft = true;
|
||||
if (playerData.getNodeLevel(parent.getNode()) >= parent.getLevel())
|
||||
if (playerData.getNodeLevel(edge.getParent()) >= edge.getLevel())
|
||||
soft = true; // Cannot continue, must check for other strong parents
|
||||
}
|
||||
}
|
||||
@ -252,16 +254,78 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
// At least one soft parent!
|
||||
if (!hasSoft || soft) playerData.setNodeState(node, NodeState.UNLOCKABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void resolvePathStates(@NotNull PlayerData playerData) {
|
||||
|
||||
// PASS 5
|
||||
//
|
||||
// I'm not sure this is the best place to do that but it works just fine.
|
||||
// Mark unlocked nodes as maxed out if maximum number of points spent
|
||||
for (var node : nodes.values())
|
||||
if (playerData.getNodeState(node) == NodeState.UNLOCKED && playerData.getNodeLevel(node) >= node.getMaxLevel())
|
||||
playerData.setNodeState(node, NodeState.MAXED_OUT);
|
||||
// Resolve path states. Iterate through parents of nodes (children would work too)
|
||||
// TODO merge this with steps 1 to 4 (my brain is fried atm)
|
||||
for (var node : nodeByCoordinate.values())
|
||||
for (var edge : node.getParents())
|
||||
playerData.setPathState(edge, resolvePathState(playerData, edge));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private PathState resolvePathState(@NotNull PlayerData playerData, @NotNull ParentInformation edge) {
|
||||
|
||||
final var from = playerData.getNodeState(edge.getParent());
|
||||
final var to = playerData.getNodeState(edge.getChild());
|
||||
final var symm = edge.isSymmetrical();
|
||||
|
||||
// Gray out path if target is fully locked
|
||||
// If symmetrical, check again after permutation
|
||||
if (to == NodeState.FULLY_LOCKED || (symm && from == NodeState.FULLY_LOCKED))
|
||||
return PathState.FULLY_LOCKED;
|
||||
|
||||
// Both are unlocked => path is taken, unlocked
|
||||
// Symmetric relation so 'symm' does not matter
|
||||
if (from.isUnlocked() && to.isUnlocked()) return PathState.UNLOCKED;
|
||||
|
||||
// If source is unlocked and target unlockable => UNLOCKABLE
|
||||
// If symmetrical, check again after permutation
|
||||
if ((from.isUnlocked() && to == NodeState.UNLOCKABLE) || (symm && (to.isUnlocked() && from == NodeState.UNLOCKABLE)))
|
||||
return PathState.UNLOCKABLE;
|
||||
|
||||
// Locked path by default
|
||||
return PathState.LOCKED;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private NodeShape resolveNodeShape(@NotNull SkillTreeNode node) {
|
||||
final var coordinates = node.getCoordinates();
|
||||
|
||||
final var upCoords = new IntCoords(coordinates.getX(), coordinates.getY() - 1);
|
||||
final var downCoords = new IntCoords(coordinates.getX(), coordinates.getY() + 1);
|
||||
final var rightCoords = new IntCoords(coordinates.getX() + 1, coordinates.getY());
|
||||
final var leftCoords = new IntCoords(coordinates.getX() - 1, coordinates.getY());
|
||||
|
||||
final var up = this.nodeByCoordinate.containsKey(upCoords) || this.pathByCoordinate.containsKey(upCoords);
|
||||
final var down = this.nodeByCoordinate.containsKey(downCoords) || this.pathByCoordinate.containsKey(downCoords);
|
||||
final var right = this.nodeByCoordinate.containsKey(rightCoords) || this.pathByCoordinate.containsKey(rightCoords);
|
||||
final var left = this.nodeByCoordinate.containsKey(leftCoords) || this.pathByCoordinate.containsKey(leftCoords);
|
||||
|
||||
if (up && right && down && left) return NodeShape.UP_RIGHT_DOWN_LEFT;
|
||||
else if (up && right && down) return NodeShape.UP_RIGHT_DOWN;
|
||||
else if (up && right && left) return NodeShape.UP_RIGHT_LEFT;
|
||||
else if (up && down && left) return NodeShape.UP_DOWN_LEFT;
|
||||
else if (down && right && left) return NodeShape.DOWN_RIGHT_LEFT;
|
||||
else if (up && right) return NodeShape.UP_RIGHT;
|
||||
else if (up && down) return NodeShape.UP_DOWN;
|
||||
else if (up && left) return NodeShape.UP_LEFT;
|
||||
else if (down && right) return NodeShape.DOWN_RIGHT;
|
||||
else if (down && left) return NodeShape.DOWN_LEFT;
|
||||
else if (right && left) return NodeShape.RIGHT_LEFT;
|
||||
else if (up) return NodeShape.UP;
|
||||
else if (down) return NodeShape.DOWN;
|
||||
else if (right) return NodeShape.RIGHT;
|
||||
else if (left) return NodeShape.LEFT;
|
||||
return NodeShape.NO_PATH;
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
private boolean isUnreachable(@NotNull SkillTreeNode node, @NotNull PlayerData playerData) {
|
||||
|
||||
// UNREACHABILITY RULES
|
||||
@ -271,12 +335,12 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
// This rule is the logical opposite of the reachability rule.
|
||||
boolean soft = false, hasSoft = false;
|
||||
|
||||
for (ParentInformation parent : node.getParents()) {
|
||||
if (parent.getType() == ParentType.STRONG && playerData.getNodeState(parent.getNode()) == NodeState.FULLY_LOCKED)
|
||||
for (var edge : node.getParents()) {
|
||||
if (edge.getType() == ParentType.STRONG && playerData.getNodeState(edge.getParent()) == NodeState.FULLY_LOCKED)
|
||||
return true;
|
||||
else if (!soft && parent.getType() == ParentType.SOFT) {
|
||||
else if (!soft && edge.getType() == ParentType.SOFT) {
|
||||
hasSoft = true;
|
||||
if (playerData.getNodeState(parent.getNode()) != NodeState.FULLY_LOCKED)
|
||||
if (playerData.getNodeState(edge.getParent()) != NodeState.FULLY_LOCKED)
|
||||
soft = true; // Cannot continue, must check for other strong parents
|
||||
}
|
||||
}
|
||||
@ -284,18 +348,7 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
return hasSoft && !soft;
|
||||
}
|
||||
|
||||
public boolean isNode(@NotNull IntegerCoordinates coordinates) {
|
||||
return coordNodes.containsKey(coordinates);
|
||||
}
|
||||
|
||||
public boolean isPath(@NotNull IntegerCoordinates coordinates) {
|
||||
return coordPaths.containsKey(coordinates);
|
||||
}
|
||||
|
||||
public boolean isPathOrNode(IntegerCoordinates coordinates) {
|
||||
return isNode(coordinates) || isPath(coordinates);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Material getItem() {
|
||||
return item;
|
||||
}
|
||||
@ -305,29 +358,38 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
return id;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Collection<SkillTreeNode> getNodes() {
|
||||
return nodes.values();
|
||||
}
|
||||
|
||||
//region Geometry
|
||||
|
||||
protected final Map<IntCoords, SkillTreeNode> nodeByCoordinate = new HashMap<>();
|
||||
protected final Map<IntCoords, ParentInformation> pathByCoordinate = new HashMap<>();
|
||||
|
||||
@NotNull
|
||||
public SkillTreeNode getNode(@NotNull IntegerCoordinates coords) {
|
||||
return Objects.requireNonNull(coordNodes.get(coords), "Could not find node in tree '" + id + "' with coordinates '" + coords + "'");
|
||||
public SkillTreeNode getNode(@NotNull IntCoords coords) {
|
||||
return Objects.requireNonNull(nodeByCoordinate.get(coords), "Could not find node in tree '" + id + "' with coordinates '" + coords + "'");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SkillTreeNode getNodeOrNull(@NotNull IntegerCoordinates coords) {
|
||||
return coordNodes.get(coords);
|
||||
public SkillTreeNode getNodeOrNull(@NotNull IntCoords coords) {
|
||||
return nodeByCoordinate.get(coords);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public SkillTreePath getPath(@NotNull IntegerCoordinates coords) {
|
||||
return Objects.requireNonNull(coordPaths.get(coords), "Could not find path in tree '" + id + "' with coordinates '" + coords + "'");
|
||||
@Nullable
|
||||
public ParentInformation getPath(@NotNull IntCoords coords) {
|
||||
return pathByCoordinate.get(coords);
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
@NotNull
|
||||
public SkillTreeNode getNode(@NotNull String name) {
|
||||
return Objects.requireNonNull(nodes.get(name), "Could not find node in tree '" + id + "' with name '" + name + "'");
|
||||
@ -338,10 +400,6 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
return icons;
|
||||
}
|
||||
|
||||
public boolean isNode(String name) {
|
||||
return nodes.containsKey(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
@ -357,6 +415,28 @@ public abstract class SkillTree implements RegisteredObject {
|
||||
|
||||
//region Deprecated
|
||||
|
||||
@Deprecated
|
||||
public boolean isNode(@NotNull IntCoords coordinates) {
|
||||
// TODO remove usage
|
||||
return nodeByCoordinate.containsKey(coordinates);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public boolean isPath(@NotNull IntCoords coordinates) {
|
||||
// TODO remove usage
|
||||
return pathByCoordinate.containsKey(coordinates);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public boolean isNode(String name) {
|
||||
return nodes.containsKey(name);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public boolean isPathOrNode(IntCoords coordinates) {
|
||||
return isNode(coordinates) || isPath(coordinates);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static SkillTree loadSkillTree(ConfigurationSection config) {
|
||||
return MMOCore.plugin.skillTreeManager.loadSkillTree(config);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user