Implement mutual exclusives
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package net.fretux.skillengine.capability;
|
package net.fretux.skillengine.capability;
|
||||||
|
|
||||||
|
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||||
import net.fretux.skillengine.skilltree.SkillNode;
|
import net.fretux.skillengine.skilltree.SkillNode;
|
||||||
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
|
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
@@ -14,22 +15,22 @@ public class PlayerSkillData {
|
|||||||
|
|
||||||
private int skillPoints;
|
private int skillPoints;
|
||||||
private final Set<ResourceLocation> unlockedNodes = new HashSet<>();
|
private final Set<ResourceLocation> unlockedNodes = new HashSet<>();
|
||||||
|
private final Set<ResourceLocation> unlockedAbilities = new HashSet<>();
|
||||||
private final Set<ResourceLocation> activeTags = new HashSet<>();
|
private final Set<ResourceLocation> activeTags = new HashSet<>();
|
||||||
|
|
||||||
public int getSkillPoints() { return skillPoints; }
|
public int getSkillPoints() { return skillPoints; }
|
||||||
public void addSkillPoints(int amount) { skillPoints += amount; }
|
public void addSkillPoints(int amount) { skillPoints += amount; }
|
||||||
|
|
||||||
public boolean isUnlocked(ResourceLocation nodeId) {
|
public boolean isUnlocked(ResourceLocation nodeId) {
|
||||||
return unlockedNodes.contains(nodeId);
|
return unlockedNodes.contains(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasTag(ResourceLocation tag) {
|
public boolean hasTag(ResourceLocation tag) {
|
||||||
return activeTags.contains(tag);
|
return activeTags.contains(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<ResourceLocation> getActiveTags() {
|
public Set<ResourceLocation> getActiveTags() {
|
||||||
return activeTags;
|
return activeTags;
|
||||||
}
|
}
|
||||||
|
private final ResourceLocation[] abilitySlots = new ResourceLocation[3];
|
||||||
|
|
||||||
|
|
||||||
public void unlockNode(SkillNode node) {
|
public void unlockNode(SkillNode node) {
|
||||||
if (unlockedNodes.add(node.getId())) {
|
if (unlockedNodes.add(node.getId())) {
|
||||||
@@ -41,7 +42,6 @@ public class PlayerSkillData {
|
|||||||
public CompoundTag save() {
|
public CompoundTag save() {
|
||||||
CompoundTag tag = new CompoundTag();
|
CompoundTag tag = new CompoundTag();
|
||||||
tag.putInt("SkillPoints", skillPoints);
|
tag.putInt("SkillPoints", skillPoints);
|
||||||
|
|
||||||
ListTag unlockedList = new ListTag();
|
ListTag unlockedList = new ListTag();
|
||||||
for (ResourceLocation id : unlockedNodes) {
|
for (ResourceLocation id : unlockedNodes) {
|
||||||
CompoundTag t = new CompoundTag();
|
CompoundTag t = new CompoundTag();
|
||||||
@@ -49,7 +49,6 @@ public class PlayerSkillData {
|
|||||||
unlockedList.add(t);
|
unlockedList.add(t);
|
||||||
}
|
}
|
||||||
tag.put("Unlocked", unlockedList);
|
tag.put("Unlocked", unlockedList);
|
||||||
|
|
||||||
ListTag tagList = new ListTag();
|
ListTag tagList = new ListTag();
|
||||||
for (ResourceLocation id : activeTags) {
|
for (ResourceLocation id : activeTags) {
|
||||||
CompoundTag t = new CompoundTag();
|
CompoundTag t = new CompoundTag();
|
||||||
@@ -57,7 +56,24 @@ public class PlayerSkillData {
|
|||||||
tagList.add(t);
|
tagList.add(t);
|
||||||
}
|
}
|
||||||
tag.put("Tags", tagList);
|
tag.put("Tags", tagList);
|
||||||
|
ListTag abilityList = new ListTag();
|
||||||
|
for (int i = 0; i < abilitySlots.length; i++) {
|
||||||
|
CompoundTag t = new CompoundTag();
|
||||||
|
if (abilitySlots[i] != null) {
|
||||||
|
t.putString("Id", abilitySlots[i].toString());
|
||||||
|
} else {
|
||||||
|
t.putString("Id", "");
|
||||||
|
}
|
||||||
|
abilityList.add(t);
|
||||||
|
}
|
||||||
|
tag.put("AbilitySlots", abilityList);
|
||||||
|
ListTag unlockedAbilitiesList = new ListTag();
|
||||||
|
for (ResourceLocation id : unlockedAbilities) {
|
||||||
|
CompoundTag t = new CompoundTag();
|
||||||
|
t.putString("Id", id.toString());
|
||||||
|
unlockedAbilitiesList.add(t);
|
||||||
|
}
|
||||||
|
tag.put("UnlockedAbilities", unlockedAbilitiesList);
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,18 +81,32 @@ public class PlayerSkillData {
|
|||||||
skillPoints = tag.getInt("SkillPoints");
|
skillPoints = tag.getInt("SkillPoints");
|
||||||
unlockedNodes.clear();
|
unlockedNodes.clear();
|
||||||
activeTags.clear();
|
activeTags.clear();
|
||||||
|
unlockedAbilities.clear();
|
||||||
ListTag unlockedList = tag.getList("Unlocked", Tag.TAG_COMPOUND);
|
ListTag unlockedList = tag.getList("Unlocked", Tag.TAG_COMPOUND);
|
||||||
for (Tag t : unlockedList) {
|
for (Tag t : unlockedList) {
|
||||||
CompoundTag c = (CompoundTag) t;
|
CompoundTag c = (CompoundTag) t;
|
||||||
unlockedNodes.add(new ResourceLocation(c.getString("Id")));
|
unlockedNodes.add(new ResourceLocation(c.getString("Id")));
|
||||||
}
|
}
|
||||||
|
|
||||||
ListTag tagList = tag.getList("Tags", Tag.TAG_COMPOUND);
|
ListTag tagList = tag.getList("Tags", Tag.TAG_COMPOUND);
|
||||||
for (Tag t : tagList) {
|
for (Tag t : tagList) {
|
||||||
CompoundTag c = (CompoundTag) t;
|
CompoundTag c = (CompoundTag) t;
|
||||||
activeTags.add(new ResourceLocation(c.getString("Tag")));
|
activeTags.add(new ResourceLocation(c.getString("Tag")));
|
||||||
}
|
}
|
||||||
|
ListTag abilityList = tag.getList("AbilitySlots", Tag.TAG_COMPOUND);
|
||||||
|
for (int i = 0; i < abilitySlots.length && i < abilityList.size(); i++) {
|
||||||
|
CompoundTag t = abilityList.getCompound(i);
|
||||||
|
String id = t.getString("Id");
|
||||||
|
if (!id.isEmpty()) {
|
||||||
|
abilitySlots[i] = new ResourceLocation(id);
|
||||||
|
} else {
|
||||||
|
abilitySlots[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ListTag unlockedAbilitiesList = tag.getList("UnlockedAbilities", Tag.TAG_COMPOUND);
|
||||||
|
for (Tag t : unlockedAbilitiesList) {
|
||||||
|
CompoundTag c = (CompoundTag) t;
|
||||||
|
unlockedAbilities.add(new ResourceLocation(c.getString("Id")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<ResourceLocation> getUnlockedNodes() {
|
public Set<ResourceLocation> getUnlockedNodes() {
|
||||||
@@ -98,4 +128,40 @@ public class PlayerSkillData {
|
|||||||
unlockedNodes.clear();
|
unlockedNodes.clear();
|
||||||
activeTags.clear();
|
activeTags.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void bindAbility(int slot, ResourceLocation abilityId) {
|
||||||
|
int index = slot - 1;
|
||||||
|
if (index < 0 || index >= abilitySlots.length) return;
|
||||||
|
// Ensure the same ability is not present in multiple slots
|
||||||
|
if (abilityId != null) {
|
||||||
|
for (int i = 0; i < abilitySlots.length; i++) {
|
||||||
|
if (abilityId.equals(abilitySlots[i])) {
|
||||||
|
abilitySlots[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
abilitySlots[index] = abilityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceLocation getAbilityInSlot(int slot) {
|
||||||
|
int index = slot - 1;
|
||||||
|
if (index < 0 || index >= abilitySlots.length) return null;
|
||||||
|
return abilitySlots[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceLocation[] getAbilitySlots() {
|
||||||
|
return abilitySlots.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAbilityUnlocked(ResourceLocation id) {
|
||||||
|
return unlockedAbilities.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockAbility(AbilityNode node) {
|
||||||
|
unlockedAbilities.add(node.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<ResourceLocation> getUnlockedAbilities() {
|
||||||
|
return unlockedAbilities;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package net.fretux.skillengine.client;
|
||||||
|
|
||||||
|
import net.fretux.skillengine.SkillEngine;
|
||||||
|
import net.fretux.skillengine.network.PacketHandler;
|
||||||
|
import net.fretux.skillengine.network.ServerboundBindAbilityPacket;
|
||||||
|
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.client.gui.components.Button;
|
||||||
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
public class AbilityBindingScreen extends Screen {
|
||||||
|
|
||||||
|
private static final int PANEL_WIDTH = 220;
|
||||||
|
private static final int PANEL_HEIGHT = 160;
|
||||||
|
|
||||||
|
private final AbilityNode ability;
|
||||||
|
|
||||||
|
public AbilityBindingScreen(AbilityNode ability) {
|
||||||
|
super(Component.literal("Bind Ability"));
|
||||||
|
this.ability = ability;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
int x = (width - PANEL_WIDTH) / 2;
|
||||||
|
int y = (height - PANEL_HEIGHT) / 2;
|
||||||
|
int buttonX = x + 20;
|
||||||
|
int buttonY = y + 80;
|
||||||
|
int boundSlot = SkilltreeClientState.getSlotOfAbility(ability.getId());
|
||||||
|
Button yBtn = addRenderableWidget(Button.builder(Component.literal("Bind to Y"),
|
||||||
|
b -> bind(1)).pos(buttonX, buttonY).size(80, 20).build());
|
||||||
|
if (boundSlot == 1) {
|
||||||
|
yBtn.active = false;
|
||||||
|
yBtn.setMessage(Component.literal("Bound to Y"));
|
||||||
|
}
|
||||||
|
Button xBtn = addRenderableWidget(Button.builder(Component.literal("Bind to X"),
|
||||||
|
b -> bind(2)).pos(buttonX + 90, buttonY).size(80, 20).build());
|
||||||
|
if (boundSlot == 2) {
|
||||||
|
xBtn.active = false;
|
||||||
|
xBtn.setMessage(Component.literal("Bound to X"));
|
||||||
|
}
|
||||||
|
Button cBtn = addRenderableWidget(Button.builder(Component.literal("Bind to C"),
|
||||||
|
b -> bind(3)).pos(buttonX, buttonY + 30).size(80, 20).build());
|
||||||
|
if (boundSlot == 3) {
|
||||||
|
cBtn.active = false;
|
||||||
|
cBtn.setMessage(Component.literal("Bound to C"));
|
||||||
|
}
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Cancel"),
|
||||||
|
b -> onClose()).pos(buttonX + 90, buttonY + 30).size(80, 20).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bind(int slot) {
|
||||||
|
SkilltreeClientState.bindAbilityLocal(slot, ability.getId());
|
||||||
|
SkillEngine.LOGGER.debug("[CLIENT] Binding ability {} to slot {} (optimistic)", ability.getId(), slot);
|
||||||
|
PacketHandler.CHANNEL.sendToServer(new ServerboundBindAbilityPacket(ability.getId(), slot));
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose() {
|
||||||
|
Minecraft.getInstance().setScreen(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean keyPressed(int key, int scancode, int mods) {
|
||||||
|
if (key == 256) {
|
||||||
|
onClose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.keyPressed(key, scancode, mods);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(GuiGraphics gfx, int mouseX, int mouseY, float partialTick) {
|
||||||
|
this.renderBackground(gfx);
|
||||||
|
int x = (width - PANEL_WIDTH) / 2;
|
||||||
|
int y = (height - PANEL_HEIGHT) / 2;
|
||||||
|
gfx.fill(0, 0, width, height, 0xAA000000);
|
||||||
|
gfx.fill(x, y, x + PANEL_WIDTH, y + PANEL_HEIGHT, 0xFF222222);
|
||||||
|
ResourceLocation icon = ability.getIcon();
|
||||||
|
if (icon != null) {
|
||||||
|
gfx.blit(icon, x + PANEL_WIDTH / 2 - 10, y + 10, 0, 0, 20, 20, 20, 20);
|
||||||
|
}
|
||||||
|
gfx.drawCenteredString(font, ability.getTitle(), x + PANEL_WIDTH / 2, y + 35, 0xFFFFFF);
|
||||||
|
if (ability.getDescription() != null) {
|
||||||
|
gfx.drawWordWrap(font, ability.getDescription(), x + 10, y + 50, PANEL_WIDTH - 20, 0xDDDDDD);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.render(gfx, mouseX, mouseY, partialTick);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package net.fretux.skillengine.client;
|
||||||
|
|
||||||
|
import net.fretux.skillengine.network.PacketHandler;
|
||||||
|
import net.fretux.skillengine.network.ServerboundBindAbilityPacket;
|
||||||
|
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||||
|
import net.fretux.skillengine.skilltree.AbilityNodeRegistry;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.client.gui.components.Button;
|
||||||
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AbilitySelectScreen extends Screen {
|
||||||
|
|
||||||
|
private final Screen parent;
|
||||||
|
private final int slot; // 1..3
|
||||||
|
|
||||||
|
private final List<AbilityNode> unlockedAbilities = new ArrayList<>();
|
||||||
|
private int page = 0;
|
||||||
|
private static final int PAGE_SIZE = 8;
|
||||||
|
|
||||||
|
private static final int PANEL_WIDTH = 260;
|
||||||
|
private static final int PANEL_HEIGHT = 220;
|
||||||
|
|
||||||
|
public AbilitySelectScreen(Screen parent, int slot) {
|
||||||
|
super(Component.literal("Select Ability"));
|
||||||
|
this.parent = parent;
|
||||||
|
this.slot = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
unlockedAbilities.clear();
|
||||||
|
for (AbilityNode node : AbilityNodeRegistry.all()) {
|
||||||
|
if (SkilltreeClientState.isAbilityUnlocked(node.getId())) {
|
||||||
|
unlockedAbilities.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildPageButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildPageButtons() {
|
||||||
|
clearWidgets();
|
||||||
|
int x = (width - PANEL_WIDTH) / 2;
|
||||||
|
int y = (height - PANEL_HEIGHT) / 2;
|
||||||
|
|
||||||
|
int start = page * PAGE_SIZE;
|
||||||
|
int end = Math.min(unlockedAbilities.size(), start + PAGE_SIZE);
|
||||||
|
int rowY = y + 40;
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
AbilityNode node = unlockedAbilities.get(i);
|
||||||
|
String label = node.getTitle().getString();
|
||||||
|
// Disable if already in this slot to avoid redundant bind
|
||||||
|
boolean alreadyInThisSlot = SkilltreeClientState.getSlotOfAbility(node.getId()) == slot;
|
||||||
|
Button btn = Button.builder(Component.literal(label), b -> select(node))
|
||||||
|
.pos(x + 15, rowY)
|
||||||
|
.size(PANEL_WIDTH - 30, 20)
|
||||||
|
.build();
|
||||||
|
if (alreadyInThisSlot) {
|
||||||
|
btn.active = false;
|
||||||
|
btn.setMessage(Component.literal(label + " (bound)"));
|
||||||
|
}
|
||||||
|
addRenderableWidget(btn);
|
||||||
|
rowY += 24;
|
||||||
|
}
|
||||||
|
// paging controls
|
||||||
|
if (page > 0) {
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("< Prev"), b -> {
|
||||||
|
page--;
|
||||||
|
buildPageButtons();
|
||||||
|
}).pos(x + 15, y + PANEL_HEIGHT - 28).size(60, 20).build());
|
||||||
|
}
|
||||||
|
if ((page + 1) * PAGE_SIZE < unlockedAbilities.size()) {
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Next >"), b -> {
|
||||||
|
page++;
|
||||||
|
buildPageButtons();
|
||||||
|
}).pos(x + 80, y + PANEL_HEIGHT - 28).size(60, 20).build());
|
||||||
|
}
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Back"), b -> back())
|
||||||
|
.pos(x + PANEL_WIDTH - 70, y + PANEL_HEIGHT - 28).size(60, 20).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void select(AbilityNode node) {
|
||||||
|
ResourceLocation id = node.getId();
|
||||||
|
// optimistic client update, then send to server
|
||||||
|
SkilltreeClientState.bindAbilityLocal(slot, id);
|
||||||
|
PacketHandler.CHANNEL.sendToServer(new ServerboundBindAbilityPacket(id, slot));
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void back() {
|
||||||
|
Minecraft.getInstance().setScreen(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPauseScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose() {
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(GuiGraphics gfx, int mouseX, int mouseY, float partialTick) {
|
||||||
|
this.renderBackground(gfx);
|
||||||
|
int x = (width - PANEL_WIDTH) / 2;
|
||||||
|
int y = (height - PANEL_HEIGHT) / 2;
|
||||||
|
gfx.fill(0, 0, width, height, 0xAA000000);
|
||||||
|
gfx.fill(x, y, x + PANEL_WIDTH, y + PANEL_HEIGHT, 0xFF222222);
|
||||||
|
String title = "Select for slot " + (slot == 1 ? "Y" : slot == 2 ? "X" : "C");
|
||||||
|
gfx.drawCenteredString(font, Component.literal(title), x + PANEL_WIDTH / 2, y + 12, 0xFFFFFF);
|
||||||
|
super.render(gfx, mouseX, mouseY, partialTick);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package net.fretux.skillengine.client;
|
||||||
|
|
||||||
|
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||||
|
import net.fretux.skillengine.skilltree.AbilityNodeRegistry;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.client.gui.components.Button;
|
||||||
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
public class AbilitySlotsScreen extends Screen {
|
||||||
|
|
||||||
|
private static final int PANEL_WIDTH = 260;
|
||||||
|
private static final int PANEL_HEIGHT = 180;
|
||||||
|
|
||||||
|
public AbilitySlotsScreen() {
|
||||||
|
super(Component.literal("Abilities"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
int x = (width - PANEL_WIDTH) / 2;
|
||||||
|
int y = (height - PANEL_HEIGHT) / 2;
|
||||||
|
|
||||||
|
int rowY = y + 40;
|
||||||
|
int buttonW = 80;
|
||||||
|
int buttonH = 20;
|
||||||
|
int buttonX = x + PANEL_WIDTH - buttonW - 15;
|
||||||
|
|
||||||
|
// Y slot = 1
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Rebind"), b -> openSelect(1))
|
||||||
|
.pos(buttonX, rowY).size(buttonW, buttonH).build());
|
||||||
|
// X slot = 2
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Rebind"), b -> openSelect(2))
|
||||||
|
.pos(buttonX, rowY + 35).size(buttonW, buttonH).build());
|
||||||
|
// C slot = 3
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Rebind"), b -> openSelect(3))
|
||||||
|
.pos(buttonX, rowY + 70).size(buttonW, buttonH).build());
|
||||||
|
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Close"), b -> onClose())
|
||||||
|
.pos(x + PANEL_WIDTH - 70, y + PANEL_HEIGHT - 28).size(60, 20).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openSelect(int slot) {
|
||||||
|
Minecraft.getInstance().setScreen(new AbilitySelectScreen(this, slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPauseScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose() {
|
||||||
|
Minecraft.getInstance().setScreen(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(GuiGraphics gfx, int mouseX, int mouseY, float partialTick) {
|
||||||
|
this.renderBackground(gfx);
|
||||||
|
int x = (width - PANEL_WIDTH) / 2;
|
||||||
|
int y = (height - PANEL_HEIGHT) / 2;
|
||||||
|
|
||||||
|
gfx.fill(0, 0, width, height, 0xAA000000);
|
||||||
|
gfx.fill(x, y, x + PANEL_WIDTH, y + PANEL_HEIGHT, 0xFF222222);
|
||||||
|
gfx.drawCenteredString(font, Component.literal("Ability Slots"), x + PANEL_WIDTH / 2, y + 12, 0xFFFFFF);
|
||||||
|
|
||||||
|
int rowY = y + 40;
|
||||||
|
drawSlotRow(gfx, x + 15, rowY, 1, "Y");
|
||||||
|
drawSlotRow(gfx, x + 15, rowY + 35, 2, "X");
|
||||||
|
drawSlotRow(gfx, x + 15, rowY + 70, 3, "C");
|
||||||
|
|
||||||
|
super.render(gfx, mouseX, mouseY, partialTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawSlotRow(GuiGraphics gfx, int x, int y, int slot, String label) {
|
||||||
|
gfx.drawString(font, label + ":", x, y + 6, 0xFFFFFF);
|
||||||
|
ResourceLocation abilityId = SkilltreeClientState.getAbilityInSlot(slot);
|
||||||
|
String title = "Empty";
|
||||||
|
ResourceLocation icon = null;
|
||||||
|
if (abilityId != null) {
|
||||||
|
AbilityNode node = AbilityNodeRegistry.get(abilityId);
|
||||||
|
if (node != null) {
|
||||||
|
title = node.getTitle().getString();
|
||||||
|
icon = node.getIcon();
|
||||||
|
} else {
|
||||||
|
title = abilityId.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (icon != null) {
|
||||||
|
gfx.blit(icon, x + 20, y - 2, 0, 0, 20, 20, 20, 20);
|
||||||
|
gfx.drawString(font, title, x + 46, y + 6, 0xDDDDDD);
|
||||||
|
} else {
|
||||||
|
gfx.drawString(font, title, x + 20, y + 6, 0x888888);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,5 +19,9 @@ public class ClientEvents {
|
|||||||
|
|
||||||
Minecraft.getInstance().setScreen(new SkilltreeScreen());
|
Minecraft.getInstance().setScreen(new SkilltreeScreen());
|
||||||
}
|
}
|
||||||
|
if (ClientKeybinds.OPEN_ABILITIES != null &&
|
||||||
|
ClientKeybinds.OPEN_ABILITIES.consumeClick()) {
|
||||||
|
Minecraft.getInstance().setScreen(new AbilitySlotsScreen());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ import org.lwjgl.glfw.GLFW;
|
|||||||
)
|
)
|
||||||
public class ClientKeybinds {
|
public class ClientKeybinds {
|
||||||
public static KeyMapping OPEN_SKILLTREE;
|
public static KeyMapping OPEN_SKILLTREE;
|
||||||
|
public static KeyMapping OPEN_ABILITIES;
|
||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public static void registerKeys(RegisterKeyMappingsEvent event) {
|
public static void registerKeys(RegisterKeyMappingsEvent event) {
|
||||||
@@ -24,5 +25,12 @@ public class ClientKeybinds {
|
|||||||
"key.categories.ui"
|
"key.categories.ui"
|
||||||
);
|
);
|
||||||
event.register(OPEN_SKILLTREE);
|
event.register(OPEN_SKILLTREE);
|
||||||
|
|
||||||
|
OPEN_ABILITIES = new KeyMapping(
|
||||||
|
"key.skillengine.openabilities",
|
||||||
|
GLFW.GLFW_KEY_M,
|
||||||
|
"key.categories.ui"
|
||||||
|
);
|
||||||
|
event.register(OPEN_ABILITIES);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,18 +12,17 @@ import java.util.Set;
|
|||||||
|
|
||||||
public class SkilltreeClientState {
|
public class SkilltreeClientState {
|
||||||
private static final Set<ResourceLocation> unlocked = new HashSet<>();
|
private static final Set<ResourceLocation> unlocked = new HashSet<>();
|
||||||
|
private static final Set<ResourceLocation> unlockedAbilities = new HashSet<>();
|
||||||
private static int currentSkillPoints = 0;
|
private static int currentSkillPoints = 0;
|
||||||
|
private static final ResourceLocation[] abilitySlots = new ResourceLocation[3];
|
||||||
|
|
||||||
public static void unlockNode(ResourceLocation id) {
|
public static void unlockNode(ResourceLocation id) {
|
||||||
SkillEngine.LOGGER.info("[CLIENT] Unlocking node: {}", id);
|
SkillEngine.LOGGER.info("[CLIENT] Unlocking node: {}", id);
|
||||||
unlocked.add(id);
|
unlocked.add(id);
|
||||||
|
|
||||||
Minecraft mc = Minecraft.getInstance();
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
|
||||||
if (mc.player != null) {
|
if (mc.player != null) {
|
||||||
mc.player.playSound(SoundEvents.EXPERIENCE_ORB_PICKUP, 1f, 1.2f);
|
mc.player.playSound(SoundEvents.EXPERIENCE_ORB_PICKUP, 1f, 1.2f);
|
||||||
}
|
}
|
||||||
|
|
||||||
mc.getToasts().addToast(
|
mc.getToasts().addToast(
|
||||||
SystemToast.multiline(
|
SystemToast.multiline(
|
||||||
mc,
|
mc,
|
||||||
@@ -43,6 +42,11 @@ public class SkilltreeClientState {
|
|||||||
unlocked.addAll(ids);
|
unlocked.addAll(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setUnlockedAbilities(Set<ResourceLocation> abilities) {
|
||||||
|
unlockedAbilities.clear();
|
||||||
|
unlockedAbilities.addAll(abilities);
|
||||||
|
}
|
||||||
|
|
||||||
public static void setCurrentSkillPoints(int points) {
|
public static void setCurrentSkillPoints(int points) {
|
||||||
currentSkillPoints = points;
|
currentSkillPoints = points;
|
||||||
}
|
}
|
||||||
@@ -50,4 +54,44 @@ public class SkilltreeClientState {
|
|||||||
public static int getCurrentSkillPoints() {
|
public static int getCurrentSkillPoints() {
|
||||||
return currentSkillPoints;
|
return currentSkillPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isAbilityUnlocked(ResourceLocation id) {
|
||||||
|
return unlockedAbilities.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void unlockAbility(ResourceLocation id) {
|
||||||
|
unlockedAbilities.add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bindAbilityLocal(int slot, ResourceLocation abilityId) {
|
||||||
|
int index = slot - 1;
|
||||||
|
if (index < 0 || index >= abilitySlots.length) return;
|
||||||
|
if (abilityId != null) {
|
||||||
|
for (int i = 0; i < abilitySlots.length; i++) {
|
||||||
|
if (abilityId.equals(abilitySlots[i])) abilitySlots[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
abilitySlots[index] = abilityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setAbilitySlots(ResourceLocation[] slots) {
|
||||||
|
for (int i = 0; i < abilitySlots.length; i++) {
|
||||||
|
abilitySlots[i] = (slots != null && i < slots.length) ? slots[i] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResourceLocation getAbilityInSlot(int slot) {
|
||||||
|
int index = slot - 1;
|
||||||
|
if (index < 0 || index >= abilitySlots.length) return null;
|
||||||
|
return abilitySlots[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getSlotOfAbility(ResourceLocation abilityId) {
|
||||||
|
if (abilityId == null) return -1;
|
||||||
|
for (int i = 0; i < abilitySlots.length; i++) {
|
||||||
|
if (abilityId.equals(abilitySlots[i])) return i + 1;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@ import com.mojang.math.Axis;
|
|||||||
import net.fretux.ascend.player.PlayerStatsProvider;
|
import net.fretux.ascend.player.PlayerStatsProvider;
|
||||||
import net.fretux.skillengine.network.PacketHandler;
|
import net.fretux.skillengine.network.PacketHandler;
|
||||||
import net.fretux.skillengine.network.ServerboundUnlockNodePacket;
|
import net.fretux.skillengine.network.ServerboundUnlockNodePacket;
|
||||||
|
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||||
|
import net.fretux.skillengine.skilltree.AbilityNodeRegistry;
|
||||||
import net.fretux.skillengine.skilltree.SkillNode;
|
import net.fretux.skillengine.skilltree.SkillNode;
|
||||||
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
|
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
|
||||||
import net.minecraft.ChatFormatting;
|
import net.minecraft.ChatFormatting;
|
||||||
@@ -14,7 +16,6 @@ import net.minecraft.client.gui.screens.Screen;
|
|||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.network.chat.FormattedText;
|
import net.minecraft.network.chat.FormattedText;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.util.FormattedCharSequence;
|
|
||||||
import net.minecraft.util.Mth;
|
import net.minecraft.util.Mth;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -22,18 +23,31 @@ import java.util.List;
|
|||||||
|
|
||||||
public class SkilltreeScreen extends Screen {
|
public class SkilltreeScreen extends Screen {
|
||||||
|
|
||||||
|
private static final int NODE_RADIUS = 12;
|
||||||
|
private static final int ABILITY_RADIUS = 12;
|
||||||
|
|
||||||
|
private static final int OVERLAY_WIDTH = 240;
|
||||||
|
private static final int OVERLAY_HEIGHT = 180;
|
||||||
|
private static final int OVERLAY_CONTENT_HEIGHT = 160;
|
||||||
|
|
||||||
|
private static final float ZOOM_STEP = 0.1f;
|
||||||
|
private static final float LINE_THICKNESS = 2.0f;
|
||||||
|
|
||||||
private float zoom = 1.0f;
|
private float zoom = 1.0f;
|
||||||
private float minZoom = 0.5f;
|
private final float minZoom = 0.5f;
|
||||||
private float maxZoom = 2.0f;
|
private final float maxZoom = 2.0f;
|
||||||
|
|
||||||
private double panX = 0;
|
private double panX = 0;
|
||||||
private double panY = 0;
|
private double panY = 0;
|
||||||
|
|
||||||
private boolean dragging = false;
|
private boolean dragging = false;
|
||||||
private double lastMouseX, lastMouseY;
|
private double lastMouseX;
|
||||||
|
private double lastMouseY;
|
||||||
|
|
||||||
private SkillNode hoveredNode = null;
|
private SkillNode hoveredNode = null;
|
||||||
private SkillNode selectedNode = null;
|
private SkillNode selectedNode = null;
|
||||||
|
private AbilityNode hoveredAbility = null;
|
||||||
|
private AbilityNode selectedAbility = null;
|
||||||
|
|
||||||
public SkilltreeScreen() {
|
public SkilltreeScreen() {
|
||||||
super(Component.literal("Ascend Skilltree"));
|
super(Component.literal("Ascend Skilltree"));
|
||||||
@@ -46,34 +60,42 @@ public class SkilltreeScreen extends Screen {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean mouseScrolled(double mouseX, double mouseY, double delta) {
|
public boolean mouseScrolled(double mouseX, double mouseY, double delta) {
|
||||||
zoom += delta * 0.1f;
|
zoom += delta * ZOOM_STEP;
|
||||||
zoom = Mth.clamp(zoom, minZoom, maxZoom);
|
zoom = Mth.clamp(zoom, minZoom, maxZoom);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||||
if (selectedNode != null) {
|
if (selectedNode != null || selectedAbility != null) {
|
||||||
return super.mouseClicked(mouseX, mouseY, button);
|
return super.mouseClicked(mouseX, mouseY, button);
|
||||||
}
|
}
|
||||||
if (button == 0) {
|
if (button == 0) {
|
||||||
SkillNode clicked = findNodeAt(mouseX, mouseY);
|
SkillNode clickedNode = findNodeAt(mouseX, mouseY);
|
||||||
if (clicked != null) {
|
if (clickedNode != null) {
|
||||||
boolean lockedByExclusivity = clicked.getExclusiveWith().stream()
|
if (isLockedByExclusivity(clickedNode)) {
|
||||||
.anyMatch(SkilltreeClientState::isUnlocked);
|
|
||||||
if (lockedByExclusivity) {
|
|
||||||
Minecraft.getInstance().player.playSound(
|
Minecraft.getInstance().player.playSound(
|
||||||
net.minecraft.sounds.SoundEvents.VILLAGER_NO, 1f, 1f
|
net.minecraft.sounds.SoundEvents.VILLAGER_NO, 1f, 1f
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (SkilltreeClientState.isUnlocked(clicked.getId())) {
|
if (SkilltreeClientState.isUnlocked(clickedNode.getId())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
selectedNode = clicked;
|
selectedNode = clickedNode;
|
||||||
rebuildOverlayButtons();
|
rebuildOverlayButtons();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
AbilityNode clickedAbility = findAbilityAt(mouseX, mouseY);
|
||||||
|
if (clickedAbility != null) {
|
||||||
|
if (SkilltreeClientState.isAbilityUnlocked(clickedAbility.getId())) {
|
||||||
|
Minecraft.getInstance().setScreen(new AbilityBindingScreen(clickedAbility));
|
||||||
|
} else {
|
||||||
|
selectedAbility = clickedAbility;
|
||||||
|
rebuildAbilityOverlayButtons();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
dragging = true;
|
dragging = true;
|
||||||
lastMouseX = mouseX;
|
lastMouseX = mouseX;
|
||||||
lastMouseY = mouseY;
|
lastMouseY = mouseY;
|
||||||
@@ -83,7 +105,7 @@ public class SkilltreeScreen extends Screen {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean mouseDragged(double mouseX, double mouseY, int button, double dx, double dy) {
|
public boolean mouseDragged(double mouseX, double mouseY, int button, double dx, double dy) {
|
||||||
if (selectedNode != null) {
|
if (selectedNode != null || selectedAbility != null) {
|
||||||
return super.mouseDragged(mouseX, mouseY, button, dx, dy);
|
return super.mouseDragged(mouseX, mouseY, button, dx, dy);
|
||||||
}
|
}
|
||||||
if (dragging && button == 0) {
|
if (dragging && button == 0) {
|
||||||
@@ -105,48 +127,26 @@ public class SkilltreeScreen extends Screen {
|
|||||||
@Override
|
@Override
|
||||||
public void render(GuiGraphics gfx, int mouseX, int mouseY, float partialTicks) {
|
public void render(GuiGraphics gfx, int mouseX, int mouseY, float partialTicks) {
|
||||||
this.renderBackground(gfx);
|
this.renderBackground(gfx);
|
||||||
|
renderSkillPointHeader(gfx);
|
||||||
|
hoveredNode = findNodeAt(mouseX, mouseY);
|
||||||
|
hoveredAbility = findAbilityAt(mouseX, mouseY);
|
||||||
|
drawGraph(gfx);
|
||||||
|
if (hoveredNode != null && selectedNode == null && selectedAbility == null) {
|
||||||
|
renderNodeTooltip(gfx, hoveredNode, mouseX, mouseY);
|
||||||
|
}
|
||||||
|
if (selectedNode != null) {
|
||||||
|
renderNodeOverlay(gfx, selectedNode);
|
||||||
|
} else if (selectedAbility != null) {
|
||||||
|
renderAbilityOverlay(gfx, selectedAbility);
|
||||||
|
}
|
||||||
|
super.render(gfx, mouseX, mouseY, partialTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderSkillPointHeader(GuiGraphics gfx) {
|
||||||
int remaining = SkilltreeClientState.getCurrentSkillPoints();
|
int remaining = SkilltreeClientState.getCurrentSkillPoints();
|
||||||
Component pointsText = Component.literal(remaining + " skillpoints remaining")
|
Component pointsText = Component.literal(remaining + " skillpoints remaining")
|
||||||
.withStyle(ChatFormatting.YELLOW);
|
.withStyle(ChatFormatting.YELLOW);
|
||||||
gfx.drawString(font, pointsText, 10, 10, 0xFFFFFF);
|
gfx.drawString(font, pointsText, 10, 10, 0xFFFFFF);
|
||||||
|
|
||||||
hoveredNode = findNodeAt(mouseX, mouseY);
|
|
||||||
drawGraph(gfx);
|
|
||||||
if (hoveredNode != null && selectedNode == null) {
|
|
||||||
List<Component> tooltip = new ArrayList<>();
|
|
||||||
tooltip.add(hoveredNode.getTitle());
|
|
||||||
String desc = hoveredNode.getDescription().getString();
|
|
||||||
List<FormattedText> wrapped = font.getSplitter().splitLines(
|
|
||||||
desc,
|
|
||||||
220,
|
|
||||||
hoveredNode.getDescription().getStyle()
|
|
||||||
);
|
|
||||||
for (FormattedText ft : wrapped) {
|
|
||||||
tooltip.add(Component.literal(ft.getString()).withStyle(ChatFormatting.GRAY));
|
|
||||||
}
|
|
||||||
tooltip.add(Component.literal("Cost: " + hoveredNode.getCost())
|
|
||||||
.withStyle(ChatFormatting.DARK_AQUA));
|
|
||||||
if (!hoveredNode.getPrereqAttributes().isEmpty()) {
|
|
||||||
tooltip.add(Component.literal("Requirements:")
|
|
||||||
.withStyle(ChatFormatting.GOLD));
|
|
||||||
Minecraft.getInstance().player.getCapability(PlayerStatsProvider.PLAYER_STATS)
|
|
||||||
.ifPresent(stats -> {
|
|
||||||
hoveredNode.getPrereqAttributes().forEach((attr, required) -> {
|
|
||||||
int current = stats.getAttributeLevel(attr);
|
|
||||||
boolean ok = current >= required;
|
|
||||||
tooltip.add(
|
|
||||||
Component.literal(" - " + attr + ": " + current + "/" + required)
|
|
||||||
.withStyle(ok ? ChatFormatting.GREEN : ChatFormatting.RED)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
gfx.renderComponentTooltip(font, tooltip, mouseX, mouseY);
|
|
||||||
}
|
|
||||||
if (selectedNode != null) {
|
|
||||||
renderOverlay(gfx, selectedNode);
|
|
||||||
}
|
|
||||||
super.render(gfx, mouseX, mouseY, partialTicks);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawGraph(GuiGraphics gfx) {
|
private void drawGraph(GuiGraphics gfx) {
|
||||||
@@ -161,35 +161,212 @@ public class SkilltreeScreen extends Screen {
|
|||||||
SkilltreeClientState.isUnlocked(neighbor.getId());
|
SkilltreeClientState.isUnlocked(neighbor.getId());
|
||||||
int linkColor = bothUnlocked ? 0xFF88FF88 : 0xFF666666;
|
int linkColor = bothUnlocked ? 0xFF88FF88 : 0xFF666666;
|
||||||
drawLine(gfx, p1[0], p1[1], p2[0], p2[1], linkColor);
|
drawLine(gfx, p1[0], p1[1], p2[0], p2[1], linkColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (AbilityNode ability : AbilityNodeRegistry.all()) {
|
||||||
|
int[] p1 = worldToScreen(ability.getX(), ability.getY());
|
||||||
|
for (ResourceLocation link : ability.getLinks()) {
|
||||||
|
SkillNode node = SkillNodeRegistry.get(link);
|
||||||
|
AbilityNode ability2 = AbilityNodeRegistry.get(link);
|
||||||
|
if (node != null) {
|
||||||
|
int[] p2 = worldToScreen(node.getX(), node.getY());
|
||||||
|
drawLine(gfx, p1[0], p1[1], p2[0], p2[1], 0xFF6666AA);
|
||||||
|
} else if (ability2 != null) {
|
||||||
|
int[] p2 = worldToScreen(ability2.getX(), ability2.getY());
|
||||||
|
drawLine(gfx, p1[0], p1[1], p2[0], p2[1], 0xFF6666AA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (AbilityNode ability : AbilityNodeRegistry.all()) {
|
||||||
|
int[] pos = worldToScreen(ability.getX(), ability.getY());
|
||||||
|
boolean unlocked = SkilltreeClientState.isAbilityUnlocked(ability.getId());
|
||||||
|
int color;
|
||||||
|
if (ability == selectedAbility) {
|
||||||
|
color = 0xFFAA8833;
|
||||||
|
} else if (ability == hoveredAbility) {
|
||||||
|
color = 0xFF8888AA;
|
||||||
|
} else if (unlocked) {
|
||||||
|
color = 0xFF3399FF;
|
||||||
|
} else {
|
||||||
|
color = 0xFF223355;
|
||||||
|
}
|
||||||
|
gfx.fill(
|
||||||
|
pos[0] - ABILITY_RADIUS,
|
||||||
|
pos[1] - ABILITY_RADIUS,
|
||||||
|
pos[0] + ABILITY_RADIUS,
|
||||||
|
pos[1] + ABILITY_RADIUS,
|
||||||
|
color
|
||||||
|
);
|
||||||
|
ResourceLocation icon = ability.getIcon();
|
||||||
|
if (icon != null) {
|
||||||
|
gfx.blit(icon, pos[0] - 8, pos[1] - 8, 0, 0, 16, 16, 16, 16);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (SkillNode node : SkillNodeRegistry.all()) {
|
for (SkillNode node : SkillNodeRegistry.all()) {
|
||||||
int[] pos = worldToScreen(node.getX(), node.getY());
|
int[] pos = worldToScreen(node.getX(), node.getY());
|
||||||
int r = 12;
|
int color = getNodeColor(node);
|
||||||
boolean unlocked = SkilltreeClientState.isUnlocked(node.getId());
|
gfx.fill(
|
||||||
int color = unlocked ? 0xFF44CC44 : 0xFF444444;
|
pos[0] - NODE_RADIUS,
|
||||||
boolean lockedByExclusivity = node.getExclusiveWith().stream()
|
pos[1] - NODE_RADIUS,
|
||||||
.anyMatch(SkilltreeClientState::isUnlocked);
|
pos[0] + NODE_RADIUS,
|
||||||
if (lockedByExclusivity) {
|
pos[1] + NODE_RADIUS,
|
||||||
color = 0xFFCC3333;
|
color
|
||||||
}
|
);
|
||||||
if (node == hoveredNode) color = 0xFF777777;
|
|
||||||
if (node == selectedNode) color = 0xFF999933;
|
|
||||||
gfx.fill(pos[0] - r, pos[1] - r, pos[0] + r, pos[1] + r, color);
|
|
||||||
ResourceLocation icon = node.getIcons();
|
ResourceLocation icon = node.getIcons();
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
int size = 16;
|
int size = 16;
|
||||||
gfx.blit(icon,
|
gfx.blit(
|
||||||
|
icon,
|
||||||
pos[0] - size / 2,
|
pos[0] - size / 2,
|
||||||
pos[1] - size / 2,
|
pos[1] - size / 2,
|
||||||
0, 0,
|
0, 0,
|
||||||
size, size,
|
size, size,
|
||||||
size, size);
|
size, size
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void renderNodeTooltip(GuiGraphics gfx, SkillNode node, int mouseX, int mouseY) {
|
||||||
|
List<Component> tooltip = new ArrayList<>();
|
||||||
|
tooltip.add(node.getTitle());
|
||||||
|
String desc = node.getDescription().getString();
|
||||||
|
List<FormattedText> wrapped = font.getSplitter().splitLines(
|
||||||
|
desc,
|
||||||
|
220,
|
||||||
|
node.getDescription().getStyle()
|
||||||
|
);
|
||||||
|
for (FormattedText ft : wrapped) {
|
||||||
|
tooltip.add(Component.literal(ft.getString()).withStyle(ChatFormatting.GRAY));
|
||||||
|
}
|
||||||
|
tooltip.add(Component.literal("Cost: " + node.getCost())
|
||||||
|
.withStyle(ChatFormatting.DARK_AQUA));
|
||||||
|
if (!node.getPrereqAttributes().isEmpty()) {
|
||||||
|
tooltip.add(Component.literal("Requirements:")
|
||||||
|
.withStyle(ChatFormatting.GOLD));
|
||||||
|
Minecraft.getInstance().player.getCapability(PlayerStatsProvider.PLAYER_STATS)
|
||||||
|
.ifPresent(stats -> node.getPrereqAttributes().forEach((attr, required) -> {
|
||||||
|
int current = stats.getAttributeLevel(attr);
|
||||||
|
boolean ok = current >= required;
|
||||||
|
tooltip.add(
|
||||||
|
Component.literal(" - " + attr + ": " + current + "/" + required)
|
||||||
|
.withStyle(ok ? ChatFormatting.GREEN : ChatFormatting.RED)
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
gfx.renderComponentTooltip(font, tooltip, mouseX, mouseY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderNodeOverlay(GuiGraphics gfx, SkillNode node) {
|
||||||
|
if (isLockedByExclusivity(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int x = (width - OVERLAY_WIDTH) / 2;
|
||||||
|
int y = (height - OVERLAY_HEIGHT) / 2;
|
||||||
|
gfx.fill(0, 0, width, height, 0x88000000);
|
||||||
|
gfx.fill(x, y, x + OVERLAY_WIDTH, y + OVERLAY_HEIGHT, 0xFF222222);
|
||||||
|
gfx.drawCenteredString(font, node.getTitle(), x + OVERLAY_WIDTH / 2, y + 10, 0xFFFFFF);
|
||||||
|
gfx.drawWordWrap(font, node.getDescription(), x + 10, y + 30, OVERLAY_WIDTH - 20, 0xDDDDDD);
|
||||||
|
final int[] textY = {y + 95};
|
||||||
|
gfx.drawString(
|
||||||
|
font,
|
||||||
|
Component.literal("Cost: " + node.getCost() + " skill points")
|
||||||
|
.withStyle(ChatFormatting.AQUA),
|
||||||
|
x + 10,
|
||||||
|
textY[0],
|
||||||
|
0xAAAAAA
|
||||||
|
);
|
||||||
|
textY[0] += 15;
|
||||||
|
if (!node.getPrereqAttributes().isEmpty()) {
|
||||||
|
gfx.drawString(
|
||||||
|
font,
|
||||||
|
Component.literal("Requirements:")
|
||||||
|
.withStyle(ChatFormatting.GOLD),
|
||||||
|
x + 10,
|
||||||
|
textY[0],
|
||||||
|
0xFFFFFF
|
||||||
|
);
|
||||||
|
textY[0] += 12;
|
||||||
|
Minecraft.getInstance().player.getCapability(PlayerStatsProvider.PLAYER_STATS)
|
||||||
|
.ifPresent(stats -> node.getPrereqAttributes().forEach((attr, required) -> {
|
||||||
|
int current = stats.getAttributeLevel(attr);
|
||||||
|
boolean ok = current >= required;
|
||||||
|
|
||||||
|
Component line = Component.literal(
|
||||||
|
" - " + attr + ": " + current + "/" + required
|
||||||
|
).withStyle(ok ? ChatFormatting.GREEN : ChatFormatting.RED);
|
||||||
|
|
||||||
|
gfx.drawString(font, line, x + 10, textY[0], 0xFFFFFF);
|
||||||
|
textY[0] += 12;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderAbilityOverlay(GuiGraphics gfx, AbilityNode ability) {
|
||||||
|
int x = (width - OVERLAY_WIDTH) / 2;
|
||||||
|
int y = (height - OVERLAY_HEIGHT) / 2;
|
||||||
|
gfx.fill(0, 0, width, height, 0x88000000);
|
||||||
|
gfx.fill(x, y, x + OVERLAY_WIDTH, y + OVERLAY_HEIGHT, 0xFF222222);
|
||||||
|
gfx.drawCenteredString(font, ability.getTitle(), x + OVERLAY_WIDTH / 2, y + 10, 0xFFFFFF);
|
||||||
|
if (ability.getDescription() != null) {
|
||||||
|
gfx.drawWordWrap(font, ability.getDescription(), x + 10, y + 30, OVERLAY_WIDTH - 20, 0xDDDDDD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildOverlayButtons() {
|
||||||
|
clearWidgets();
|
||||||
|
if (selectedNode == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int x = (width - OVERLAY_WIDTH) / 2;
|
||||||
|
int y = (height - OVERLAY_CONTENT_HEIGHT) / 2;
|
||||||
|
boolean alreadyUnlocked = SkilltreeClientState.isUnlocked(selectedNode.getId());
|
||||||
|
if (!alreadyUnlocked) {
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Unlock"), btn -> {
|
||||||
|
PacketHandler.CHANNEL.sendToServer(
|
||||||
|
new ServerboundUnlockNodePacket(selectedNode.getId())
|
||||||
|
);
|
||||||
|
selectedNode = null;
|
||||||
|
clearWidgets();
|
||||||
|
}).pos(x + OVERLAY_WIDTH / 2 - 40, y + OVERLAY_CONTENT_HEIGHT - 20)
|
||||||
|
.size(80, 20)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Close"), btn -> {
|
||||||
|
selectedNode = null;
|
||||||
|
clearWidgets();
|
||||||
|
}).pos(x + OVERLAY_WIDTH - 60, y - 5)
|
||||||
|
.size(50, 20)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildAbilityOverlayButtons() {
|
||||||
|
clearWidgets();
|
||||||
|
if (selectedAbility == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int x = (width - OVERLAY_WIDTH) / 2;
|
||||||
|
int y = (height - OVERLAY_CONTENT_HEIGHT) / 2;
|
||||||
|
boolean unlocked = SkilltreeClientState.isAbilityUnlocked(selectedAbility.getId());
|
||||||
|
if (!unlocked) {
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Unlock"), btn -> {
|
||||||
|
PacketHandler.CHANNEL.sendToServer(
|
||||||
|
new ServerboundUnlockNodePacket(selectedAbility.getId())
|
||||||
|
);
|
||||||
|
selectedAbility = null;
|
||||||
|
clearWidgets();
|
||||||
|
}).pos(x + OVERLAY_WIDTH / 2 - 40, y + OVERLAY_CONTENT_HEIGHT - 20)
|
||||||
|
.size(80, 20)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
addRenderableWidget(Button.builder(Component.literal("Close"), btn -> {
|
||||||
|
selectedAbility = null;
|
||||||
|
clearWidgets();
|
||||||
|
}).pos(x + OVERLAY_WIDTH - 60, y - 5)
|
||||||
|
.size(50, 20)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
private int[] worldToScreen(float wx, float wy) {
|
private int[] worldToScreen(float wx, float wy) {
|
||||||
double sx = width / 2.0 + panX + wx * zoom;
|
double sx = width / 2.0 + panX + wx * zoom;
|
||||||
double sy = height / 2.0 + panY - wy * zoom;
|
double sy = height / 2.0 + panY - wy * zoom;
|
||||||
@@ -199,83 +376,51 @@ public class SkilltreeScreen extends Screen {
|
|||||||
private SkillNode findNodeAt(double mouseX, double mouseY) {
|
private SkillNode findNodeAt(double mouseX, double mouseY) {
|
||||||
for (SkillNode node : SkillNodeRegistry.all()) {
|
for (SkillNode node : SkillNodeRegistry.all()) {
|
||||||
int[] pos = worldToScreen(node.getX(), node.getY());
|
int[] pos = worldToScreen(node.getX(), node.getY());
|
||||||
int r = 12;
|
if (isPointInCircle(mouseX, mouseY, pos[0], pos[1], NODE_RADIUS)) {
|
||||||
if (mouseX >= pos[0] - r && mouseX <= pos[0] + r &&
|
|
||||||
mouseY >= pos[1] - r && mouseY <= pos[1] + r) {
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderOverlay(GuiGraphics gfx, SkillNode node) {
|
private AbilityNode findAbilityAt(double mouseX, double mouseY) {
|
||||||
boolean lockedByExclusivity = node.getExclusiveWith().stream()
|
for (AbilityNode ability : AbilityNodeRegistry.all()) {
|
||||||
.anyMatch(SkilltreeClientState::isUnlocked);
|
int[] pos = worldToScreen(ability.getX(), ability.getY());
|
||||||
if (lockedByExclusivity) return;
|
if (isPointInCircle(mouseX, mouseY, pos[0], pos[1], ABILITY_RADIUS)) {
|
||||||
int w = 240;
|
return ability;
|
||||||
int h = 180;
|
}
|
||||||
int x = (width - w) / 2;
|
|
||||||
int y = (height - h) / 2;
|
|
||||||
gfx.fill(0, 0, width, height, 0x88000000);
|
|
||||||
gfx.fill(x, y, x + w, y + h, 0xFF222222);
|
|
||||||
gfx.drawCenteredString(font, node.getTitle(), x + w / 2, y + 10, 0xFFFFFF);
|
|
||||||
gfx.drawWordWrap(font, node.getDescription(), x + 10, y + 30, w - 20, 0xDDDDDD);
|
|
||||||
final int[] textY = {y + 95};
|
|
||||||
gfx.drawString(font,
|
|
||||||
Component.literal("Cost: " + node.getCost() + " skill points")
|
|
||||||
.withStyle(ChatFormatting.AQUA),
|
|
||||||
x + 10, textY[0],
|
|
||||||
0xAAAAAA);
|
|
||||||
textY[0] += 15;
|
|
||||||
if (!node.getPrereqAttributes().isEmpty()) {
|
|
||||||
gfx.drawString(font,
|
|
||||||
Component.literal("Requirements:")
|
|
||||||
.withStyle(ChatFormatting.GOLD),
|
|
||||||
x + 10, textY[0],
|
|
||||||
0xFFFFFF);
|
|
||||||
textY[0] += 12;
|
|
||||||
Minecraft.getInstance().player.getCapability(PlayerStatsProvider.PLAYER_STATS)
|
|
||||||
.ifPresent(stats -> {
|
|
||||||
for (var entry : node.getPrereqAttributes().entrySet()) {
|
|
||||||
String attr = entry.getKey();
|
|
||||||
int required = entry.getValue();
|
|
||||||
int current = stats.getAttributeLevel(attr);
|
|
||||||
boolean ok = current >= required;
|
|
||||||
Component line = Component.literal(
|
|
||||||
" - " + attr + ": " + current + "/" + required
|
|
||||||
).withStyle(ok ? ChatFormatting.GREEN : ChatFormatting.RED);
|
|
||||||
gfx.drawString(font, line, x + 10, textY[0], 0xFFFFFF);
|
|
||||||
textY[0] += 12;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void rebuildOverlayButtons() {
|
private boolean isPointInCircle(double mx, double my, int cx, int cy, int radius) {
|
||||||
clearWidgets();
|
return mx >= cx - radius && mx <= cx + radius &&
|
||||||
if (selectedNode == null) return;
|
my >= cy - radius && my <= cy + radius;
|
||||||
int w = 240;
|
}
|
||||||
int h = 160;
|
|
||||||
int x = (width - w) / 2;
|
private boolean isLockedByExclusivity(SkillNode node) {
|
||||||
int y = (height - h) / 2;
|
return node.getExclusiveWith().stream().anyMatch(SkilltreeClientState::isUnlocked);
|
||||||
boolean alreadyUnlocked = SkilltreeClientState.isUnlocked(selectedNode.getId());
|
}
|
||||||
if (!alreadyUnlocked) {
|
|
||||||
addRenderableWidget(Button.builder(Component.literal("Unlock"), btn -> {
|
private int getNodeColor(SkillNode node) {
|
||||||
PacketHandler.CHANNEL.sendToServer(
|
boolean unlocked = SkilltreeClientState.isUnlocked(node.getId());
|
||||||
new ServerboundUnlockNodePacket(selectedNode.getId())
|
boolean lockedByExclusivity = isLockedByExclusivity(node);
|
||||||
);
|
int color;
|
||||||
selectedNode = null;
|
if (lockedByExclusivity) {
|
||||||
clearWidgets();
|
color = 0xFFCC3333;
|
||||||
}).pos(x + w / 2 - 40, y + h - 20).size(80, 20).build());
|
} else if (node == selectedNode) {
|
||||||
|
color = 0xFF999933;
|
||||||
|
} else if (node == hoveredNode) {
|
||||||
|
color = 0xFF777777;
|
||||||
|
} else if (unlocked) {
|
||||||
|
color = 0xFF44CC44;
|
||||||
|
} else {
|
||||||
|
color = 0xFF444444;
|
||||||
}
|
}
|
||||||
addRenderableWidget(Button.builder(Component.literal("Close"), btn -> {
|
return color;
|
||||||
selectedNode = null;
|
|
||||||
clearWidgets();
|
|
||||||
}).pos(x + w - 60, y - 5).size(50, 20).build());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawLine(GuiGraphics gfx, int x1, int y1, int x2, int y2, int color) {
|
private void drawLine(GuiGraphics gfx, int x1, int y1, int x2, int y2, int color) {
|
||||||
float thickness = 2.0f;
|
|
||||||
float dx = x2 - x1;
|
float dx = x2 - x1;
|
||||||
float dy = y2 - y1;
|
float dy = y2 - y1;
|
||||||
float length = (float) Math.sqrt(dx * dx + dy * dy);
|
float length = (float) Math.sqrt(dx * dx + dy * dy);
|
||||||
@@ -283,11 +428,13 @@ public class SkilltreeScreen extends Screen {
|
|||||||
gfx.pose().pushPose();
|
gfx.pose().pushPose();
|
||||||
gfx.pose().translate(x1, y1, 0);
|
gfx.pose().translate(x1, y1, 0);
|
||||||
gfx.pose().mulPose(Axis.ZP.rotation(angle));
|
gfx.pose().mulPose(Axis.ZP.rotation(angle));
|
||||||
gfx.fill(0,
|
gfx.fill(
|
||||||
(int) (-thickness / 2f),
|
0,
|
||||||
|
(int) (-LINE_THICKNESS / 2f),
|
||||||
(int) length,
|
(int) length,
|
||||||
(int) (thickness / 2f),
|
(int) (LINE_THICKNESS / 2f),
|
||||||
color);
|
color
|
||||||
|
);
|
||||||
gfx.pose().popPose();
|
gfx.pose().popPose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ public class SkillEnginePlayerEvents {
|
|||||||
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> {
|
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> {
|
||||||
PacketHandler.CHANNEL.send(
|
PacketHandler.CHANNEL.send(
|
||||||
PacketDistributor.PLAYER.with(() -> player),
|
PacketDistributor.PLAYER.with(() -> player),
|
||||||
new ClientboundSyncSkillsPacket(data.getUnlockedNodes(), data.getSkillPoints())
|
new ClientboundSyncSkillsPacket(
|
||||||
|
data.getUnlockedNodes(),
|
||||||
|
data.getUnlockedAbilities(),
|
||||||
|
data.getSkillPoints(),
|
||||||
|
data.getAbilitySlots()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.fretux.skillengine.network;
|
package net.fretux.skillengine.network;
|
||||||
|
|
||||||
|
import net.fretux.skillengine.skilltree.AbilityNodeRegistry;
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
import net.minecraft.network.FriendlyByteBuf;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraftforge.network.NetworkEvent;
|
import net.minecraftforge.network.NetworkEvent;
|
||||||
@@ -30,8 +31,10 @@ public class ClientboundNodeUnlockedPacket {
|
|||||||
|
|
||||||
public static void handle(ClientboundNodeUnlockedPacket msg, Supplier<NetworkEvent.Context> ctx) {
|
public static void handle(ClientboundNodeUnlockedPacket msg, Supplier<NetworkEvent.Context> ctx) {
|
||||||
ctx.get().enqueueWork(() -> {
|
ctx.get().enqueueWork(() -> {
|
||||||
// Update client-side unlocked set and points immediately so UI can reflect changes this frame
|
if (AbilityNodeRegistry.get(msg.id) != null)
|
||||||
SkilltreeClientState.unlockNode(msg.id);
|
SkilltreeClientState.unlockAbility(msg.id);
|
||||||
|
else
|
||||||
|
SkilltreeClientState.unlockNode(msg.id);
|
||||||
SkilltreeClientState.setCurrentSkillPoints(msg.newSkillPoints);
|
SkilltreeClientState.setCurrentSkillPoints(msg.newSkillPoints);
|
||||||
});
|
});
|
||||||
ctx.get().setPacketHandled(true);
|
ctx.get().setPacketHandled(true);
|
||||||
|
|||||||
@@ -12,35 +12,61 @@ import java.util.function.Supplier;
|
|||||||
public class ClientboundSyncSkillsPacket {
|
public class ClientboundSyncSkillsPacket {
|
||||||
|
|
||||||
private final Set<ResourceLocation> unlocked;
|
private final Set<ResourceLocation> unlocked;
|
||||||
|
private final Set<ResourceLocation> unlockedAbilities;
|
||||||
private final int skillPoints;
|
private final int skillPoints;
|
||||||
|
private final ResourceLocation[] abilitySlots;
|
||||||
|
|
||||||
public ClientboundSyncSkillsPacket(Set<ResourceLocation> unlocked, int skillPoints) {
|
public ClientboundSyncSkillsPacket(Set<ResourceLocation> unlocked,
|
||||||
|
Set<ResourceLocation> unlockedAbilities,
|
||||||
|
int skillPoints,
|
||||||
|
ResourceLocation[] abilitySlots) {
|
||||||
this.unlocked = unlocked;
|
this.unlocked = unlocked;
|
||||||
|
this.unlockedAbilities = unlockedAbilities;
|
||||||
this.skillPoints = skillPoints;
|
this.skillPoints = skillPoints;
|
||||||
|
this.abilitySlots = abilitySlots;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void encode(ClientboundSyncSkillsPacket msg, FriendlyByteBuf buf) {
|
public static void encode(ClientboundSyncSkillsPacket msg, FriendlyByteBuf buf) {
|
||||||
buf.writeInt(msg.unlocked.size());
|
buf.writeInt(msg.unlocked.size());
|
||||||
for (ResourceLocation id : msg.unlocked) {
|
for (ResourceLocation id : msg.unlocked) buf.writeResourceLocation(id);
|
||||||
buf.writeResourceLocation(id);
|
buf.writeInt(msg.unlockedAbilities.size());
|
||||||
}
|
for (ResourceLocation id : msg.unlockedAbilities) buf.writeResourceLocation(id);
|
||||||
buf.writeInt(msg.skillPoints);
|
buf.writeInt(msg.skillPoints);
|
||||||
|
// ability slots (nullable entries)
|
||||||
|
int count = msg.abilitySlots != null ? msg.abilitySlots.length : 0;
|
||||||
|
buf.writeInt(count);
|
||||||
|
if (count > 0) {
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
ResourceLocation id = msg.abilitySlots[i];
|
||||||
|
buf.writeBoolean(id != null);
|
||||||
|
if (id != null) buf.writeResourceLocation(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ClientboundSyncSkillsPacket decode(FriendlyByteBuf buf) {
|
public static ClientboundSyncSkillsPacket decode(FriendlyByteBuf buf) {
|
||||||
int size = buf.readInt();
|
int size = buf.readInt();
|
||||||
Set<ResourceLocation> result = new HashSet<>();
|
Set<ResourceLocation> unlocked = new HashSet<>();
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) unlocked.add(buf.readResourceLocation());
|
||||||
result.add(buf.readResourceLocation());
|
int abilitySize = buf.readInt();
|
||||||
}
|
Set<ResourceLocation> unlockedAbilities = new HashSet<>();
|
||||||
|
for (int i = 0; i < abilitySize; i++) unlockedAbilities.add(buf.readResourceLocation());
|
||||||
int points = buf.readInt();
|
int points = buf.readInt();
|
||||||
return new ClientboundSyncSkillsPacket(result, points);
|
int slotsCount = buf.readInt();
|
||||||
|
ResourceLocation[] slots = new ResourceLocation[Math.max(slotsCount, 0)];
|
||||||
|
for (int i = 0; i < slots.length; i++) {
|
||||||
|
boolean present = buf.readBoolean();
|
||||||
|
slots[i] = present ? buf.readResourceLocation() : null;
|
||||||
|
}
|
||||||
|
return new ClientboundSyncSkillsPacket(unlocked, unlockedAbilities, points, slots);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void handle(ClientboundSyncSkillsPacket msg, Supplier<NetworkEvent.Context> ctx) {
|
public static void handle(ClientboundSyncSkillsPacket msg, Supplier<NetworkEvent.Context> ctx) {
|
||||||
ctx.get().enqueueWork(() -> {
|
ctx.get().enqueueWork(() -> {
|
||||||
SkilltreeClientState.setUnlocked(msg.unlocked);
|
SkilltreeClientState.setUnlocked(msg.unlocked);
|
||||||
|
SkilltreeClientState.setUnlockedAbilities(msg.unlockedAbilities);
|
||||||
SkilltreeClientState.setCurrentSkillPoints(msg.skillPoints);
|
SkilltreeClientState.setCurrentSkillPoints(msg.skillPoints);
|
||||||
|
SkilltreeClientState.setAbilitySlots(msg.abilitySlots);
|
||||||
});
|
});
|
||||||
ctx.get().setPacketHandled(true);
|
ctx.get().setPacketHandled(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,12 +37,22 @@ public class PacketHandler {
|
|||||||
.decoder(ClientboundSyncSkillsPacket::decode)
|
.decoder(ClientboundSyncSkillsPacket::decode)
|
||||||
.consumerMainThread(ClientboundSyncSkillsPacket::handle)
|
.consumerMainThread(ClientboundSyncSkillsPacket::handle)
|
||||||
.add();
|
.add();
|
||||||
|
CHANNEL.messageBuilder(ServerboundBindAbilityPacket.class, id++, NetworkDirection.PLAY_TO_SERVER)
|
||||||
|
.encoder(ServerboundBindAbilityPacket::encode)
|
||||||
|
.decoder(ServerboundBindAbilityPacket::decode)
|
||||||
|
.consumerMainThread(ServerboundBindAbilityPacket::handle)
|
||||||
|
.add();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void syncSkillsTo(ServerPlayer player) {
|
public static void syncSkillsTo(ServerPlayer player) {
|
||||||
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> {
|
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> {
|
||||||
ClientboundSyncSkillsPacket packet =
|
ClientboundSyncSkillsPacket packet =
|
||||||
new ClientboundSyncSkillsPacket(data.getUnlockedNodes(), data.getSkillPoints());
|
new ClientboundSyncSkillsPacket(
|
||||||
|
data.getUnlockedNodes(),
|
||||||
|
data.getUnlockedAbilities(),
|
||||||
|
data.getSkillPoints(),
|
||||||
|
data.getAbilitySlots()
|
||||||
|
);
|
||||||
CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), packet);
|
CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), packet);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package net.fretux.skillengine.network;
|
||||||
|
|
||||||
|
import net.fretux.skillengine.capability.SkillEngineCapabilities;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.fretux.skillengine.network.PacketHandler;
|
||||||
|
import net.minecraft.network.FriendlyByteBuf;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraftforge.network.NetworkEvent;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class ServerboundBindAbilityPacket {
|
||||||
|
|
||||||
|
private final ResourceLocation abilityId;
|
||||||
|
private final int slot;
|
||||||
|
|
||||||
|
public ServerboundBindAbilityPacket(ResourceLocation id, int slot) {
|
||||||
|
this.abilityId = id;
|
||||||
|
this.slot = slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void encode(ServerboundBindAbilityPacket msg, FriendlyByteBuf buf) {
|
||||||
|
buf.writeResourceLocation(msg.abilityId);
|
||||||
|
buf.writeInt(msg.slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerboundBindAbilityPacket decode(FriendlyByteBuf buf) {
|
||||||
|
return new ServerboundBindAbilityPacket(buf.readResourceLocation(), buf.readInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handle(ServerboundBindAbilityPacket msg, Supplier<NetworkEvent.Context> ctx) {
|
||||||
|
ctx.get().enqueueWork(() -> {
|
||||||
|
ServerPlayer player = ctx.get().getSender();
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data ->
|
||||||
|
data.bindAbility(msg.slot, msg.abilityId)
|
||||||
|
);
|
||||||
|
// Sync back to client so UI reflects updated bindings
|
||||||
|
PacketHandler.syncSkillsTo(player);
|
||||||
|
});
|
||||||
|
ctx.get().setPacketHandled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,7 @@ package net.fretux.skillengine.network;
|
|||||||
|
|
||||||
import net.fretux.skillengine.SkillEngine;
|
import net.fretux.skillengine.SkillEngine;
|
||||||
import net.fretux.skillengine.capability.SkillEngineCapabilities;
|
import net.fretux.skillengine.capability.SkillEngineCapabilities;
|
||||||
import net.fretux.skillengine.skilltree.SkillLogic;
|
import net.fretux.skillengine.skilltree.*;
|
||||||
import net.fretux.skillengine.skilltree.SkillNode;
|
|
||||||
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
|
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
import net.minecraft.network.FriendlyByteBuf;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
@@ -37,27 +35,45 @@ public class ServerboundUnlockNodePacket {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SkillNode node = SkillNodeRegistry.get(msg.nodeId);
|
SkillNode node = SkillNodeRegistry.get(msg.nodeId);
|
||||||
if (node == null) {
|
AbilityNode ability = AbilityNodeRegistry.get(msg.nodeId);
|
||||||
SkillEngine.LOGGER.warn("Unlock packet for unknown node {}", msg.nodeId);
|
if (node == null && ability == null) {
|
||||||
|
SkillEngine.LOGGER.warn("Unlock packet for unknown node or ability {}", msg.nodeId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> {
|
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> {
|
||||||
SkillEngine.LOGGER.info("Received unlock request for {} from {} (points={})",
|
if (node != null) {
|
||||||
msg.nodeId, player.getGameProfile().getName(), data.getSkillPoints());
|
SkillEngine.LOGGER.info("Received skill unlock request for {} from {} (points={})",
|
||||||
|
msg.nodeId, player.getGameProfile().getName(), data.getSkillPoints());
|
||||||
|
if (SkillLogic.canUnlock(data, player, node)) {
|
||||||
|
data.unlockNode(node);
|
||||||
|
|
||||||
if (SkillLogic.canUnlock(data, player, node)) {
|
PacketHandler.CHANNEL.send(
|
||||||
data.unlockNode(node);
|
PacketDistributor.PLAYER.with(() -> player),
|
||||||
SkillEngine.LOGGER.info("Node {} unlocked for {}", msg.nodeId, player.getGameProfile().getName());
|
new ClientboundNodeUnlockedPacket(node.getId(), data.getSkillPoints())
|
||||||
PacketHandler.CHANNEL.send(
|
);
|
||||||
PacketDistributor.PLAYER.with(() -> player),
|
} else {
|
||||||
new ClientboundNodeUnlockedPacket(node.getId(), data.getSkillPoints())
|
SkillEngine.LOGGER.info("Cannot unlock skill node {} for {}",
|
||||||
);
|
msg.nodeId, player.getGameProfile().getName());
|
||||||
} else {
|
}
|
||||||
SkillEngine.LOGGER.info("Cannot unlock node {} for {} (canUnlock=false)",
|
return;
|
||||||
msg.nodeId, player.getGameProfile().getName());
|
}
|
||||||
|
if (ability != null) {
|
||||||
|
SkillEngine.LOGGER.info("Received ability unlock request for {} from {} (points={})",
|
||||||
|
msg.nodeId, player.getGameProfile().getName(), data.getSkillPoints());
|
||||||
|
if (!data.isAbilityUnlocked(ability.getId())) {
|
||||||
|
data.unlockAbility(ability);
|
||||||
|
PacketHandler.CHANNEL.send(
|
||||||
|
PacketDistributor.PLAYER.with(() -> player),
|
||||||
|
new ClientboundNodeUnlockedPacket(ability.getId(), data.getSkillPoints())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
SkillEngine.LOGGER.info("Ability {} already unlocked for {}",
|
||||||
|
msg.nodeId, player.getGameProfile().getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.get().setPacketHandled(true);
|
ctx.get().setPacketHandled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package net.fretux.skillengine.skilltree;
|
||||||
|
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AbilityNode {
|
||||||
|
|
||||||
|
private final ResourceLocation id;
|
||||||
|
private final Component title;
|
||||||
|
private final Component description;
|
||||||
|
private final float x, y;
|
||||||
|
private final List<ResourceLocation> links;
|
||||||
|
private final List<ResourceLocation> tags;
|
||||||
|
private final ResourceLocation icon;
|
||||||
|
private final int cooldown;
|
||||||
|
|
||||||
|
public AbilityNode(ResourceLocation id,
|
||||||
|
Component title,
|
||||||
|
Component description,
|
||||||
|
float x, float y,
|
||||||
|
List<ResourceLocation> links,
|
||||||
|
List<ResourceLocation> tags,
|
||||||
|
ResourceLocation icon,
|
||||||
|
int cooldown) {
|
||||||
|
|
||||||
|
this.id = id;
|
||||||
|
this.title = title;
|
||||||
|
this.description = description;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.links = List.copyOf(links);
|
||||||
|
this.tags = List.copyOf(tags);
|
||||||
|
this.icon = icon;
|
||||||
|
this.cooldown = cooldown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceLocation getId() { return id; }
|
||||||
|
public Component getTitle() { return title; }
|
||||||
|
public Component getDescription() { return description; }
|
||||||
|
public float getX() { return x; }
|
||||||
|
public float getY() { return y; }
|
||||||
|
public List<ResourceLocation> getLinks() { return links; }
|
||||||
|
public List<ResourceLocation> getTags() { return tags; }
|
||||||
|
public ResourceLocation getIcon() { return icon; }
|
||||||
|
public int getCooldown() { return cooldown; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package net.fretux.skillengine.skilltree;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import net.fretux.skillengine.SkillEngine;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.packs.resources.ResourceManager;
|
||||||
|
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
|
||||||
|
import net.minecraft.util.profiling.ProfilerFiller;
|
||||||
|
import net.minecraftforge.event.AddReloadListenerEvent;
|
||||||
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Mod.EventBusSubscriber(modid = SkillEngine.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
|
||||||
|
public class AbilityNodeLoader extends SimpleJsonResourceReloadListener {
|
||||||
|
|
||||||
|
private static final Gson GSON = new GsonBuilder().create();
|
||||||
|
|
||||||
|
public AbilityNodeLoader() {
|
||||||
|
super(GSON, "abilitynodes");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onAddReloadListeners(AddReloadListenerEvent event) {
|
||||||
|
event.addListener(new AbilityNodeLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void apply(Map<ResourceLocation, JsonElement> jsons,
|
||||||
|
ResourceManager resourceManager,
|
||||||
|
ProfilerFiller profiler) {
|
||||||
|
|
||||||
|
SkillEngine.LOGGER.info("Loading ability nodes...");
|
||||||
|
|
||||||
|
AbilityNodeRegistry.clear();
|
||||||
|
|
||||||
|
jsons.forEach((id, json) -> {
|
||||||
|
JsonObject obj = json.getAsJsonObject();
|
||||||
|
|
||||||
|
Component title = Component.literal(obj.get("title").getAsString());
|
||||||
|
Component description = Component.literal(obj.get("description").getAsString());
|
||||||
|
JsonObject pos = obj.getAsJsonObject("position");
|
||||||
|
float x = pos.get("x").getAsFloat();
|
||||||
|
float y = pos.get("y").getAsFloat();
|
||||||
|
|
||||||
|
List<ResourceLocation> links = new ArrayList<>();
|
||||||
|
obj.getAsJsonArray("links").forEach(e ->
|
||||||
|
links.add(new ResourceLocation(e.getAsString())));
|
||||||
|
|
||||||
|
List<ResourceLocation> tags = new ArrayList<>();
|
||||||
|
obj.getAsJsonArray("tags").forEach(e ->
|
||||||
|
tags.add(new ResourceLocation(e.getAsString())));
|
||||||
|
|
||||||
|
ResourceLocation icon = new ResourceLocation(obj.get("icon").getAsString());
|
||||||
|
|
||||||
|
int cooldown = obj.has("cooldown") ? obj.get("cooldown").getAsInt() : 0;
|
||||||
|
|
||||||
|
AbilityNode node = new AbilityNode(id, title, description, x, y, links, tags, icon, cooldown);
|
||||||
|
AbilityNodeRegistry.put(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
SkillEngine.LOGGER.info("Loaded {} active ability nodes", AbilityNodeRegistry.all().size());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package net.fretux.skillengine.skilltree;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class AbilityNodeRegistry {
|
||||||
|
|
||||||
|
private static final Map<ResourceLocation, AbilityNode> ABILITIES = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
public static void clear() {
|
||||||
|
ABILITIES.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void put(AbilityNode node) {
|
||||||
|
ABILITIES.put(node.getId(), node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AbilityNode get(ResourceLocation id) {
|
||||||
|
return ABILITIES.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Collection<AbilityNode> all() {
|
||||||
|
return ABILITIES.values();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"id": "skillengine:berserk",
|
||||||
|
"title": "Berserk",
|
||||||
|
"description": "Enter a frenzied state. Increased damage and speed.",
|
||||||
|
"position": {
|
||||||
|
"x": 110,
|
||||||
|
"y": 120
|
||||||
|
},
|
||||||
|
"links": [
|
||||||
|
"skillengine:strength"
|
||||||
|
],
|
||||||
|
"tags": ["skillengine:berserk"],
|
||||||
|
"icon": "minecraft:textures/item/blaze_powder.png",
|
||||||
|
"cooldown": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"id": "skillengine:strength",
|
||||||
|
"title": "Strength",
|
||||||
|
"description": "Stronk",
|
||||||
|
"cost": 0,
|
||||||
|
"position": {
|
||||||
|
"x": 45,
|
||||||
|
"y": 45
|
||||||
|
},
|
||||||
|
"links": ["skillengine:welcome"],
|
||||||
|
"tags": [
|
||||||
|
"skillengine:strength",
|
||||||
|
"skillengine:stronk"
|
||||||
|
],
|
||||||
|
"icons": "minecraft:textures/item/book.png",
|
||||||
|
"prerequisites": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user