Skill trees now cache node&path shapes&states to reduce impact on performance

This commit is contained in:
Jules 2025-10-13 23:42:55 +02:00
parent d7dbdfca6e
commit 5225e97042
16 changed files with 631 additions and 385 deletions

View File

@ -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;
}

View File

@ -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
}

View File

@ -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.

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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));
}
}
/**

View File

@ -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() {

View File

@ -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");

View File

@ -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());
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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 {
/**

View File

@ -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)
};
}

View File

@ -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);