extend ability system to support dynamic slot counts, add skill tree validation, new prerequisites API, and extended mod commands
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package net.fretux.skillengine.skilltree;
|
||||
|
||||
import net.fretux.skillengine.api.AbilityHandlerRegistry;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
@@ -48,6 +49,8 @@ public class AbilityNode {
|
||||
public int getCooldown() { return cooldown; }
|
||||
|
||||
public void execute(ServerPlayer player) {
|
||||
player.sendSystemMessage(Component.literal("Activated ability: " + id));
|
||||
if (!AbilityHandlerRegistry.execute(player, this)) {
|
||||
player.sendSystemMessage(Component.literal("Activated ability: " + id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,28 @@ import java.util.Map;
|
||||
|
||||
public class AbilityNodeRegistry {
|
||||
private static final Map<ResourceLocation, AbilityNode> ABILITIES = new LinkedHashMap<>();
|
||||
private static final Map<ResourceLocation, Integer> DUPLICATE_COUNTS = new LinkedHashMap<>();
|
||||
|
||||
public static void clear() {
|
||||
ABILITIES.clear();
|
||||
DUPLICATE_COUNTS.clear();
|
||||
}
|
||||
|
||||
public static void put(AbilityNode node) {
|
||||
ABILITIES.put(node.getId(), node);
|
||||
if (ABILITIES.put(node.getId(), node) != null) {
|
||||
DUPLICATE_COUNTS.merge(node.getId(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
|
||||
public static AbilityNode get(ResourceLocation id) {
|
||||
return ABILITIES.get(id);
|
||||
}
|
||||
|
||||
public static Collection<AbilityNode> all() {
|
||||
return ABILITIES.values();
|
||||
}
|
||||
|
||||
public static Map<ResourceLocation, Integer> duplicateCounts() {
|
||||
return Map.copyOf(DUPLICATE_COUNTS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package net.fretux.skillengine.skilltree;
|
||||
|
||||
import net.fretux.ascend.player.PlayerStatsProvider;
|
||||
import net.fretux.skillengine.api.PrerequisiteProviderRegistry;
|
||||
import net.fretux.skillengine.capability.PlayerSkillData;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -43,13 +44,13 @@ public class SkillLogic {
|
||||
|
||||
public static boolean meetsAscendPrerequisites(Player player, SkillNode node) {
|
||||
if (node.getPrereqAttributes().isEmpty()) return true;
|
||||
return player.getCapability(PlayerStatsProvider.PLAYER_STATS).map(stats -> {
|
||||
for (Map.Entry<String, Integer> req : node.getPrereqAttributes().entrySet()) {
|
||||
int current = stats.getAttributeLevel(req.getKey());
|
||||
if (current < req.getValue()) return false;
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) return true;
|
||||
for (Map.Entry<String, Integer> req : node.getPrereqAttributes().entrySet()) {
|
||||
if (!PrerequisiteProviderRegistry.meets(serverPlayer, node, req.getKey(), req.getValue())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).orElse(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean collectUnlockPlan(PlayerSkillData data,
|
||||
|
||||
@@ -16,6 +16,9 @@ public class SkillNode {
|
||||
private final float y;
|
||||
private final List<ResourceLocation> links;
|
||||
private final List<ResourceLocation> tags;
|
||||
private final ResourceLocation tree;
|
||||
private final String category;
|
||||
private final String layer;
|
||||
private final ResourceLocation icons;
|
||||
private final Map<String, Integer> prereqAttributes;
|
||||
private final List<ResourceLocation> exclusiveWith;
|
||||
@@ -27,6 +30,9 @@ public class SkillNode {
|
||||
float x, float y,
|
||||
List<ResourceLocation> links,
|
||||
List<ResourceLocation> tags,
|
||||
ResourceLocation tree,
|
||||
String category,
|
||||
String layer,
|
||||
ResourceLocation icons,
|
||||
Map<String, Integer> prereqAttributes,
|
||||
List<ResourceLocation> exclusiveWith){
|
||||
@@ -39,6 +45,9 @@ public class SkillNode {
|
||||
this.y = y;
|
||||
this.links = List.copyOf(links);
|
||||
this.tags = List.copyOf(tags);
|
||||
this.tree = tree;
|
||||
this.category = category;
|
||||
this.layer = layer;
|
||||
this.icons = icons;
|
||||
this.prereqAttributes = Map.copyOf(prereqAttributes);
|
||||
this.exclusiveWith = List.copyOf(exclusiveWith);
|
||||
@@ -76,6 +85,18 @@ public class SkillNode {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public ResourceLocation getTree() {
|
||||
return tree;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public String getLayer() {
|
||||
return layer;
|
||||
}
|
||||
|
||||
public ResourceLocation getIcons() {
|
||||
return icons;
|
||||
}
|
||||
@@ -91,4 +112,4 @@ public class SkillNode {
|
||||
public boolean isRoot() {
|
||||
return links.isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,18 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = SkillEngine.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
|
||||
public class SkillNodeLoader extends SimpleJsonResourceReloadListener {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().create();
|
||||
private static final ResourceLocation DEFAULT_TREE =
|
||||
ResourceLocation.fromNamespaceAndPath(SkillEngine.MODID, "main");
|
||||
private static final Pattern TRANSLATION_KEY =
|
||||
Pattern.compile("[a-z0-9_.-]+\\.[a-z0-9_.-]+");
|
||||
|
||||
public SkillNodeLoader() {
|
||||
super(GSON, "skillnodes");
|
||||
@@ -37,15 +41,14 @@ public class SkillNodeLoader extends SimpleJsonResourceReloadListener {
|
||||
@NotNull ResourceManager resourceManager,
|
||||
@NotNull ProfilerFiller profiler) {
|
||||
SkillEngine.LOGGER.info("Loading skill nodes...");
|
||||
validateSingleSkillTreeProvider(jsons);
|
||||
SkillNodeRegistry.clear();
|
||||
jsons.forEach((id, element) -> {
|
||||
JsonObject obj = element.getAsJsonObject();
|
||||
ResourceLocation nodeId = obj.has("id")
|
||||
? ResourceLocation.parse(obj.get("id").getAsString())
|
||||
: id;
|
||||
String title = obj.get("title").getAsString();
|
||||
String description = obj.get("description").getAsString();
|
||||
Component title = readComponent(obj, "title");
|
||||
Component description = readComponent(obj, "description");
|
||||
int cost = obj.get("cost").getAsInt();
|
||||
JsonObject pos = obj.getAsJsonObject("position");
|
||||
float x = pos.get("x").getAsFloat();
|
||||
@@ -54,8 +57,15 @@ public class SkillNodeLoader extends SimpleJsonResourceReloadListener {
|
||||
obj.getAsJsonArray("links").forEach(e ->
|
||||
links.add(ResourceLocation.parse(e.getAsString())));
|
||||
List<ResourceLocation> tags = new ArrayList<>();
|
||||
obj.getAsJsonArray("tags").forEach(e ->
|
||||
tags.add(ResourceLocation.parse(e.getAsString())));
|
||||
if (obj.has("tags")) {
|
||||
obj.getAsJsonArray("tags").forEach(e ->
|
||||
tags.add(ResourceLocation.parse(e.getAsString())));
|
||||
}
|
||||
ResourceLocation tree = obj.has("tree")
|
||||
? ResourceLocation.parse(obj.get("tree").getAsString())
|
||||
: ResourceLocation.fromNamespaceAndPath(nodeId.getNamespace(), DEFAULT_TREE.getPath());
|
||||
String category = obj.has("category") ? obj.get("category").getAsString() : "";
|
||||
String layer = obj.has("layer") ? obj.get("layer").getAsString() : "";
|
||||
ResourceLocation icons = ResourceLocation.parse(obj.get("icons").getAsString());
|
||||
Map<String, Integer> prereqAttributes = new HashMap<>();
|
||||
if (obj.has("prerequisites")) {
|
||||
@@ -71,12 +81,15 @@ public class SkillNodeLoader extends SimpleJsonResourceReloadListener {
|
||||
}
|
||||
SkillNode node = new SkillNode(
|
||||
nodeId,
|
||||
Component.literal(title),
|
||||
Component.literal(description),
|
||||
title,
|
||||
description,
|
||||
cost,
|
||||
x, y,
|
||||
links,
|
||||
tags,
|
||||
tree,
|
||||
category,
|
||||
layer,
|
||||
icons,
|
||||
prereqAttributes,
|
||||
exclusiveWith
|
||||
@@ -87,28 +100,23 @@ public class SkillNodeLoader extends SimpleJsonResourceReloadListener {
|
||||
SkillEngine.LOGGER.info("Loaded {} skill nodes", SkillNodeRegistry.all().size());
|
||||
}
|
||||
|
||||
private static void validateSingleSkillTreeProvider(Map<ResourceLocation, JsonElement> jsons) {
|
||||
Map<String, List<ResourceLocation>> providers = new LinkedHashMap<>();
|
||||
jsons.keySet().stream()
|
||||
.filter(id -> !SkillEngine.MODID.equals(id.getNamespace()))
|
||||
.forEach(id -> providers.computeIfAbsent(id.getNamespace(), ignored -> new ArrayList<>()).add(id));
|
||||
|
||||
if (providers.size() <= 1) {
|
||||
return;
|
||||
private static Component readComponent(JsonObject obj, String key) {
|
||||
if (!obj.has(key)) {
|
||||
return Component.empty();
|
||||
}
|
||||
|
||||
StringBuilder message = new StringBuilder();
|
||||
message.append("Skill Engine detected multiple mods adding skill tree nodes: ");
|
||||
providers.forEach((namespace, nodeIds) ->
|
||||
message.append(namespace)
|
||||
.append(" (")
|
||||
.append(nodeIds.size())
|
||||
.append(" nodes), "));
|
||||
message.setLength(message.length() - 2);
|
||||
message.append(". Skill Engine supports one skill tree provider at a time because multiple skill trees overlap in the same UI. Remove all but one skill tree addon.");
|
||||
|
||||
String error = message.toString();
|
||||
SkillEngine.LOGGER.error(error);
|
||||
throw new IllegalStateException(error);
|
||||
JsonElement element = obj.get(key);
|
||||
if (element.isJsonObject()) {
|
||||
JsonObject text = element.getAsJsonObject();
|
||||
if (text.has("translate")) {
|
||||
return Component.translatable(text.get("translate").getAsString());
|
||||
}
|
||||
if (text.has("text")) {
|
||||
return Component.literal(text.get("text").getAsString());
|
||||
}
|
||||
}
|
||||
String value = element.getAsString();
|
||||
return TRANSLATION_KEY.matcher(value).matches()
|
||||
? Component.translatable(value)
|
||||
: Component.literal(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,36 @@ package net.fretux.skillengine.skilltree;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SkillNodeRegistry {
|
||||
|
||||
private static final Map<ResourceLocation, SkillNode> NODES = new LinkedHashMap<>();
|
||||
private static final Map<ResourceLocation, Map<ResourceLocation, SkillNode>> NODES_BY_TREE = new LinkedHashMap<>();
|
||||
private static final Map<ResourceLocation, Integer> DUPLICATE_COUNTS = new LinkedHashMap<>();
|
||||
|
||||
public static void clear() {
|
||||
NODES.clear();
|
||||
NODES_BY_TREE.clear();
|
||||
DUPLICATE_COUNTS.clear();
|
||||
}
|
||||
|
||||
public static void put(SkillNode node) {
|
||||
NODES.put(node.getId(), node);
|
||||
SkillNode previous = NODES.put(node.getId(), node);
|
||||
if (previous != null) {
|
||||
DUPLICATE_COUNTS.merge(node.getId(), 1, Integer::sum);
|
||||
Map<ResourceLocation, SkillNode> previousTree = NODES_BY_TREE.get(previous.getTree());
|
||||
if (previousTree != null) {
|
||||
previousTree.remove(previous.getId());
|
||||
if (previousTree.isEmpty()) {
|
||||
NODES_BY_TREE.remove(previous.getTree());
|
||||
}
|
||||
}
|
||||
}
|
||||
NODES_BY_TREE.computeIfAbsent(node.getTree(), ignored -> new LinkedHashMap<>())
|
||||
.put(node.getId(), node);
|
||||
}
|
||||
|
||||
public static SkillNode get(ResourceLocation id) {
|
||||
@@ -25,4 +42,17 @@ public class SkillNodeRegistry {
|
||||
public static Collection<SkillNode> all() {
|
||||
return NODES.values();
|
||||
}
|
||||
|
||||
public static List<ResourceLocation> trees() {
|
||||
return List.copyOf(NODES_BY_TREE.keySet());
|
||||
}
|
||||
|
||||
public static Collection<SkillNode> byTree(ResourceLocation tree) {
|
||||
Map<ResourceLocation, SkillNode> nodes = NODES_BY_TREE.get(tree);
|
||||
return nodes != null ? nodes.values() : List.of();
|
||||
}
|
||||
|
||||
public static Map<ResourceLocation, Integer> duplicateCounts() {
|
||||
return Map.copyOf(DUPLICATE_COUNTS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package net.fretux.skillengine.skilltree;
|
||||
|
||||
import net.fretux.skillengine.api.PrerequisiteProviderRegistry;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SkillTreeValidator {
|
||||
|
||||
private SkillTreeValidator() {}
|
||||
|
||||
public static List<String> validate() {
|
||||
List<String> issues = new ArrayList<>();
|
||||
SkillNodeRegistry.duplicateCounts().forEach((id, count) ->
|
||||
issues.add(id + " was loaded " + (count + 1) + " times; node ids must be unique"));
|
||||
AbilityNodeRegistry.duplicateCounts().forEach((id, count) ->
|
||||
issues.add(id + " was loaded " + (count + 1) + " times; ability ids must be unique"));
|
||||
for (SkillNode node : SkillNodeRegistry.all()) {
|
||||
validateNode(node, issues);
|
||||
}
|
||||
for (AbilityNode ability : AbilityNodeRegistry.all()) {
|
||||
validateAbility(ability, issues);
|
||||
}
|
||||
detectSkillCycles(issues);
|
||||
detectAbilityCycles(issues);
|
||||
return issues;
|
||||
}
|
||||
|
||||
private static void validateNode(SkillNode node, List<String> issues) {
|
||||
if (!isPngTexture(node.getIcons())) {
|
||||
issues.add(node.getId() + " has invalid icon path " + node.getIcons() + " (expected a .png texture)");
|
||||
}
|
||||
for (ResourceLocation parent : node.getLinks()) {
|
||||
if (SkillNodeRegistry.get(parent) == null) {
|
||||
issues.add(node.getId() + " links to missing skill node " + parent);
|
||||
}
|
||||
}
|
||||
for (ResourceLocation exclusive : node.getExclusiveWith()) {
|
||||
SkillNode other = SkillNodeRegistry.get(exclusive);
|
||||
if (other == null) {
|
||||
issues.add(node.getId() + " is exclusive with missing node " + exclusive);
|
||||
} else if (exclusive.equals(node.getId())) {
|
||||
issues.add(node.getId() + " is exclusive with itself");
|
||||
} else if (!other.getExclusiveWith().contains(node.getId())) {
|
||||
issues.add(node.getId() + " is exclusive with " + exclusive + " but the reverse entry is missing");
|
||||
}
|
||||
}
|
||||
Set<ResourceLocation> requiredSkillNodes = collectRequiredSkillNodes(node);
|
||||
for (ResourceLocation exclusive : node.getExclusiveWith()) {
|
||||
if (requiredSkillNodes.contains(exclusive)) {
|
||||
issues.add(node.getId() + " is impossible to unlock because required ancestor " + exclusive + " is mutually exclusive");
|
||||
}
|
||||
}
|
||||
List<ResourceLocation> requiredList = new ArrayList<>(requiredSkillNodes);
|
||||
for (int i = 0; i < requiredList.size(); i++) {
|
||||
SkillNode first = SkillNodeRegistry.get(requiredList.get(i));
|
||||
if (first == null) continue;
|
||||
for (int j = i + 1; j < requiredList.size(); j++) {
|
||||
ResourceLocation second = requiredList.get(j);
|
||||
if (first.getExclusiveWith().contains(second)) {
|
||||
issues.add(node.getId() + " is impossible to unlock because required nodes "
|
||||
+ first.getId() + " and " + second + " are mutually exclusive");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String key : node.getPrereqAttributes().keySet()) {
|
||||
int separator = key.indexOf(':');
|
||||
if (separator >= 0 && (separator == 0 || separator == key.length() - 1)) {
|
||||
issues.add(node.getId() + " has malformed prerequisite key " + key);
|
||||
continue;
|
||||
}
|
||||
String namespace = separator > 0 ? key.substring(0, separator) : "ascend";
|
||||
if (!PrerequisiteProviderRegistry.hasProvider(namespace)) {
|
||||
issues.add(node.getId() + " requires " + key + " but no prerequisite provider is registered for " + namespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateAbility(AbilityNode ability, List<String> issues) {
|
||||
if (!isPngTexture(ability.getIcon())) {
|
||||
issues.add(ability.getId() + " has invalid icon path " + ability.getIcon() + " (expected a .png texture)");
|
||||
}
|
||||
for (ResourceLocation parent : ability.getLinks()) {
|
||||
if (SkillNodeRegistry.get(parent) == null && AbilityNodeRegistry.get(parent) == null) {
|
||||
issues.add(ability.getId() + " links to missing node or ability " + parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPngTexture(ResourceLocation id) {
|
||||
return id != null && id.getPath().endsWith(".png");
|
||||
}
|
||||
|
||||
private static void detectSkillCycles(List<String> issues) {
|
||||
Set<ResourceLocation> visited = new HashSet<>();
|
||||
Set<ResourceLocation> visiting = new HashSet<>();
|
||||
for (SkillNode node : SkillNodeRegistry.all()) {
|
||||
visit(node, visited, visiting, issues);
|
||||
}
|
||||
}
|
||||
|
||||
private static void detectAbilityCycles(List<String> issues) {
|
||||
Set<ResourceLocation> visited = new HashSet<>();
|
||||
Set<ResourceLocation> visiting = new HashSet<>();
|
||||
for (AbilityNode ability : AbilityNodeRegistry.all()) {
|
||||
visitAbility(ability, visited, visiting, issues);
|
||||
}
|
||||
}
|
||||
|
||||
private static void visit(SkillNode node,
|
||||
Set<ResourceLocation> visited,
|
||||
Set<ResourceLocation> visiting,
|
||||
List<String> issues) {
|
||||
ResourceLocation id = node.getId();
|
||||
if (visited.contains(id)) return;
|
||||
if (!visiting.add(id)) {
|
||||
issues.add("Cycle detected at skill node " + id);
|
||||
return;
|
||||
}
|
||||
for (ResourceLocation parentId : node.getLinks()) {
|
||||
SkillNode parent = SkillNodeRegistry.get(parentId);
|
||||
if (parent != null) {
|
||||
visit(parent, visited, visiting, issues);
|
||||
}
|
||||
}
|
||||
visiting.remove(id);
|
||||
visited.add(id);
|
||||
}
|
||||
|
||||
private static void visitAbility(AbilityNode ability,
|
||||
Set<ResourceLocation> visited,
|
||||
Set<ResourceLocation> visiting,
|
||||
List<String> issues) {
|
||||
ResourceLocation id = ability.getId();
|
||||
if (visited.contains(id)) return;
|
||||
if (!visiting.add(id)) {
|
||||
issues.add("Cycle detected at ability node " + id);
|
||||
return;
|
||||
}
|
||||
for (ResourceLocation parentId : ability.getLinks()) {
|
||||
AbilityNode parent = AbilityNodeRegistry.get(parentId);
|
||||
if (parent != null) {
|
||||
visitAbility(parent, visited, visiting, issues);
|
||||
}
|
||||
}
|
||||
visiting.remove(id);
|
||||
visited.add(id);
|
||||
}
|
||||
|
||||
private static Set<ResourceLocation> collectRequiredSkillNodes(SkillNode node) {
|
||||
Set<ResourceLocation> required = new HashSet<>();
|
||||
collectRequiredSkillNodes(node, required, new HashSet<>());
|
||||
required.remove(node.getId());
|
||||
return required;
|
||||
}
|
||||
|
||||
private static void collectRequiredSkillNodes(SkillNode node,
|
||||
Set<ResourceLocation> required,
|
||||
Set<ResourceLocation> visiting) {
|
||||
if (!visiting.add(node.getId())) return;
|
||||
for (ResourceLocation parentId : node.getLinks()) {
|
||||
SkillNode parent = SkillNodeRegistry.get(parentId);
|
||||
if (parent != null && required.add(parentId)) {
|
||||
collectRequiredSkillNodes(parent, required, visiting);
|
||||
}
|
||||
}
|
||||
visiting.remove(node.getId());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user