diff --git a/common/src/main/java/com/wynntils/core/consumers/features/FeatureManager.java b/common/src/main/java/com/wynntils/core/consumers/features/FeatureManager.java index 2ec1a40a01..dbbb393091 100644 --- a/common/src/main/java/com/wynntils/core/consumers/features/FeatureManager.java +++ b/common/src/main/java/com/wynntils/core/consumers/features/FeatureManager.java @@ -125,6 +125,7 @@ import com.wynntils.features.ui.BulkBuyFeature; import com.wynntils.features.ui.ContainerScrollFeature; import com.wynntils.features.ui.CosmeticsPreviewFeature; +import com.wynntils.features.ui.CustomAbilityTreeFeature; import com.wynntils.features.ui.CustomCharacterSelectionScreenFeature; import com.wynntils.features.ui.CustomLoadingScreenFeature; import com.wynntils.features.ui.CustomSeaskipperScreenFeature; @@ -329,6 +330,7 @@ public void init() { registerFeature(new BulkBuyFeature()); registerFeature(new ContainerScrollFeature()); registerFeature(new CosmeticsPreviewFeature()); + registerFeature(new CustomAbilityTreeFeature()); registerFeature(new CustomCharacterSelectionScreenFeature()); registerFeature(new CustomLoadingScreenFeature()); registerFeature(new CustomSeaskipperScreenFeature()); diff --git a/common/src/main/java/com/wynntils/features/debug/AbilityTreeDataDumpFeature.java b/common/src/main/java/com/wynntils/features/debug/AbilityTreeDataDumpFeature.java index 1c86c01538..50ff28e318 100644 --- a/common/src/main/java/com/wynntils/features/debug/AbilityTreeDataDumpFeature.java +++ b/common/src/main/java/com/wynntils/features/debug/AbilityTreeDataDumpFeature.java @@ -1,5 +1,5 @@ /* - * Copyright © Wynntils 2023. + * Copyright © Wynntils 2023-2024. * This file is released under LGPLv3. See LICENSE for full license details. */ package com.wynntils.features.debug; @@ -9,52 +9,38 @@ import com.wynntils.core.components.Managers; import com.wynntils.core.components.Models; import com.wynntils.core.consumers.features.Feature; +import com.wynntils.core.consumers.features.properties.RegisterKeyBind; import com.wynntils.core.consumers.features.properties.StartDisabled; +import com.wynntils.core.keybinds.KeyBind; import com.wynntils.core.persisted.config.Category; import com.wynntils.core.persisted.config.ConfigCategory; -import com.wynntils.mc.event.ContainerClickEvent; import com.wynntils.models.abilitytree.type.AbilityTreeInfo; -import com.wynntils.models.items.items.gui.AbilityTreeItem; -import com.wynntils.utils.mc.KeyboardUtils; import com.wynntils.utils.mc.McUtils; import java.io.File; import java.util.Locale; -import java.util.Optional; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.network.chat.Component; -import net.minecraftforge.eventbus.api.EventPriority; -import net.minecraftforge.eventbus.api.SubscribeEvent; +import org.lwjgl.glfw.GLFW; // NOTE: This feature was intented to be used on fully reset ability trees. -// Although support for parsing any tree is present, I would still recommend using a fresh tree to avoid any -// issues. +// Although support for parsing any tree is present, +// a fresh tree must be used for the best results. @StartDisabled @ConfigCategory(Category.DEBUG) public class AbilityTreeDataDumpFeature extends Feature { private static final File SAVE_FOLDER = WynntilsMod.getModStorageDir("debug"); - @SubscribeEvent(priority = EventPriority.HIGHEST) - public void onInventoryClick(ContainerClickEvent event) { - if (!(McUtils.mc().screen instanceof AbstractContainerScreen abstractContainerScreen)) return; - if (!KeyboardUtils.isShiftDown()) return; - - Optional abilityTreeItem = Models.Item.asWynnItem(event.getItemStack(), AbilityTreeItem.class); - - if (abilityTreeItem.isEmpty()) return; - - event.setCanceled(true); - McUtils.player().closeContainer(); - - // Wait for the container to close - Managers.TickScheduler.scheduleNextTick( - () -> Models.AbilityTree.ABILITY_TREE_CONTAINER_QUERIES.dumpAbilityTree(this::saveToDisk)); - } + @RegisterKeyBind + public KeyBind dumpAbilityTree = new KeyBind( + "Dump Ability Tree", + GLFW.GLFW_KEY_0, + true, + () -> Models.AbilityTree.ABILITY_TREE_CONTAINER_QUERIES.dumpAbilityTree(this::saveToDisk)); private void saveToDisk(AbilityTreeInfo abilityTreeInfo) { // Save the dump to a file JsonElement element = Managers.Json.GSON.toJsonTree(abilityTreeInfo); - String fileName = Models.Character.getClassType().getName().toLowerCase(Locale.ROOT) + "_ablities.json"; + String fileName = Models.Character.getClassType().getName().toLowerCase(Locale.ROOT) + "_abilities.json"; File jsonFile = new File(SAVE_FOLDER, fileName); Managers.Json.savePreciousJson(jsonFile, element.getAsJsonObject()); diff --git a/common/src/main/java/com/wynntils/features/ui/CustomAbilityTreeFeature.java b/common/src/main/java/com/wynntils/features/ui/CustomAbilityTreeFeature.java new file mode 100644 index 0000000000..f89a7e8bcf --- /dev/null +++ b/common/src/main/java/com/wynntils/features/ui/CustomAbilityTreeFeature.java @@ -0,0 +1,33 @@ +/* + * Copyright © Wynntils 2023-2024. + * This file is released under LGPLv3. See LICENSE for full license details. + */ +package com.wynntils.features.ui; + +import com.wynntils.core.components.Models; +import com.wynntils.core.consumers.features.Feature; +import com.wynntils.core.persisted.config.Category; +import com.wynntils.core.persisted.config.ConfigCategory; +import com.wynntils.mc.event.ContainerClickEvent; +import com.wynntils.models.items.items.gui.AbilityTreeItem; +import com.wynntils.utils.mc.KeyboardUtils; +import com.wynntils.utils.mc.McUtils; +import java.util.Optional; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +@ConfigCategory(Category.UI) +public class CustomAbilityTreeFeature extends Feature { + @SubscribeEvent + public void onInventoryClick(ContainerClickEvent event) { + if (!(McUtils.mc().screen instanceof AbstractContainerScreen)) return; + if (!KeyboardUtils.isShiftDown()) return; + + Optional abilityTreeItem = Models.Item.asWynnItem(event.getItemStack(), AbilityTreeItem.class); + if (abilityTreeItem.isEmpty()) return; + + event.setCanceled(true); + + Models.AbilityTree.openCustomAbilityTreeScreen(); + } +} diff --git a/common/src/main/java/com/wynntils/models/abilitytree/AbilityTreeContainerQueries.java b/common/src/main/java/com/wynntils/models/abilitytree/AbilityTreeContainerQueries.java index b97b1f8b97..704877eb8b 100644 --- a/common/src/main/java/com/wynntils/models/abilitytree/AbilityTreeContainerQueries.java +++ b/common/src/main/java/com/wynntils/models/abilitytree/AbilityTreeContainerQueries.java @@ -14,9 +14,9 @@ import com.wynntils.handlers.container.type.ContainerContent; import com.wynntils.models.abilitytree.parser.UnprocessedAbilityTreeInfo; import com.wynntils.models.abilitytree.type.AbilityTreeInfo; +import com.wynntils.models.abilitytree.type.AbilityTreeInstance; import com.wynntils.models.abilitytree.type.AbilityTreeNodeState; import com.wynntils.models.abilitytree.type.AbilityTreeSkillNode; -import com.wynntils.models.abilitytree.type.ParsedAbilityTree; import com.wynntils.models.containers.ContainerModel; import com.wynntils.utils.mc.McUtils; import com.wynntils.utils.type.Pair; @@ -40,19 +40,20 @@ public void dumpAbilityTree(Consumer supplier) { queryAbilityTree(new AbilityTreeContainerQueries.AbilityPageDumper(supplier)); } - public void updateParsedAbilityTree() { + public void parseAbilityTree(AbilityTreeInfo abilityTreeInfo) { McUtils.player().closeContainer(); // Wait for the container to close - Managers.TickScheduler.scheduleNextTick(() -> queryAbilityTree( - new AbilityTreeContainerQueries.AbilityPageSoftProcessor(Models.AbilityTree::setCurrentAbilityTree))); + Managers.TickScheduler.scheduleNextTick( + () -> queryAbilityTree(new AbilityTreeContainerQueries.AbilityPageSoftProcessor((abilityTreeInstance) -> + Models.AbilityTree.setAbilityTreeInstance(abilityTreeInfo, abilityTreeInstance)))); } private void queryAbilityTree(AbilityTreeProcessor processor) { ScriptedContainerQuery query = ScriptedContainerQuery.builder("Ability Tree Query") .onError(msg -> { WynntilsMod.warn("Problem querying Ability Tree: " + msg); - McUtils.sendErrorToClient("Dumping Ability Tree failed"); + Models.AbilityTree.setAbilityTreeInstance(null, null); }) // Open character/compass menu @@ -139,9 +140,9 @@ protected void processPage(ContainerContent content, int page) { */ private static class AbilityPageSoftProcessor extends AbilityTreeProcessor { private final Map collectedInfo = new LinkedHashMap<>(); - private final Consumer callback; + private final Consumer callback; - protected AbilityPageSoftProcessor(Consumer callback) { + protected AbilityPageSoftProcessor(Consumer callback) { this.callback = callback; } @@ -163,7 +164,7 @@ protected void processPage(ContainerContent content, int page) { boolean lastPage = page == Models.AbilityTree.ABILITY_TREE_PAGES; if (lastPage) { - callback.accept(new ParsedAbilityTree(ImmutableMap.copyOf(collectedInfo))); + callback.accept(new AbilityTreeInstance(ImmutableMap.copyOf(collectedInfo))); } } } diff --git a/common/src/main/java/com/wynntils/models/abilitytree/AbilityTreeModel.java b/common/src/main/java/com/wynntils/models/abilitytree/AbilityTreeModel.java index 25481af9db..0511c80cac 100644 --- a/common/src/main/java/com/wynntils/models/abilitytree/AbilityTreeModel.java +++ b/common/src/main/java/com/wynntils/models/abilitytree/AbilityTreeModel.java @@ -1,34 +1,44 @@ /* - * Copyright © Wynntils 2023. + * Copyright © Wynntils 2023-2024. * This file is released under LGPLv3. See LICENSE for full license details. */ package com.wynntils.models.abilitytree; import com.google.common.reflect.TypeToken; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; +import com.wynntils.core.WynntilsMod; import com.wynntils.core.components.Managers; import com.wynntils.core.components.Model; +import com.wynntils.core.components.Models; import com.wynntils.core.net.Download; import com.wynntils.core.net.UrlId; +import com.wynntils.mc.event.ScreenClosedEvent; import com.wynntils.models.abilitytree.parser.AbilityTreeParser; import com.wynntils.models.abilitytree.type.AbilityTreeInfo; -import com.wynntils.models.abilitytree.type.AbilityTreeNodeState; +import com.wynntils.models.abilitytree.type.AbilityTreeInstance; +import com.wynntils.models.abilitytree.type.AbilityTreeQueryState; import com.wynntils.models.abilitytree.type.AbilityTreeSkillNode; -import com.wynntils.models.abilitytree.type.ParsedAbilityTree; import com.wynntils.models.character.type.ClassType; +import com.wynntils.screens.abilities.CustomAbilityTreeScreen; +import com.wynntils.utils.mc.McUtils; import java.lang.reflect.Type; +import java.util.EnumMap; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import net.minecraftforge.eventbus.api.SubscribeEvent; public class AbilityTreeModel extends Model { public static final int ABILITY_TREE_PAGES = 7; public static final AbilityTreeParser ABILITY_TREE_PARSER = new AbilityTreeParser(); public static final AbilityTreeContainerQueries ABILITY_TREE_CONTAINER_QUERIES = new AbilityTreeContainerQueries(); - private Map abiliiyTreeMap = new HashMap<>(); - private ParsedAbilityTree currentAbilityTree; + // Ability Tree Infos sourced from our data (remote) + private Map abilityTreeMap = new EnumMap<>(ClassType.class); + + // Parsed Ability Tree Instance and State + private AbilityTreeInstance abilityTreeInstance = null; + private AbilityTreeQueryState abilityTreeQueryState = null; public AbilityTreeModel() { super(List.of()); @@ -41,35 +51,106 @@ public void reloadData() { Download dl = Managers.Net.download(UrlId.DATA_STATIC_ABILITIES); dl.handleReader(reader -> { Type type = new TypeToken>() {}.getType(); - Gson gson = new GsonBuilder().create(); - Map abilityMap = gson.fromJson(reader, type); + Map abilityMap = Managers.Json.GSON.fromJson(reader, type); - Map tempMap = new HashMap<>(); + Map tempMap = new EnumMap<>(ClassType.class); abilityMap.forEach((key, value) -> tempMap.put(ClassType.fromName(key), value)); - abiliiyTreeMap = tempMap; + abilityTreeMap = tempMap; }); } - public void setCurrentAbilityTree(ParsedAbilityTree currentAbilityTree) { - this.currentAbilityTree = currentAbilityTree; + @SubscribeEvent + public void onScreenClosed(ScreenClosedEvent event) { + if (!(event.getScreen() instanceof CustomAbilityTreeScreen)) return; + + abilityTreeInstance = null; + abilityTreeQueryState = null; } - public AbilityTreeNodeState getNodeState(AbilityTreeSkillNode node) { - if (currentAbilityTree == null) { - return AbilityTreeNodeState.LOCKED; + public void openCustomAbilityTreeScreen() { + if (abilityTreeInstance != null || abilityTreeQueryState != null) { + WynntilsMod.warn("Opened an ability tree screen while one was already open. This should not happen."); + // try continuing anyway + } + + abilityTreeInstance = null; + abilityTreeQueryState = null; + + // Check if the player has a class parsed + if (Models.Character.getClassType() == ClassType.NONE) { + setAbilityTreeQueryState(AbilityTreeQueryState.ERROR_CLASS_NOT_PARSED); + } + + // Check if we have the ability tree info for the class + if (Models.Character.hasCharacter() && !abilityTreeMap.containsKey(Models.Character.getClassType())) { + setAbilityTreeQueryState(AbilityTreeQueryState.ERROR_NO_CLASS_DATA); + } + + // Start parsing the ability tree instance, if there are no errors + if (abilityTreeQueryState == null) { + setAbilityTreeQueryState(AbilityTreeQueryState.PARSING); + ABILITY_TREE_CONTAINER_QUERIES.parseAbilityTree(abilityTreeMap.get(Models.Character.getClassType())); + } + + // Open the custom ability tree screen + McUtils.mc().setScreen(new CustomAbilityTreeScreen()); + } + + public void setAbilityTreeInstance(AbilityTreeInfo abilityTreeInfo, AbilityTreeInstance abilityTreeInstance) { + if (abilityTreeInfo == null || abilityTreeInstance == null) { + // We have an error, set the state to error + setAbilityTreeQueryState(AbilityTreeQueryState.ERROR_PARSING_INSTANCE); + return; + } + + // Validate that the ability tree instance matches the ability tree info + if (!validateAbilityTreeInstance(abilityTreeInfo, abilityTreeInstance)) { + // We have an error, set the state to error + setAbilityTreeQueryState(AbilityTreeQueryState.ERROR_API_INFO_OUTDATED); + return; } - return currentAbilityTree.nodes().keySet().stream() - .filter(n -> n.equals(node)) - .map(currentAbilityTree.nodes()::get) - .findFirst() - .orElse(AbilityTreeNodeState.LOCKED); + this.abilityTreeInstance = abilityTreeInstance; + setAbilityTreeQueryState(AbilityTreeQueryState.PARSED); } - public AbilityTreeInfo getAbilityTree(ClassType type) { - return abiliiyTreeMap.get(type); + public Optional getAbilityTreeInfo() { + return Optional.ofNullable(abilityTreeMap.get(Models.Character.getClassType())); + } + + public Optional getAbilityTreeInstance() { + return Optional.ofNullable(abilityTreeInstance); + } + + public AbilityTreeQueryState getAbilityTreeQueryState() { + return abilityTreeQueryState; + } + + private void setAbilityTreeQueryState(AbilityTreeQueryState abilityTreeQueryState) { + this.abilityTreeQueryState = abilityTreeQueryState; + + if (McUtils.mc().screen instanceof CustomAbilityTreeScreen abilityTreeScreen) { + abilityTreeScreen.onAbilityTreeQueryStateChanged(abilityTreeQueryState); + } + } + + private boolean validateAbilityTreeInstance( + AbilityTreeInfo abilityTreeInfo, AbilityTreeInstance abilityTreeInstance) { + // Firstly, check node counts + if (abilityTreeInfo.nodes().size() != abilityTreeInstance.nodes().size()) { + return false; + } + + // Secondly, check if all nodes can be found in the instance + for (AbilityTreeSkillNode node : abilityTreeInfo.nodes()) { + if (!abilityTreeInstance.nodes().containsKey(node)) { + return false; + } + } + + return true; } } diff --git a/common/src/main/java/com/wynntils/models/abilitytree/parser/AbilityTreeParser.java b/common/src/main/java/com/wynntils/models/abilitytree/parser/AbilityTreeParser.java index 92e3a6b157..8f9b0387ef 100644 --- a/common/src/main/java/com/wynntils/models/abilitytree/parser/AbilityTreeParser.java +++ b/common/src/main/java/com/wynntils/models/abilitytree/parser/AbilityTreeParser.java @@ -1,14 +1,16 @@ /* - * Copyright © Wynntils 2023. + * Copyright © Wynntils 2023-2024. * This file is released under LGPLv3. See LICENSE for full license details. */ package com.wynntils.models.abilitytree.parser; +import com.wynntils.core.WynntilsMod; import com.wynntils.core.text.PartStyle; import com.wynntils.core.text.StyledText; import com.wynntils.models.abilitytree.type.AbilityTreeLocation; import com.wynntils.models.abilitytree.type.AbilityTreeNodeState; import com.wynntils.models.abilitytree.type.AbilityTreeSkillNode; +import com.wynntils.models.abilitytree.type.ArchetypeInfo; import com.wynntils.models.abilitytree.type.ArchetypeRequirement; import com.wynntils.models.abilitytree.type.ItemInformation; import com.wynntils.utils.mc.LoreUtils; @@ -23,7 +25,8 @@ import net.minecraft.world.item.Items; public final class AbilityTreeParser { - private static final Pattern NODE_NAME_PATTERN = Pattern.compile("§.(Unlock )?§l(.+)(§r§. ability)?"); + // Node patterns + private static final Pattern NODE_NAME_PATTERN = Pattern.compile("§.(Unlock )?(?:§f)?§l(.+)((?:§.)? ability)?"); private static final Pattern NODE_POINT_COST_PATTERN = Pattern.compile("§.. §7Ability Points: §f(\\d+)"); private static final Pattern NODE_BLOCKS_ABILITY_PATTERN = Pattern.compile("§c- §7(.+)"); private static final Pattern NODE_REQUIRED_ABILITY_PATTERN = Pattern.compile("§.. §7Required Ability: §f(.+)"); @@ -35,13 +38,17 @@ public final class AbilityTreeParser { private static final Pattern NODE_REQUIREMENT_NOT_MET = Pattern.compile("§cYou do not meet the requirements"); private static final Pattern NODE_UNLOCKED = Pattern.compile("§eYou already unlocked this ability"); + // Connection patterns private static final StyledText CONNECTION_NAME = StyledText.fromString(" "); + // Archetype patterns + private static final Pattern ARCHETYPE_NAME_PATTERN = Pattern.compile("§.§l(.+) Archetype"); + public Pair parseNodeFromItem( ItemStack itemStack, int page, int slot, int id) { StyledText nameStyledText = StyledText.fromComponent(itemStack.getHoverName()); - AbilityTreeNodeState state = AbilityTreeNodeState.LOCKED; + AbilityTreeNodeState state = AbilityTreeNodeState.UNREACHABLE; StyledText actualName; if (nameStyledText.getPartCount() == 1) { actualName = nameStyledText; @@ -112,6 +119,12 @@ public Pair parseNodeFromItem( state = AbilityTreeNodeState.UNLOCKED; continue; } + + matcher = text.getMatcher(NODE_REQUIREMENT_NOT_MET); + if (matcher.matches()) { + state = AbilityTreeNodeState.REQUIREMENT_NOT_MET; + continue; + } } if (state == AbilityTreeNodeState.UNLOCKABLE || state == AbilityTreeNodeState.UNLOCKED) { @@ -128,20 +141,15 @@ public Pair parseNodeFromItem( // Skip final empty line includedLines = tempList.subList(0, tempList.size() - 1); - } else if (state == AbilityTreeNodeState.LOCKED) { + } else if (state == AbilityTreeNodeState.REQUIREMENT_NOT_MET) { // Skip empty line + "requirement not met" - if (includedLines - .get(includedLines.size() - 1) - .getMatcher(NODE_REQUIREMENT_NOT_MET) - .matches()) { - includedLines = includedLines.subList(0, includedLines.size() - 2); - } + includedLines = includedLines.subList(0, includedLines.size() - 2); } ItemInformation itemInformation = new ItemInformation( Item.getId(itemStack.getItem()), switch (state) { - case LOCKED -> itemStack.getDamageValue(); + case UNREACHABLE, REQUIREMENT_NOT_MET -> itemStack.getDamageValue(); case UNLOCKABLE -> itemStack.getDamageValue() - 1; case UNLOCKED -> itemStack.getDamageValue() - 2; case BLOCKED -> itemStack.getDamageValue() - 3; @@ -174,6 +182,36 @@ public Pair parseNodeFromItem( return Pair.of(node, state); } + public ArchetypeInfo parseArchetypeFromItem(ItemStack itemStack) { + StyledText nameStyledText = StyledText.fromComponent(itemStack.getHoverName()); + Matcher matcher = nameStyledText.getMatcher(ARCHETYPE_NAME_PATTERN); + + if (!matcher.matches()) { + // We should not get here, as we should only be calling this method on items that match the pattern + WynntilsMod.error("Failed to parse archetype name from item stack: " + itemStack.getHoverName()); + return null; + } + + String name = matcher.group(1); + + List loreStyledText = LoreUtils.getLore(itemStack); + + List description = new ArrayList<>(); + for (StyledText text : loreStyledText) { + description.add(text.getString()); + } + + // Remove the last two lines of the lore, which are the archetype count and the empty line + description.remove(description.size() - 1); + description.remove(description.size() - 1); + + return new ArchetypeInfo( + name, + nameStyledText.getString(), + description, + new ItemInformation(Item.getId(itemStack.getItem()), itemStack.getDamageValue())); + } + public boolean isNodeItem(ItemStack itemStack, int slot) { StyledText nameStyledText = StyledText.fromComponent(itemStack.getHoverName()); return itemStack.getItem() == Items.STONE_AXE @@ -185,4 +223,11 @@ public boolean isConnectionItem(ItemStack itemStack) { return itemStack.getItem() == Items.STONE_AXE && StyledText.fromComponent(itemStack.getHoverName()).equals(CONNECTION_NAME); } + + public boolean isArchetypeItem(ItemStack itemStack) { + return itemStack.getItem() == Items.STONE_AXE + && StyledText.fromComponent(itemStack.getHoverName()) + .getMatcher(ARCHETYPE_NAME_PATTERN) + .matches(); + } } diff --git a/common/src/main/java/com/wynntils/models/abilitytree/parser/UnprocessedAbilityTreeInfo.java b/common/src/main/java/com/wynntils/models/abilitytree/parser/UnprocessedAbilityTreeInfo.java index 0fc17431bc..6bc6e226f4 100644 --- a/common/src/main/java/com/wynntils/models/abilitytree/parser/UnprocessedAbilityTreeInfo.java +++ b/common/src/main/java/com/wynntils/models/abilitytree/parser/UnprocessedAbilityTreeInfo.java @@ -1,5 +1,5 @@ /* - * Copyright © Wynntils 2023. + * Copyright © Wynntils 2023-2024. * This file is released under LGPLv3. See LICENSE for full license details. */ package com.wynntils.models.abilitytree.parser; @@ -9,10 +9,12 @@ import com.wynntils.models.abilitytree.type.AbilityTreeInfo; import com.wynntils.models.abilitytree.type.AbilityTreeLocation; import com.wynntils.models.abilitytree.type.AbilityTreeSkillNode; +import com.wynntils.models.abilitytree.type.ArchetypeInfo; import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -24,6 +26,7 @@ public class UnprocessedAbilityTreeInfo { private final Map connectionMap = new HashMap<>(); private final Map nodeMap = new HashMap<>(); + private final LinkedHashMap archetypeMap = new LinkedHashMap<>(); private boolean processed = false; private void addNodeFromItem(ItemStack itemStack, int page, int slot) { @@ -41,6 +44,11 @@ private void addConnectionFromItem(ItemStack itemStack, int page, int slot) { AbilityTreeConnectionType.fromDamage(itemStack.getDamageValue())); } + private void addArchetypeFromItem(ItemStack itemStack) { + ArchetypeInfo archetype = Models.AbilityTree.ABILITY_TREE_PARSER.parseArchetypeFromItem(itemStack); + archetypeMap.put(archetype.name(), archetype); + } + public void processItem(ItemStack itemStack, int page, int slot, boolean processConnections) { if (Models.AbilityTree.ABILITY_TREE_PARSER.isNodeItem(itemStack, slot)) { addNodeFromItem(itemStack, page, slot); @@ -49,6 +57,12 @@ public void processItem(ItemStack itemStack, int page, int slot, boolean process if (processConnections && Models.AbilityTree.ABILITY_TREE_PARSER.isConnectionItem(itemStack)) { addConnectionFromItem(itemStack, page, slot); + return; + } + + if (Models.AbilityTree.ABILITY_TREE_PARSER.isArchetypeItem(itemStack)) { + addArchetypeFromItem(itemStack); + return; } } @@ -206,6 +220,6 @@ public AbilityTreeInfo getProcesssed() { processed = true; } - return new AbilityTreeInfo(nodes); + return new AbilityTreeInfo(nodes, archetypeMap); } } diff --git a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeConnectionNode.java b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeConnectionNode.java new file mode 100644 index 0000000000..9c6e7a60c3 --- /dev/null +++ b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeConnectionNode.java @@ -0,0 +1,124 @@ +/* + * Copyright © Wynntils 2023-2024. + * This file is released under LGPLv3. See LICENSE for full license details. + */ +package com.wynntils.models.abilitytree.type; + +import com.wynntils.core.components.Models; +import com.wynntils.utils.type.Pair; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; +import net.minecraft.world.item.ItemStack; + +public class AbilityTreeConnectionNode { + private AbilityTreeConnectionType connectionType; + + // {up, down, left, right} + // There can be multiple suppliers from a side + private Set[] nodes; + private Set> nodePairs; + private boolean[] connections = new boolean[4]; + + // This constuctor is used for new connections, so there are only 2 non-null nodes + public AbilityTreeConnectionNode(AbilityTreeConnectionType connectionType, AbilityTreeSkillNode[] nodes) { + this.connectionType = connectionType; + this.nodes = Arrays.stream(nodes).map(Collections::singleton).toArray(Set[]::new); + + List nonNullNodes = Arrays.stream(this.nodes) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .toList(); + this.nodePairs = Collections.singleton(new Pair<>(nonNullNodes.get(0), nonNullNodes.get(1))); + + updateConnectedStates(); + } + + // This node is used for merging connections, so there can be multiple nodes in each direction + private AbilityTreeConnectionNode( + AbilityTreeConnectionType connectionType, + Set[] nodes, + Set> nodePairs) { + this.connectionType = connectionType; + this.nodes = nodes; + this.nodePairs = nodePairs; + + updateConnectedStates(); + } + + private void updateConnectedStates() { + Optional treeOptional = Models.AbilityTree.getAbilityTreeInstance(); + + if (treeOptional.isEmpty()) return; + + AbilityTreeInstance abilityTreeInstance = treeOptional.get(); + + List activeNodes = nodePairs.stream() + .filter(pair -> abilityTreeInstance.getNodeState(pair.a()) == AbilityTreeNodeState.UNLOCKED + && abilityTreeInstance.getNodeState(pair.b()) == AbilityTreeNodeState.UNLOCKED) + .flatMap(pair -> Stream.of(pair.a(), pair.b())) + .toList(); + + for (int i = 0; i < nodes.length; i++) { + connections[i] = false; + + for (AbilityTreeSkillNode nodeInDirection : nodes[i]) { + if (activeNodes.contains(nodeInDirection)) { + connections[i] = true; + break; + } + } + } + } + + public ItemStack getItemStack() { + // FIXME: Try to do this less times + updateConnectedStates(); + return connectionType.getItemStack(connections); + } + + public AbilityTreeConnectionType getConnectionType() { + return connectionType; + } + + public AbilityTreeConnectionNode merge(AbilityTreeConnectionNode other) { + if (other == null) { + return this; + } + + AbilityTreeConnectionType newConnectionType; + + if (this.getConnectionType() == other.getConnectionType()) { + // Connection type is the same, we only need to merge the nodes + newConnectionType = connectionType; + } else { + // Connection type is different, we need to merge the nodes and the connection type + newConnectionType = AbilityTreeConnectionType.merge(connectionType, other.getConnectionType()); + } + + Set[] newNodes = new HashSet[4]; + + for (int i = 0; i < nodes.length; i++) { + Set nodes = this.nodes[i]; + newNodes[i] = new HashSet<>(nodes); + } + + Set[] sets = other.nodes; + for (int i = 0; i < sets.length; i++) { + Set nodes = sets[i]; + newNodes[i].addAll(nodes); + } + + Set> newPairs = new HashSet<>(this.nodePairs); + newPairs.addAll(other.nodePairs); + + // Return a merged instance + return new AbilityTreeConnectionNode(newConnectionType, newNodes, newPairs); + } +} diff --git a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeConnectionType.java b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeConnectionType.java index 7da3d57a51..fd40f9b482 100644 --- a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeConnectionType.java +++ b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeConnectionType.java @@ -1,5 +1,5 @@ /* - * Copyright © Wynntils 2023. + * Copyright © Wynntils 2023-2024. * This file is released under LGPLv3. See LICENSE for full license details. */ package com.wynntils.models.abilitytree.type; @@ -72,9 +72,9 @@ public enum AbilityTreeConnectionType { new boolean[] {true, false, true, false}, 22), new boolean[] {true, true, true, false}, List.of( - Pair.of(VERTICAL, DOWN_RIGHT_TURN), + Pair.of(VERTICAL, DOWN_LEFT_TURN), Pair.of(VERTICAL, UP_RIGHT_TURN), - Pair.of(DOWN_LEFT_TURN, UP_LEFT_TURN))), + Pair.of(DOWN_LEFT_TURN, UP_RIGHT_TURN))), THREE_WAY_DOWN( 23, Map.of( @@ -96,9 +96,9 @@ public enum AbilityTreeConnectionType { new boolean[] {true, false, true, false}, 32), new boolean[] {true, false, true, true}, List.of( - Pair.of(VERTICAL, DOWN_LEFT_TURN), + Pair.of(VERTICAL, DOWN_RIGHT_TURN), Pair.of(VERTICAL, UP_LEFT_TURN), - Pair.of(DOWN_RIGHT_TURN, UP_RIGHT_TURN))), + Pair.of(DOWN_RIGHT_TURN, UP_LEFT_TURN))), FOUR_WAY( 1, diff --git a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeInfo.java b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeInfo.java index 01eb9cf940..174cd95fcb 100644 --- a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeInfo.java +++ b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeInfo.java @@ -1,12 +1,17 @@ /* - * Copyright © Wynntils 2023. + * Copyright © Wynntils 2023-2024. * This file is released under LGPLv3. See LICENSE for full license details. */ package com.wynntils.models.abilitytree.type; +import java.util.LinkedHashMap; import java.util.List; /** * This class contains all relevant info to a specific class' ability tree. */ -public record AbilityTreeInfo(List nodes) {} + +// The archetypeInfoMap is a LinkedHashMap because the order of the archetypes is important +@SuppressWarnings("CollectionDeclaredAsConcreteClass") +public record AbilityTreeInfo( + List nodes, LinkedHashMap archetypeInfoMap) {} diff --git a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeInstance.java b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeInstance.java new file mode 100644 index 0000000000..09812d5deb --- /dev/null +++ b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeInstance.java @@ -0,0 +1,20 @@ +/* + * Copyright © Wynntils 2023-2024. + * This file is released under LGPLv3. See LICENSE for full license details. + */ +package com.wynntils.models.abilitytree.type; + +import com.google.common.collect.ImmutableMap; + +/** + * This class represents the current ability tree, where all nodes have a state. + */ +public record AbilityTreeInstance(ImmutableMap nodes) { + public AbilityTreeNodeState getNodeState(AbilityTreeSkillNode node) { + return nodes().keySet().stream() + .filter(n -> n.equals(node)) + .map(nodes()::get) + .findFirst() + .orElse(AbilityTreeNodeState.UNREACHABLE); + } +} diff --git a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeLocation.java b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeLocation.java index 7731ecbabc..783b2dfdeb 100644 --- a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeLocation.java +++ b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeLocation.java @@ -1,5 +1,5 @@ /* - * Copyright © Wynntils 2023. + * Copyright © Wynntils 2023-2024. * This file is released under LGPLv3. See LICENSE for full license details. */ package com.wynntils.models.abilitytree.type; @@ -14,8 +14,8 @@ * @param col The column of the ability tree. Index starts at 0. */ public record AbilityTreeLocation(int page, int row, int col) implements Comparable { - private static final int MAX_ROWS = 6; - private static final int MAX_COLS = 9; + public static final int MAX_ROWS = 6; + public static final int MAX_COLS = 9; public static AbilityTreeLocation fromSlot(int slot, int page) { int row = slot / MAX_COLS; diff --git a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeNodeState.java b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeNodeState.java index 5ee543c029..95a25329a8 100644 --- a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeNodeState.java +++ b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeNodeState.java @@ -1,12 +1,32 @@ /* - * Copyright © Wynntils 2023. + * Copyright © Wynntils 2023-2024. * This file is released under LGPLv3. See LICENSE for full license details. */ package com.wynntils.models.abilitytree.type; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + public enum AbilityTreeNodeState { - LOCKED, - UNLOCKABLE, - UNLOCKED, - BLOCKED + UNREACHABLE(Component.translatable("screens.wynntils.abilityTree.nodeState.unreachable") + .withStyle(ChatFormatting.RED)), + REQUIREMENT_NOT_MET(Component.translatable("screens.wynntils.abilityTree.nodeState.requirementNotMet") + .withStyle(ChatFormatting.RED)), + UNLOCKABLE(Component.translatable("screens.wynntils.abilityTree.nodeState.unlockable") + .withStyle(ChatFormatting.GREEN)), + UNLOCKED(Component.translatable("screens.wynntils.abilityTree.nodeState.unlocked") + .withStyle(ChatFormatting.YELLOW)), + BLOCKED(Component.translatable("screens.wynntils.abilityTree.nodeState.blocked") + .withStyle(ChatFormatting.DARK_RED)); + + private final MutableComponent component; + + AbilityTreeNodeState(MutableComponent component) { + this.component = component; + } + + public MutableComponent getComponent() { + return component; + } } diff --git a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeQueryState.java b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeQueryState.java new file mode 100644 index 0000000000..7d3b415afe --- /dev/null +++ b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeQueryState.java @@ -0,0 +1,43 @@ +/* + * Copyright © Wynntils 2024. + * This file is released under LGPLv3. See LICENSE for full license details. + */ +package com.wynntils.models.abilitytree.type; + +import java.util.function.Supplier; +import net.minecraft.network.chat.Component; + +public enum AbilityTreeQueryState { + // Actively doing a query + PARSING(Component.translatable("screens.wynntils.abilityTree.parsing")), + + // Successful query + PARSED(() -> null), + + // Errors occurred during parsing + ERROR_CLASS_NOT_PARSED(Component.translatable("screens.wynntils.abilityTree.errorClassNotParsed")), + ERROR_NO_CLASS_DATA(Component.translatable("screens.wynntils.abilityTree.errorNoClassData")), + ERROR_PARSING_INSTANCE(Component.translatable("screens.wynntils.abilityTree.errorParsingInstance")), + ERROR_API_INFO_OUTDATED(Component.translatable("screens.wynntils.abilityTree.apiInfoOutdated")); + + private final Supplier componentSupplier; + + AbilityTreeQueryState(Component component) { + this.componentSupplier = () -> component; + } + + AbilityTreeQueryState(Supplier componentSupplier) { + this.componentSupplier = componentSupplier; + } + + public Component getComponent() { + return componentSupplier.get(); + } + + /** + * @return true if the query is in a state where the screen can accept user input + */ + public boolean isReady() { + return this == PARSED; + } +} diff --git a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeSkillNode.java b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeSkillNode.java index f8fd30e0ac..8ea529592c 100644 --- a/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeSkillNode.java +++ b/common/src/main/java/com/wynntils/models/abilitytree/type/AbilityTreeSkillNode.java @@ -1,11 +1,15 @@ /* - * Copyright © Wynntils 2023. + * Copyright © Wynntils 2023-2024. * This file is released under LGPLv3. See LICENSE for full license details. */ package com.wynntils.models.abilitytree.type; +import com.wynntils.core.text.StyledText; +import java.util.ArrayList; import java.util.List; import java.util.Objects; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; public record AbilityTreeSkillNode( int id, @@ -20,23 +24,54 @@ public record AbilityTreeSkillNode( String archetype, AbilityTreeLocation location, List connections) { + public List getDescription(AbilityTreeNodeState state, AbilityTreeInstance abilityTree) { + List description = new ArrayList<>(); + + description.add(Component.literal(formattedName)); + + description.addAll(description().stream() + .map(StyledText::fromString) + .map(StyledText::getComponent) + .toList()); + + description.add(Component.empty()); + + if (state == AbilityTreeNodeState.BLOCKED) { + List blockedByNodes = abilityTree.nodes().keySet().stream() + .filter(node -> abilityTree.getNodeState(node) == AbilityTreeNodeState.UNLOCKED) + .filter(node -> node.blocks().contains(this.name())) + .toList(); + + description.add(Component.translatable("screens.wynntils.abilityTree.nodeState.blockedBy") + .withStyle(ChatFormatting.RED) + .withStyle(ChatFormatting.BOLD)); + + description.addAll(blockedByNodes.stream() + .map(AbilityTreeSkillNode::name) + .map(nodeName -> Component.literal("- ") + .withStyle(ChatFormatting.RED) + .append(Component.literal(nodeName))) + .toList()); + + description.add(Component.empty()); + } + + description.add(state.getComponent()); + + return description; + } + @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AbilityTreeSkillNode that = (AbilityTreeSkillNode) o; - return id == that.id - && cost == that.cost - && Objects.equals(formattedName, that.formattedName) - && Objects.equals(requiredAbility, that.requiredAbility) - && Objects.equals(requiredArchetype, that.requiredArchetype) - && Objects.equals(archetype, that.archetype) - && Objects.equals(location, that.location); + return id == that.id && Objects.equals(name, that.name) && Objects.equals(location, that.location); } @Override public int hashCode() { - return Objects.hash(id, formattedName, cost, requiredAbility, requiredArchetype, archetype, location); + return Objects.hash(id, name, location); } } diff --git a/common/src/main/java/com/wynntils/models/abilitytree/type/ArchetypeInfo.java b/common/src/main/java/com/wynntils/models/abilitytree/type/ArchetypeInfo.java new file mode 100644 index 0000000000..38934f4546 --- /dev/null +++ b/common/src/main/java/com/wynntils/models/abilitytree/type/ArchetypeInfo.java @@ -0,0 +1,46 @@ +/* + * Copyright © Wynntils 2024. + * This file is released under LGPLv3. See LICENSE for full license details. + */ +package com.wynntils.models.abilitytree.type; + +import java.util.List; +import java.util.stream.Stream; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; + +public record ArchetypeInfo( + String name, String formattedName, List description, ItemInformation itemInformation) { + public List getTooltip(AbilityTreeInstance abilityTree) { + // The tooltip consists of the name and description + int archetypeCount = (int) abilityTree.nodes().keySet().stream() + .filter(node -> node.archetype() != null) + .filter(node -> node.archetype().equalsIgnoreCase(name)) + .count(); + + int unlockedCount = (int) abilityTree.nodes().keySet().stream() + .filter(node -> node.archetype() != null) + .filter(node -> node.archetype().equalsIgnoreCase(name)) + .filter(node -> abilityTree.getNodeState(node) == AbilityTreeNodeState.UNLOCKED) + .count(); + + return Stream.concat( + Stream.concat( + Stream.of(Component.literal(formattedName)), + description.stream().map(Component::literal).map(c -> (Component) c)), + Stream.of( + Component.empty(), + Component.literal("✔ ") + .withStyle(ChatFormatting.GREEN) + .append(Component.translatable( + "screens.wynntils.abilityTree.archetype.unlockedAbilities") + .withStyle(ChatFormatting.GRAY) + .append(Component.literal(": ")) + .append(Component.literal(String.valueOf(unlockedCount)) + .withStyle(ChatFormatting.WHITE)) + .append(Component.literal("/")) + .append(Component.literal(String.valueOf(archetypeCount)) + .withStyle(ChatFormatting.GRAY))))) + .toList(); + } +} diff --git a/common/src/main/java/com/wynntils/models/abilitytree/type/ParsedAbilityTree.java b/common/src/main/java/com/wynntils/models/abilitytree/type/ParsedAbilityTree.java deleted file mode 100644 index 4881d6db44..0000000000 --- a/common/src/main/java/com/wynntils/models/abilitytree/type/ParsedAbilityTree.java +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright © Wynntils 2023. - * This file is released under LGPLv3. See LICENSE for full license details. - */ -package com.wynntils.models.abilitytree.type; - -import com.google.common.collect.ImmutableMap; - -/** - * This class represents the current ability tree, where all nodes have a state. - */ -public record ParsedAbilityTree(ImmutableMap nodes) {} diff --git a/common/src/main/java/com/wynntils/screens/abilities/CustomAbilityTreeScreen.java b/common/src/main/java/com/wynntils/screens/abilities/CustomAbilityTreeScreen.java new file mode 100644 index 0000000000..4dd5f38011 --- /dev/null +++ b/common/src/main/java/com/wynntils/screens/abilities/CustomAbilityTreeScreen.java @@ -0,0 +1,592 @@ +/* + * Copyright © Wynntils 2023-2024. + * This file is released under LGPLv3. See LICENSE for full license details. + */ +package com.wynntils.screens.abilities; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.wynntils.core.WynntilsMod; +import com.wynntils.core.components.Models; +import com.wynntils.core.consumers.screens.WynntilsScreen; +import com.wynntils.core.text.StyledText; +import com.wynntils.models.abilitytree.type.AbilityTreeConnectionNode; +import com.wynntils.models.abilitytree.type.AbilityTreeConnectionType; +import com.wynntils.models.abilitytree.type.AbilityTreeInfo; +import com.wynntils.models.abilitytree.type.AbilityTreeInstance; +import com.wynntils.models.abilitytree.type.AbilityTreeLocation; +import com.wynntils.models.abilitytree.type.AbilityTreeQueryState; +import com.wynntils.models.abilitytree.type.AbilityTreeSkillNode; +import com.wynntils.models.abilitytree.type.ArchetypeInfo; +import com.wynntils.screens.abilities.widgets.AbilityArchetypeWidget; +import com.wynntils.screens.abilities.widgets.AbilityNodeConnectionWidget; +import com.wynntils.screens.abilities.widgets.AbilityNodeWidget; +import com.wynntils.screens.abilities.widgets.AbilityTreePageSelectorButton; +import com.wynntils.screens.base.TooltipProvider; +import com.wynntils.utils.MathUtils; +import com.wynntils.utils.colors.CommonColors; +import com.wynntils.utils.render.FontRenderer; +import com.wynntils.utils.render.RenderUtils; +import com.wynntils.utils.render.Texture; +import com.wynntils.utils.render.type.HorizontalAlignment; +import com.wynntils.utils.render.type.TextShadow; +import com.wynntils.utils.render.type.VerticalAlignment; +import com.wynntils.utils.type.Pair; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.network.chat.Component; + +public class CustomAbilityTreeScreen extends WynntilsScreen { + private static final int NODE_AREA_OFFSET_X = 81; + private static final int NODE_AREA_OFFSET_Y = 22; + + private static final int NODE_AREA_WIDTH = 156; + private static final int NODE_AREA_HEIGHT = 98; + + private static final int UP_ARROW_X = 250; + private static final int UP_ARROW_Y = 117; + + private static final int DOWN_ARROW_X = 278; + private static final int DOWN_ARROW_Y = 117; + + private final AbilityTreeInfo abilityTreeInfo; + + private final List nodeWidgets = new ArrayList<>(); + private final Map connectionWidgets = new LinkedHashMap(); + + private final List archetypeWidgets = new ArrayList<>(); + + private float currentScrollPercentage; + + public CustomAbilityTreeScreen() { + super(Component.literal("Ability Tree")); + + abilityTreeInfo = Models.AbilityTree.getAbilityTreeInfo().orElse(null); + + setCurrentScrollPercentage(0); + } + + // region Init + + @Override + protected void doInit() { + this.addRenderableWidget(new AbilityTreePageSelectorButton( + UP_ARROW_X, + UP_ARROW_Y, + Texture.ABILITY_TREE_UP_ARROW.width(), + Texture.ABILITY_TREE_UP_ARROW.height(), + this, + true)); + + this.addRenderableWidget(new AbilityTreePageSelectorButton( + DOWN_ARROW_X, + DOWN_ARROW_Y, + Texture.ABILITY_TREE_DOWN_ARROW.width(), + Texture.ABILITY_TREE_DOWN_ARROW.height(), + this, + false)); + } + + // endregion + + // region Rendering + + @Override + public void doRender(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + PoseStack poseStack = guiGraphics.pose(); + + renderBackground(guiGraphics, mouseX, mouseY, partialTick); + + poseStack.pushPose(); + // Make the drawing origin the start of the texture, centered on the screen + poseStack.translate( + (this.width - Texture.ABILITY_TREE_BACKGROUND.width()) / 2, + (this.height - Texture.ABILITY_TREE_BACKGROUND.height()) / 2, + 0); + + final int backgroundWidth = Texture.ABILITY_TREE_BACKGROUND.width(); + final int backgroundHeight = Texture.ABILITY_TREE_BACKGROUND.height(); + RenderUtils.drawScalingTexturedRect( + poseStack, + Texture.ABILITY_TREE_BACKGROUND.resource(), + 0, + 0, + 0, + backgroundWidth, + backgroundHeight, + backgroundWidth, + backgroundHeight); + + if (Models.AbilityTree.getAbilityTreeQueryState().isReady()) { + renderNodes(guiGraphics, mouseX, mouseY, partialTick); + renderWidgets(guiGraphics, mouseX, mouseY, partialTick); + + // Render tooltips + renderTooltip(guiGraphics, mouseX, mouseY); + } else { + FontRenderer.getInstance() + .renderAlignedTextInBox( + poseStack, + StyledText.fromComponent(Models.AbilityTree.getAbilityTreeQueryState() + .getComponent()), + 0, + backgroundWidth, + 0, + backgroundHeight, + 0, + CommonColors.WHITE, + HorizontalAlignment.CENTER, + VerticalAlignment.MIDDLE, + TextShadow.OUTLINE, + 1f); + } + + poseStack.popPose(); + } + + private void renderTooltip(GuiGraphics guiGraphics, int mouseX, int mouseY) { + // Translate the mouse position to match what is rendered on the screen + int scaledMouseX = mouseX - (this.width - Texture.ABILITY_TREE_BACKGROUND.width()) / 2; + int scaledMouseY = mouseY - (this.height - Texture.ABILITY_TREE_BACKGROUND.height()) / 2; + + // For the node widgets, we need to check if the mouse is over the node, + // with the scroll offset applied + int nodeWidgetMouseX = scaledMouseX - NODE_AREA_OFFSET_X; + int nodeWidgetMouseY = scaledMouseY - NODE_AREA_OFFSET_Y; + + // Only show tooltips if the mouse is within the node area + if (nodeWidgetMouseX >= 0 + && nodeWidgetMouseX <= NODE_AREA_WIDTH + && nodeWidgetMouseY >= 0 + && nodeWidgetMouseY <= NODE_AREA_HEIGHT) { + nodeWidgetMouseY += NODE_AREA_HEIGHT * currentScrollPercentage; + + for (AbilityNodeWidget nodeWidget : nodeWidgets) { + if (nodeWidget.isMouseOver(nodeWidgetMouseX, nodeWidgetMouseY)) { + List tooltipLines = nodeWidget.getTooltipLines(); + guiGraphics.renderTooltip( + FontRenderer.getInstance().getFont(), + tooltipLines, + Optional.empty(), + scaledMouseX, + scaledMouseY); + return; + } + } + } + + for (AbilityArchetypeWidget archetypeWidget : archetypeWidgets) { + if (archetypeWidget.isMouseOver(scaledMouseX, scaledMouseY)) { + List tooltipLines = archetypeWidget.getTooltipLines(); + guiGraphics.renderTooltip( + FontRenderer.getInstance().getFont(), + tooltipLines, + Optional.empty(), + scaledMouseX, + scaledMouseY); + return; + } + } + + for (GuiEventListener child : this.children()) { + if (child instanceof TooltipProvider tooltipProvider) { + if (child.isMouseOver(scaledMouseX, scaledMouseY)) { + List tooltipLines = tooltipProvider.getTooltipLines(); + guiGraphics.renderTooltip( + FontRenderer.getInstance().getFont(), + tooltipLines, + Optional.empty(), + scaledMouseX, + scaledMouseY); + return; + } + } + } + } + + private void renderNodes(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + PoseStack poseStack = guiGraphics.pose(); + + poseStack.pushPose(); + + // Set the node area offset + poseStack.translate(NODE_AREA_OFFSET_X, NODE_AREA_OFFSET_Y, 0); + + // Make this area a mask, so we only render the nodes within this area + // Make sure it's a bit larger than the actual area, so we don't cut off the nodes + RenderUtils.createRectMask(poseStack, -5, -5, NODE_AREA_WIDTH + 10, NODE_AREA_HEIGHT + 5); + + // Translate the nodes based on the current scroll percentage + poseStack.translate(0, -NODE_AREA_HEIGHT * currentScrollPercentage, 0); + + nodeWidgets.forEach(widget -> widget.render(guiGraphics, mouseX, mouseY, partialTick)); + connectionWidgets.values().forEach(widget -> widget.render(guiGraphics, mouseX, mouseY, partialTick)); + + RenderUtils.clearMask(); + + poseStack.popPose(); + } + + private void renderWidgets(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + PoseStack poseStack = guiGraphics.pose(); + + poseStack.pushPose(); + + archetypeWidgets.forEach(widget -> widget.render(guiGraphics, mouseX, mouseY, partialTick)); + renderables.forEach(widget -> widget.render(guiGraphics, mouseX, mouseY, partialTick)); + + poseStack.popPose(); + } + + // endregion + + // region Mouse Handlers + + @Override + public boolean doMouseClicked(double mouseX, double mouseY, int button) { + if (!Models.AbilityTree.getAbilityTreeQueryState().isReady()) return false; + + // Translate the mouse position to match what is rendered on the screen + double scaledMouseX = mouseX - (this.width - Texture.ABILITY_TREE_BACKGROUND.width()) / 2; + double scaledMouseY = mouseY - (this.height - Texture.ABILITY_TREE_BACKGROUND.height()) / 2; + + for (GuiEventListener child : this.children()) { + if (child.isMouseOver(scaledMouseX, scaledMouseY)) { + if (child.mouseClicked(scaledMouseX, scaledMouseY, button)) { + return true; + } + } + } + + return false; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) { + if (!Models.AbilityTree.getAbilityTreeQueryState().isReady()) return false; + + setCurrentScrollPercentage((float) (currentScrollPercentage - scrollY)); + return true; + } + + // endregion + + // region Page Management + + public void setCurrentScrollPercentage(float page) { + currentScrollPercentage = MathUtils.clamp(page, 0, Models.AbilityTree.ABILITY_TREE_PAGES - 1); + } + + public float getCurrentScrollPercentage() { + return currentScrollPercentage; + } + + // endregion + + // region Events from Model + + public void onAbilityTreeQueryStateChanged(AbilityTreeQueryState newState) { + switch (newState) { + case PARSED: + reconstructWidgets(Models.AbilityTree.getAbilityTreeInstance() + .orElseThrow(() -> new IllegalStateException( + "No ability tree instance found, but the query state got set to PARSED."))); + break; + default: + break; + } + } + + // endregion + + // region Node Widget Building + + private void reconstructWidgets(AbilityTreeInstance abilityTreeInstance) { + nodeWidgets.clear(); + connectionWidgets.clear(); + + // Add nodes and connections for every page, + // we calculate what's rendered based on the current scroll percentage + // at render time + for (int page = 1; page <= Models.AbilityTree.ABILITY_TREE_PAGES; page++) { + int currentPage = page; + + int currentPageYRenderOffset = NODE_AREA_HEIGHT * (currentPage - 1); + + List currentPageNodeWidgets = new ArrayList<>(); + + abilityTreeInfo.nodes().stream() + .filter(node -> node.location().page() == currentPage) + .forEach(node -> { + Pair renderLocation = getRenderLocation(node.location()); + + AbilityNodeWidget nodeWidget = new AbilityNodeWidget( + renderLocation.a() - AbilityNodeWidget.SIZE / 2, + renderLocation.b() - AbilityNodeWidget.SIZE / 2 + currentPageYRenderOffset, + AbilityNodeWidget.SIZE, + AbilityNodeWidget.SIZE, + abilityTreeInstance, + node); + currentPageNodeWidgets.add(nodeWidget); + nodeWidgets.add(nodeWidget); + }); + + // We do this "backwards": + // First find connection nodes from last page + List multiPageConnectionNodesFromLastPage = abilityTreeInfo.nodes().stream() + .filter(node -> node.location().page() == currentPage - 1) + .filter(node -> currentPageNodeWidgets.stream() + .map(AbilityNodeWidget::getNode) + .map(AbilityTreeSkillNode::id) + .anyMatch(node.connections()::contains)) + .toList(); + + // Then, find the nodes they connect to on this page + for (AbilityTreeSkillNode connectionNode : multiPageConnectionNodesFromLastPage) { + List multiPageConnections = currentPageNodeWidgets.stream() + .map(AbilityNodeWidget::getNode) + .filter(node -> connectionNode.connections().contains(node.id())) + .toList(); + + for (AbilityTreeSkillNode currentNode : multiPageConnections) { + final int col = currentNode.location().col(); + final int row = currentNode.location().row(); + + // Multi page connections are basically the same as vertical connections, + // when the receiving node is the one rendered. But this has to be handled first. + addConnectionsVertically(currentNode, connectionNode, col, 0, row, currentPageYRenderOffset); + } + } + + for (AbilityNodeWidget nodeWidget : currentPageNodeWidgets) { + final AbilityTreeSkillNode currentNode = nodeWidget.getNode(); + final int col = currentNode.location().col(); + final int row = currentNode.location().row(); + + for (Integer connection : currentNode.connections()) { + Optional connectionOptional = abilityTreeInfo.nodes().stream() + .filter(node -> node.id() == connection) + .findFirst(); + + if (connectionOptional.isEmpty()) { + WynntilsMod.warn("Unable to find connection node for " + connection); + continue; + } + + AbilityTreeSkillNode connectionNode = connectionOptional.get(); + + final int connectionCol = connectionNode.location().col(); + final int connectionRow = connectionNode.location().row(); + + // Only horizontal connections are needed for the same column + if (row == connectionRow) { + addConnectionsHorizontally( + currentNode, connectionNode, col, row, connectionCol, currentPageYRenderOffset); + continue; + } + + // Only vertical connections are needed for the same row + if (col == connectionCol) { + addConnectionsVertically( + currentNode, connectionNode, col, row, connectionRow, currentPageYRenderOffset); + continue; + } + + // Handle complex connections here + + // Firstly, we add horizontal connections, if the turn is not enough + if (Math.abs(col - connectionCol) > 1) { + addConnectionsHorizontally( + currentNode, connectionNode, col, row, connectionCol, currentPageYRenderOffset); + } + + // Then we add the turn + addTurnConnection(currentNode, connectionNode, col, row, connectionCol, currentPageYRenderOffset); + + // Finally, we add vertical connections, if the turn is not enough, or if the connection is on the + // next + // page + if (Math.abs(row - connectionRow) > 1 || row > connectionRow) { + addConnectionsVertically( + currentNode, + connectionNode, + connectionCol, + row, + connectionRow, + currentPageYRenderOffset); + } + } + } + } + + int lowerBoxRenderX = NODE_AREA_OFFSET_X; + int lowerBoxRenderY = NODE_AREA_OFFSET_Y + NODE_AREA_HEIGHT; + + // 2/3 of the width of the node area, divided by 4, as there are 4 "spaces" between the archetypes + int spaceBetweenArchetypes = (NODE_AREA_WIDTH - AbilityArchetypeWidget.SIZE * 3) * 2 / 3 / 4; + + // Render the archetypes in the left half of the lower box + int archetypeCount = 0; + for (Map.Entry entry : + abilityTreeInfo.archetypeInfoMap().entrySet()) { + ArchetypeInfo archetypeInfo = entry.getValue(); + + AbilityArchetypeWidget archetypeWidget = new AbilityArchetypeWidget( + lowerBoxRenderX + archetypeCount * (AbilityArchetypeWidget.SIZE + spaceBetweenArchetypes) + 10, + lowerBoxRenderY + 7, + AbilityArchetypeWidget.SIZE, + AbilityArchetypeWidget.SIZE, + Component.literal(archetypeInfo.name()), + abilityTreeInfo, + abilityTreeInstance, + archetypeInfo); + + archetypeWidgets.add(archetypeWidget); + + archetypeCount++; + } + } + + private Pair getRenderLocation(AbilityTreeLocation location) { + float horizontalChunkWidth = (float) NODE_AREA_WIDTH / AbilityTreeLocation.MAX_COLS; + float verticalChunkHeight = (float) NODE_AREA_HEIGHT / AbilityTreeLocation.MAX_ROWS; + + return Pair.of((int) (location.col() * horizontalChunkWidth + horizontalChunkWidth / 2f), (int) + (location.row() * verticalChunkHeight + verticalChunkHeight / 2f)); + } + + // endregion + + // region Connection Logic + + private void addTurnConnection( + AbilityTreeSkillNode currentNode, + AbilityTreeSkillNode connectionNode, + int currentCol, + int currentRow, + int connectionCol, + int currentPageYRenderOffset) { + AbilityTreeLocation location = + new AbilityTreeLocation(currentNode.location().page(), currentRow, connectionCol); + Pair renderLocation = getRenderLocation(location); + + AbilityTreeConnectionNode node; + + if (currentCol < connectionCol) { + node = new AbilityTreeConnectionNode( + AbilityTreeConnectionType.DOWN_RIGHT_TURN, + new AbilityTreeSkillNode[] {null, null, connectionNode, currentNode}); + } else { + node = new AbilityTreeConnectionNode( + AbilityTreeConnectionType.DOWN_LEFT_TURN, + new AbilityTreeSkillNode[] {null, currentNode, connectionNode, null}); + } + + AbilityNodeConnectionWidget oldWidget = connectionWidgets.get(location); + AbilityTreeConnectionNode merged = node.merge(oldWidget != null ? oldWidget.getNode() : null); + + connectionWidgets.put( + location, + new AbilityNodeConnectionWidget( + renderLocation.a() - AbilityNodeConnectionWidget.SIZE / 2, + renderLocation.b() - AbilityNodeConnectionWidget.SIZE / 2 + currentPageYRenderOffset, + AbilityNodeConnectionWidget.SIZE, + AbilityNodeConnectionWidget.SIZE, + merged)); + } + + private void addConnectionsHorizontally( + AbilityTreeSkillNode currentNode, + AbilityTreeSkillNode connectionNode, + int currentCol, + int currentRow, + int targetCol, + int currentPageYRenderOffset) { + AbilityTreeSkillNode startNode = currentCol < targetCol ? currentNode : connectionNode; + AbilityTreeSkillNode endNode = currentCol < targetCol ? connectionNode : currentNode; + + int startCol = Math.min(currentCol, targetCol) + 1; + int endCol = Math.max(currentCol, targetCol) - 1; + + for (int i = startCol; i <= endCol; i++) { + AbilityTreeLocation location = + new AbilityTreeLocation(currentNode.location().page(), currentRow, i); + Pair renderLocation = getRenderLocation(location); + + AbilityTreeConnectionNode node = new AbilityTreeConnectionNode( + AbilityTreeConnectionType.HORIZONTAL, new AbilityTreeSkillNode[] {null, endNode, null, startNode}); + + AbilityNodeConnectionWidget oldWidget = connectionWidgets.get(location); + AbilityTreeConnectionNode merged = node.merge(oldWidget != null ? oldWidget.getNode() : null); + + connectionWidgets.put( + location, + new AbilityNodeConnectionWidget( + renderLocation.a() - AbilityNodeConnectionWidget.SIZE / 2, + renderLocation.b() - AbilityNodeConnectionWidget.SIZE / 2 + currentPageYRenderOffset, + AbilityNodeConnectionWidget.SIZE, + AbilityNodeConnectionWidget.SIZE, + merged)); + } + } + + private void addConnectionsVertically( + AbilityTreeSkillNode currentNode, + AbilityTreeSkillNode connectionNode, + int currentCol, + int currentRow, + int targetRow, + int currentPageYRenderOffset) { + AbilityTreeSkillNode startNode; + AbilityTreeSkillNode endNode; + int startRow; + int endRow; + + if (currentNode.location().page() > connectionNode.location().page()) { + startNode = connectionNode; + endNode = currentNode; + + startRow = 0; + endRow = targetRow - 1; + } else if (currentNode.location().page() < connectionNode.location().page()) { + startNode = currentNode; + endNode = connectionNode; + + startRow = currentRow + 1; + endRow = AbilityTreeLocation.MAX_ROWS - 1; + } else { + startNode = currentRow < targetRow ? currentNode : connectionNode; + endNode = currentRow < targetRow ? connectionNode : currentNode; + + startRow = Math.min(currentRow, targetRow) + 1; + endRow = Math.max(currentRow, targetRow) - 1; + } + + for (int i = startRow; i <= endRow; i++) { + AbilityTreeLocation location = + new AbilityTreeLocation(currentNode.location().page(), i, currentCol); + Pair renderLocation = getRenderLocation(location); + + AbilityTreeConnectionNode node = new AbilityTreeConnectionNode( + AbilityTreeConnectionType.VERTICAL, new AbilityTreeSkillNode[] {startNode, null, endNode, null}); + + AbilityNodeConnectionWidget oldWidget = connectionWidgets.get(location); + AbilityTreeConnectionNode merged = node.merge(oldWidget != null ? oldWidget.getNode() : null); + + connectionWidgets.put( + location, + new AbilityNodeConnectionWidget( + renderLocation.a() - AbilityNodeConnectionWidget.SIZE / 2, + renderLocation.b() - AbilityNodeConnectionWidget.SIZE / 2 + currentPageYRenderOffset, + AbilityNodeConnectionWidget.SIZE, + AbilityNodeConnectionWidget.SIZE, + merged)); + } + } + + // endregion +} diff --git a/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityArchetypeWidget.java b/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityArchetypeWidget.java new file mode 100644 index 0000000000..d534a125aa --- /dev/null +++ b/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityArchetypeWidget.java @@ -0,0 +1,57 @@ +/* + * Copyright © Wynntils 2024. + * This file is released under LGPLv3. See LICENSE for full license details. + */ +package com.wynntils.screens.abilities.widgets; + +import com.wynntils.models.abilitytree.type.AbilityTreeInfo; +import com.wynntils.models.abilitytree.type.AbilityTreeInstance; +import com.wynntils.models.abilitytree.type.ArchetypeInfo; +import com.wynntils.screens.base.TooltipProvider; +import java.util.List; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +public class AbilityArchetypeWidget extends AbstractWidget implements TooltipProvider { + public static final int SIZE = 20; + + private final AbilityTreeInfo abilityTreeInfo; + private final AbilityTreeInstance abilityTreeInstance; + private final ArchetypeInfo archetypeInfo; + + public AbilityArchetypeWidget( + int x, + int y, + int width, + int height, + Component message, + AbilityTreeInfo abilityTreeInfo, + AbilityTreeInstance abilityTreeInstance, + ArchetypeInfo archetypeInfo) { + super(x, y, width, height, message); + this.abilityTreeInfo = abilityTreeInfo; + this.abilityTreeInstance = abilityTreeInstance; + this.archetypeInfo = archetypeInfo; + } + + @Override + protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + ItemStack itemStack = + new ItemStack(Item.byId(archetypeInfo.itemInformation().itemId())); + itemStack.setDamageValue(archetypeInfo.itemInformation().damage()); + + guiGraphics.renderItem(itemStack, this.getX(), this.getY()); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) {} + + @Override + public List getTooltipLines() { + return archetypeInfo.getTooltip(abilityTreeInstance); + } +} diff --git a/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityNodeConnectionWidget.java b/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityNodeConnectionWidget.java new file mode 100644 index 0000000000..e93544db0b --- /dev/null +++ b/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityNodeConnectionWidget.java @@ -0,0 +1,34 @@ +/* + * Copyright © Wynntils 2023-2024. + * This file is released under LGPLv3. See LICENSE for full license details. + */ +package com.wynntils.screens.abilities.widgets; + +import com.wynntils.models.abilitytree.type.AbilityTreeConnectionNode; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; + +public class AbilityNodeConnectionWidget extends AbstractWidget { + public static final int SIZE = 20; + + private final AbilityTreeConnectionNode node; + + public AbilityNodeConnectionWidget(int x, int y, int width, int height, AbilityTreeConnectionNode node) { + super(x, y, width, height, Component.literal("Connection Node")); + this.node = node; + } + + @Override + public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + guiGraphics.renderItem(node.getItemStack(), this.getX(), this.getY()); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) {} + + public AbilityTreeConnectionNode getNode() { + return node; + } +} diff --git a/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityNodeWidget.java b/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityNodeWidget.java new file mode 100644 index 0000000000..7f49d3303a --- /dev/null +++ b/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityNodeWidget.java @@ -0,0 +1,65 @@ +/* + * Copyright © Wynntils 2023-2024. + * This file is released under LGPLv3. See LICENSE for full license details. + */ +package com.wynntils.screens.abilities.widgets; + +import com.wynntils.models.abilitytree.type.AbilityTreeInstance; +import com.wynntils.models.abilitytree.type.AbilityTreeNodeState; +import com.wynntils.models.abilitytree.type.AbilityTreeSkillNode; +import com.wynntils.screens.base.TooltipProvider; +import java.util.List; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +public class AbilityNodeWidget extends AbstractWidget implements TooltipProvider { + public static final int SIZE = 20; + + private final AbilityTreeInstance abilityTreeInstance; + private final AbilityTreeSkillNode node; + + public AbilityNodeWidget( + int x, int y, int width, int height, AbilityTreeInstance abilityTreeInstance, AbilityTreeSkillNode node) { + super(x, y, width, height, Component.literal(node.name())); + this.abilityTreeInstance = abilityTreeInstance; + this.node = node; + } + + @Override + public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + ItemStack itemStack = new ItemStack(Item.byId(node.itemInformation().itemId())); + + AbilityTreeNodeState nodeState = abilityTreeInstance.getNodeState(node); + int damage = + switch (nodeState) { + case UNREACHABLE, REQUIREMENT_NOT_MET -> node.itemInformation() + .getLockedDamage(); + case UNLOCKABLE -> node.itemInformation().getUnlockableDamage(); + case UNLOCKED -> node.itemInformation().getUnlockedDamage(); + case BLOCKED -> node.itemInformation().getBlockedDamage(); + }; + + itemStack.setDamageValue(damage); + CompoundTag tag = itemStack.getOrCreateTag(); + tag.putBoolean("Unbreakable", true); + + guiGraphics.renderItem(itemStack, this.getX(), this.getY()); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) {} + + public AbilityTreeSkillNode getNode() { + return node; + } + + @Override + public List getTooltipLines() { + return node.getDescription(abilityTreeInstance.getNodeState(node), abilityTreeInstance); + } +} diff --git a/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityTreePageSelectorButton.java b/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityTreePageSelectorButton.java new file mode 100644 index 0000000000..29da87dee3 --- /dev/null +++ b/common/src/main/java/com/wynntils/screens/abilities/widgets/AbilityTreePageSelectorButton.java @@ -0,0 +1,59 @@ +/* + * Copyright © Wynntils 2023-2024. + * This file is released under LGPLv3. See LICENSE for full license details. + */ +package com.wynntils.screens.abilities.widgets; + +import com.wynntils.core.components.Models; +import com.wynntils.screens.abilities.CustomAbilityTreeScreen; +import com.wynntils.utils.mc.McUtils; +import com.wynntils.utils.render.RenderUtils; +import com.wynntils.utils.render.Texture; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; +import net.minecraft.sounds.SoundEvents; + +public class AbilityTreePageSelectorButton extends AbstractWidget { + private final CustomAbilityTreeScreen screen; + private final boolean upDirection; + + public AbilityTreePageSelectorButton( + int x, int y, int width, int height, CustomAbilityTreeScreen screen, boolean up) { + super(x, y, width, height, Component.literal(up ? "Page Up" : "Page Down")); + this.screen = screen; + this.upDirection = up; + } + + @Override + public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + if (upDirection && screen.getCurrentScrollPercentage() == 0) return; + if (!upDirection && screen.getCurrentScrollPercentage() == Models.AbilityTree.ABILITY_TREE_PAGES - 1) return; + + Texture texture = upDirection ? Texture.ABILITY_TREE_UP_ARROW : Texture.ABILITY_TREE_DOWN_ARROW; + + RenderUtils.drawTexturedRect(guiGraphics.pose(), texture, this.getX(), this.getY()); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + // Check if we're at the top or bottom of the tree + if (upDirection && screen.getCurrentScrollPercentage() == 0) return true; + if (!upDirection && screen.getCurrentScrollPercentage() == Models.AbilityTree.ABILITY_TREE_PAGES - 1) + return true; + + if (upDirection) { + screen.setCurrentScrollPercentage(screen.getCurrentScrollPercentage() - 0.1f); + } else { + screen.setCurrentScrollPercentage(screen.getCurrentScrollPercentage() + 0.1f); + } + + McUtils.playSoundUI(SoundEvents.UI_BUTTON_CLICK.value()); + + return true; + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput narrationElementOutput) {} +} diff --git a/common/src/main/java/com/wynntils/utils/render/Texture.java b/common/src/main/java/com/wynntils/utils/render/Texture.java index 915fc01870..2771cbdc3b 100644 --- a/common/src/main/java/com/wynntils/utils/render/Texture.java +++ b/common/src/main/java/com/wynntils/utils/render/Texture.java @@ -151,6 +151,12 @@ public enum Texture { WOODWORKING_STATION("icons/map/woodworking_station.png", 17, 15), // endregion + // region Ability Tree + ABILITY_TREE_BACKGROUND("ability_tree/ability_tree_background.png", 317, 161), + ABILITY_TREE_DOWN_ARROW("ability_tree/down_button.png", 14, 14), + ABILITY_TREE_UP_ARROW("ability_tree/up_button.png", 14, 14), + // endregion + // region Item Storage ITEM_RECORD("item_storage/record.png", 171, 167), ITEM_RECORD_ADD("item_storage/record_add.png", 9, 18), diff --git a/common/src/main/resources/assets/wynntils/lang/en_us.json b/common/src/main/resources/assets/wynntils/lang/en_us.json index 26375b8866..fdd5e9dcab 100644 --- a/common/src/main/resources/assets/wynntils/lang/en_us.json +++ b/common/src/main/resources/assets/wynntils/lang/en_us.json @@ -285,6 +285,10 @@ "feature.wynntils.contentTrackerOverlay.overlay.contentTracker.title": "Tracked", "feature.wynntils.cosmeticsPreview.description": "Renders your player in the cosmetics screen.", "feature.wynntils.cosmeticsPreview.name": "Cosmetics Player Preview", + "feature.wynntils.customAbilityTree.description": "Adds a custom ability tree UI, allowing you to edit your abilities without saving the changes, adding presets, and more.", + "feature.wynntils.customAbilityTree.name": "Custom Ability Tree", + "feature.wynntils.customAbilityTree.noAbilityTreeData": "Could not download ability tree data. Try restarting your game.", + "feature.wynntils.customAbilityTree.noClassData": "Class data was not parsed. Try rejoining this character.", "feature.wynntils.customBankPageNames.description": "Adds an option to set custom page names in bank containers", "feature.wynntils.customBankPageNames.name": "Custom Bank Page Names", "feature.wynntils.customBankQuickJumps.accountBankDestinations.description": "Which pages would you like your account bank quick jump buttons to jump to? Enter a list of six numbers separated with a comma.", @@ -1785,6 +1789,18 @@ "overlay.wynntils.textOverlay.fontScale.name": "Font Scale", "overlay.wynntils.textOverlay.textShadow.description": "Should the text be drawn with a shadow?", "overlay.wynntils.textOverlay.textShadow.name": "Text Shadow", + "screens.wynntils.abilityTree.apiInfoOutdated": "The ability tree API info is outdated or missing. Try restarting the game.", + "screens.wynntils.abilityTree.archetype.unlockedAbilities": "Unlocked Abilities: ", + "screens.wynntils.abilityTree.errorClassNotParsed": "Failed to parse class data. Rejoin your character to try again.", + "screens.wynntils.abilityTree.errorNoClassData": "No ability tree info found for class. Try restarting the game.", + "screens.wynntils.abilityTree.errorParsingInstance": "Failed to parse ability tree. Reopen this screen to try again.", + "screens.wynntils.abilityTree.nodeState.blocked": "This node is blocked", + "screens.wynntils.abilityTree.nodeState.blockedBy": "Blocked by:", + "screens.wynntils.abilityTree.nodeState.requirementNotMet": "You do not meet the requirements", + "screens.wynntils.abilityTree.nodeState.unlockable": "Click to unlock this ability", + "screens.wynntils.abilityTree.nodeState.unlocked": "You already unlocked this ability", + "screens.wynntils.abilityTree.nodeState.unreachable": "You cannot reach this node", + "screens.wynntils.abilityTree.parsing": "Parsing ability tree...", "screens.wynntils.changelog.name": "Changelog", "screens.wynntils.characterSelection.add.discussion": "Clicking here will open the GUI to create a new character.", "screens.wynntils.characterSelection.add.name": "[>] Click here to add a new character", diff --git a/common/src/main/resources/assets/wynntils/textures/ability_tree/ability_tree_background.png b/common/src/main/resources/assets/wynntils/textures/ability_tree/ability_tree_background.png new file mode 100644 index 0000000000..891f1c535c Binary files /dev/null and b/common/src/main/resources/assets/wynntils/textures/ability_tree/ability_tree_background.png differ diff --git a/common/src/main/resources/assets/wynntils/textures/ability_tree/down_button.png b/common/src/main/resources/assets/wynntils/textures/ability_tree/down_button.png new file mode 100644 index 0000000000..10bee4af97 Binary files /dev/null and b/common/src/main/resources/assets/wynntils/textures/ability_tree/down_button.png differ diff --git a/common/src/main/resources/assets/wynntils/textures/ability_tree/up_button.png b/common/src/main/resources/assets/wynntils/textures/ability_tree/up_button.png new file mode 100644 index 0000000000..a280c5b67c Binary files /dev/null and b/common/src/main/resources/assets/wynntils/textures/ability_tree/up_button.png differ