Initial commit

This commit is contained in:
2025-11-24 23:10:25 +01:00
commit 6f06da5f31
41 changed files with 3443 additions and 0 deletions
@@ -0,0 +1,23 @@
package net.fretux.skillengine;
import com.mojang.logging.LogUtils;
import net.fretux.skillengine.capability.SkillEngineCapabilities;
import net.fretux.skillengine.network.PacketHandler;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import org.slf4j.Logger;
@Mod(SkillEngine.MODID)
public class SkillEngine {
public static final String MODID = "skillengine";
public static final Logger LOGGER = LogUtils.getLogger();
public SkillEngine() {
IEventBus modBus = FMLJavaModLoadingContext.get().getModEventBus();
PacketHandler.register();
}
}
@@ -0,0 +1,86 @@
package net.fretux.skillengine.capability;
import net.fretux.skillengine.skilltree.SkillNode;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.resources.ResourceLocation;
import java.util.HashSet;
import java.util.Set;
public class PlayerSkillData {
private int skillPoints;
private final Set<ResourceLocation> unlockedNodes = new HashSet<>();
private final Set<ResourceLocation> activeTags = new HashSet<>();
public int getSkillPoints() { return skillPoints; }
public void addSkillPoints(int amount) { skillPoints += amount; }
public boolean isUnlocked(ResourceLocation nodeId) {
return unlockedNodes.contains(nodeId);
}
public boolean hasTag(ResourceLocation tag) {
return activeTags.contains(tag);
}
public Set<ResourceLocation> getActiveTags() {
return activeTags;
}
public void unlockNode(SkillNode node) {
if (unlockedNodes.add(node.getId())) {
activeTags.addAll(node.getTags());
skillPoints -= node.getCost();
}
}
/* ---------- NBT ---------- */
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
tag.putInt("SkillPoints", skillPoints);
ListTag unlockedList = new ListTag();
for (ResourceLocation id : unlockedNodes) {
CompoundTag t = new CompoundTag();
t.putString("Id", id.toString());
unlockedList.add(t);
}
tag.put("Unlocked", unlockedList);
ListTag tagList = new ListTag();
for (ResourceLocation id : activeTags) {
CompoundTag t = new CompoundTag();
t.putString("Tag", id.toString());
tagList.add(t);
}
tag.put("Tags", tagList);
return tag;
}
public void load(CompoundTag tag) {
skillPoints = tag.getInt("SkillPoints");
unlockedNodes.clear();
activeTags.clear();
ListTag unlockedList = tag.getList("Unlocked", Tag.TAG_COMPOUND);
for (Tag t : unlockedList) {
CompoundTag c = (CompoundTag) t;
unlockedNodes.add(new ResourceLocation(c.getString("Id")));
}
ListTag tagList = tag.getList("Tags", Tag.TAG_COMPOUND);
for (Tag t : tagList) {
CompoundTag c = (CompoundTag) t;
activeTags.add(new ResourceLocation(c.getString("Tag")));
}
}
public Set<ResourceLocation> getUnlockedNodes() {
return unlockedNodes;
}
}
@@ -0,0 +1,32 @@
package net.fretux.skillengine.capability;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.common.capabilities.*;
import net.minecraftforge.common.util.LazyOptional;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PlayerSkillProvider implements ICapabilityProvider, ICapabilitySerializable<CompoundTag> {
public static final ResourceLocation ID = new ResourceLocation("skillengine", "player_skill_data");
private final PlayerSkillData backend = new PlayerSkillData();
private final LazyOptional<PlayerSkillData> optional = LazyOptional.of(() -> backend);
@Override
public @NotNull <T> LazyOptional<T> getCapability(@NotNull Capability<T> cap, @Nullable Direction side) {
return cap == SkillEngineCapabilities.PLAYER_SKILLS ? optional.cast() : LazyOptional.empty();
}
@Override
public CompoundTag serializeNBT() {
return backend.save();
}
@Override
public void deserializeNBT(CompoundTag nbt) {
backend.load(nbt);
}
}
@@ -0,0 +1,30 @@
package net.fretux.skillengine.capability;
import net.fretux.skillengine.SkillEngine;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.CapabilityManager;
import net.minecraftforge.common.capabilities.CapabilityToken;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(modid = SkillEngine.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public class SkillEngineCapabilities {
public static final Capability<PlayerSkillData> PLAYER_SKILLS =
CapabilityManager.get(new CapabilityToken<>() {});
@SubscribeEvent
public static void onAttachCapabilities(AttachCapabilitiesEvent<Entity> event) {
if (event.getObject() instanceof Player player) {
SkillEngine.LOGGER.info("Attaching PlayerSkillData capability");
event.addCapability(
new ResourceLocation(SkillEngine.MODID, "player_skill_data"),
new PlayerSkillProvider()
);
}
}
}
@@ -0,0 +1,23 @@
package net.fretux.skillengine.client;
import net.fretux.skillengine.SkillEngine;
import net.minecraft.client.Minecraft;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.InputEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(
modid = SkillEngine.MODID,
value = Dist.CLIENT
)
public class ClientEvents {
@SubscribeEvent
public static void onKeyInput(InputEvent.Key event) {
if (ClientKeybinds.OPEN_SKILLTREE != null &&
ClientKeybinds.OPEN_SKILLTREE.consumeClick()) {
Minecraft.getInstance().setScreen(new SkilltreeScreen());
}
}
}
@@ -0,0 +1,28 @@
package net.fretux.skillengine.client;
import net.fretux.skillengine.SkillEngine;
import net.minecraft.client.KeyMapping;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.RegisterKeyMappingsEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.lwjgl.glfw.GLFW;
@Mod.EventBusSubscriber(
modid = SkillEngine.MODID,
bus = Mod.EventBusSubscriber.Bus.MOD,
value = Dist.CLIENT
)
public class ClientKeybinds {
public static KeyMapping OPEN_SKILLTREE;
@SubscribeEvent
public static void registerKeys(RegisterKeyMappingsEvent event) {
OPEN_SKILLTREE = new KeyMapping(
"key.skillengine.openskilltree",
GLFW.GLFW_KEY_N,
"key.categories.ui"
);
event.register(OPEN_SKILLTREE);
}
}
@@ -0,0 +1,44 @@
package net.fretux.skillengine.client;
import net.fretux.skillengine.SkillEngine;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.toasts.SystemToast;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvents;
import java.util.HashSet;
import java.util.Set;
public class SkilltreeClientState {
private static final Set<ResourceLocation> unlocked = new HashSet<>();
public static void unlockNode(ResourceLocation id) {
SkillEngine.LOGGER.info("[CLIENT] Unlocking node: {}", id);
unlocked.add(id);
Minecraft mc = Minecraft.getInstance();
if (mc.player != null) {
mc.player.playSound(SoundEvents.EXPERIENCE_ORB_PICKUP, 1f, 1.2f);
}
mc.getToasts().addToast(
SystemToast.multiline(
mc,
SystemToast.SystemToastIds.TUTORIAL_HINT,
Component.literal("Node Unlocked"),
Component.literal(id.toString())
)
);
}
public static boolean isUnlocked(ResourceLocation id) {
return unlocked.contains(id);
}
public static void setUnlocked(Set<ResourceLocation> ids) {
unlocked.clear();
unlocked.addAll(ids);
}
}
@@ -0,0 +1,216 @@
package net.fretux.skillengine.client;
import com.mojang.math.Axis;
import net.fretux.skillengine.SkillEngine;
import net.fretux.skillengine.network.PacketHandler;
import net.fretux.skillengine.network.ServerboundUnlockNodePacket;
import net.fretux.skillengine.skilltree.SkillNode;
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
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 net.minecraft.util.Mth;
public class SkilltreeScreen extends Screen {
private float zoom = 1.0f;
private float minZoom = 0.5f;
private float maxZoom = 2.0f;
private double panX = 0;
private double panY = 0;
private boolean dragging = false;
private double lastMouseX, lastMouseY;
private SkillNode hoveredNode = null;
private SkillNode selectedNode = null;
public SkilltreeScreen() {
super(Component.literal("Ascend Skilltree"));
}
@Override
protected void init() {
super.init();
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double delta) {
zoom += delta * 0.1f;
zoom = Mth.clamp(zoom, minZoom, maxZoom);
return true;
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (selectedNode != null) {
return super.mouseClicked(mouseX, mouseY, button);
}
if (button == 0) {
SkillNode clicked = findNodeAt(mouseX, mouseY);
if (clicked != null) {
if (SkilltreeClientState.isUnlocked(clicked.getId())) {
return true;
}
selectedNode = clicked;
rebuildOverlayButtons();
return true;
}
dragging = true;
lastMouseX = mouseX;
lastMouseY = mouseY;
}
return super.mouseClicked(mouseX, mouseY, button);
}
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double dx, double dy) {
// Don't pan while overlay is open
if (selectedNode != null) {
return super.mouseDragged(mouseX, mouseY, button, dx, dy);
}
if (dragging && button == 0) {
panX += dx;
panY += dy;
return true;
}
return super.mouseDragged(mouseX, mouseY, button, dx, dy);
}
@Override
public boolean mouseReleased(double mouseX, double mouseY, int button) {
if (button == 0) {
dragging = false;
}
return super.mouseReleased(mouseX, mouseY, button);
}
@Override
public void render(GuiGraphics gfx, int mouseX, int mouseY, float partialTicks) {
this.renderBackground(gfx);
hoveredNode = findNodeAt(mouseX, mouseY);
drawGraph(gfx);
if (hoveredNode != null && selectedNode == null) {
gfx.renderTooltip(font, hoveredNode.getTitle(), mouseX, mouseY);
}
if (selectedNode != null) {
renderOverlay(gfx, selectedNode);
}
super.render(gfx, mouseX, mouseY, partialTicks);
}
private void drawGraph(GuiGraphics gfx) {
for (SkillNode node : SkillNodeRegistry.all()) {
int[] p1 = worldToScreen(node.getX(), node.getY());
for (ResourceLocation neighborId : node.getLinks()) {
SkillNode neighbor = SkillNodeRegistry.get(neighborId);
if (neighbor == null) continue;
int[] p2 = worldToScreen(neighbor.getX(), neighbor.getY());
boolean bothUnlocked =
SkilltreeClientState.isUnlocked(node.getId()) &&
SkilltreeClientState.isUnlocked(neighbor.getId());
int linkColor = bothUnlocked ? 0xFF88FF88 : 0xFF666666;
drawLine(gfx, p1[0], p1[1], p2[0], p2[1], linkColor);
}
}
for (SkillNode node : SkillNodeRegistry.all()) {
int[] pos = worldToScreen(node.getX(), node.getY());
int r = 12;
boolean unlocked = SkilltreeClientState.isUnlocked(node.getId());
int color = unlocked
? 0xFF44CC44
: 0xFF444444;
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);
gfx.drawString(font,
"",
pos[0] + r + 2,
pos[1] - r,
0xFFFFFFFF);
}
}
private int[] worldToScreen(float wx, float wy) {
double sx = width / 2.0 + panX + wx * zoom;
double sy = height / 2.0 + panY + wy * zoom;
return new int[] { (int) sx, (int) sy };
}
private SkillNode findNodeAt(double mouseX, double mouseY) {
for (SkillNode node : SkillNodeRegistry.all()) {
int[] pos = worldToScreen(node.getX(), node.getY());
int r = 12;
if (mouseX >= pos[0] - r && mouseX <= pos[0] + r &&
mouseY >= pos[1] - r && mouseY <= pos[1] + r) {
return node;
}
}
return null;
}
private void renderOverlay(GuiGraphics gfx, SkillNode node) {
int w = 240;
int h = 160;
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);
gfx.drawString(font,
Component.literal("Cost: " + node.getCost() + " skill points"),
x + 10, y + h - 45, 0xAAAAAA);
}
private void rebuildOverlayButtons() {
clearWidgets();
if (selectedNode == null) return;
int w = 240;
int h = 160;
int x = (width - w) / 2;
int y = (height - h) / 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 + w / 2 - 40, y + h - 30).size(80, 20).build());
}
addRenderableWidget(Button.builder(Component.literal("Close"), btn -> {
selectedNode = null;
clearWidgets();
}).pos(x + w - 60, y + 10).size(50, 20).build());
}
private void drawLine(GuiGraphics gfx, int x1, int y1, int x2, int y2, int color) {
float thickness = 2.0f;
float dx = x2 - x1;
float dy = y2 - y1;
float length = (float) Math.sqrt(dx * dx + dy * dy);
float angle = (float) Math.atan2(dy, dx);
gfx.pose().pushPose();
gfx.pose().translate(x1, y1, 0);
gfx.pose().mulPose(Axis.ZP.rotation(angle));
gfx.fill(0,
(int) (-thickness / 2f),
(int) length,
(int) (thickness / 2f),
color);
gfx.pose().popPose();
}
@Override
public boolean isPauseScreen() {
return false;
}
}
@@ -0,0 +1,71 @@
package net.fretux.skillengine.compat;
import net.fretux.ascend.player.PlayerStats;
import net.fretux.ascend.player.PlayerStatsProvider;
import net.fretux.skillengine.SkillEngine;
import net.fretux.skillengine.capability.PlayerSkillData;
import net.fretux.skillengine.capability.SkillEngineCapabilities;
import net.minecraft.world.entity.player.Player;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Mod.EventBusSubscriber(modid = SkillEngine.MODID)
public class AscendCompatHandler {
private static final int SKILL_POINTS_PER_ASCEND_LEVEL = 5;
private static final Map<UUID, Integer> LAST_ASCEND_LEVEL = new ConcurrentHashMap<>();
private static void initAscendLevel(Player player) {
if (player.level().isClientSide) return;
player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(ascendStats -> {
int currentLevel = ascendStats.getAscendLevel();
LAST_ASCEND_LEVEL.put(player.getUUID(), currentLevel);
});
}
@SubscribeEvent
public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
initAscendLevel(event.getEntity());
}
@SubscribeEvent
public static void onPlayerClone(PlayerEvent.Clone event) {
if (event.getEntity().level().isClientSide) return;
Player newPlayer = event.getEntity();
Player original = event.getOriginal();
int last = LAST_ASCEND_LEVEL.getOrDefault(original.getUUID(), 1);
LAST_ASCEND_LEVEL.put(newPlayer.getUUID(), last);
}
@SubscribeEvent
public static void onPlayerChangedDimension(PlayerEvent.PlayerChangedDimensionEvent event) {
initAscendLevel(event.getEntity());
}
@SubscribeEvent
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
if (event.phase != TickEvent.Phase.END) return;
Player player = event.player;
if (player.level().isClientSide) return;
player.getCapability(PlayerStatsProvider.PLAYER_STATS).ifPresent(ascendStats -> {
int currentLevel = ascendStats.getAscendLevel();
int lastLevel = LAST_ASCEND_LEVEL.getOrDefault(player.getUUID(), currentLevel);
if (currentLevel > lastLevel) {
int levelsGained = currentLevel - lastLevel;
int pointsToGive = levelsGained * SKILL_POINTS_PER_ASCEND_LEVEL;
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(skillData -> {
skillData.addSkillPoints(pointsToGive);
});
}
LAST_ASCEND_LEVEL.put(player.getUUID(), currentLevel);
});
}
}
@@ -0,0 +1,45 @@
package net.fretux.skillengine.events;
import net.fretux.skillengine.SkillEngine;
import net.fretux.skillengine.capability.SkillEngineCapabilities;
import net.fretux.skillengine.network.ClientboundSyncSkillsPacket;
import net.fretux.skillengine.network.PacketHandler;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.network.PacketDistributor;
@Mod.EventBusSubscriber(modid = SkillEngine.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public class SkillEnginePlayerEvents {
private static void sync(ServerPlayer player) {
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> {
PacketHandler.CHANNEL.send(
PacketDistributor.PLAYER.with(() -> player),
new ClientboundSyncSkillsPacket(data.getUnlockedNodes())
);
});
}
@SubscribeEvent
public static void onLogin(PlayerEvent.PlayerLoggedInEvent event) {
if (event.getEntity() instanceof ServerPlayer player) {
sync(player);
}
}
@SubscribeEvent
public static void onRespawn(PlayerEvent.PlayerRespawnEvent event) {
if (event.getEntity() instanceof ServerPlayer player) {
sync(player);
}
}
@SubscribeEvent
public static void onChangeDimension(PlayerEvent.PlayerChangedDimensionEvent event) {
if (event.getEntity() instanceof ServerPlayer player) {
sync(player);
}
}
}
@@ -0,0 +1,33 @@
package net.fretux.skillengine.network;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.network.NetworkEvent;
import net.fretux.skillengine.client.SkilltreeClientState;
import java.util.function.Supplier;
public class ClientboundNodeUnlockedPacket {
private final ResourceLocation id;
public ClientboundNodeUnlockedPacket(ResourceLocation id) {
this.id = id;
}
public static void encode(ClientboundNodeUnlockedPacket msg, FriendlyByteBuf buf) {
buf.writeResourceLocation(msg.id);
}
public static ClientboundNodeUnlockedPacket decode(FriendlyByteBuf buf) {
return new ClientboundNodeUnlockedPacket(buf.readResourceLocation());
}
public static void handle(ClientboundNodeUnlockedPacket msg, Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
SkilltreeClientState.unlockNode(msg.id);
});
ctx.get().setPacketHandled(true);
}
}
@@ -0,0 +1,42 @@
package net.fretux.skillengine.network;
import net.fretux.skillengine.client.SkilltreeClientState;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.network.NetworkEvent;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
public class ClientboundSyncSkillsPacket {
private final Set<ResourceLocation> unlocked;
public ClientboundSyncSkillsPacket(Set<ResourceLocation> unlocked) {
this.unlocked = unlocked;
}
public static void encode(ClientboundSyncSkillsPacket msg, FriendlyByteBuf buf) {
buf.writeInt(msg.unlocked.size());
for (ResourceLocation id : msg.unlocked) {
buf.writeResourceLocation(id);
}
}
public static ClientboundSyncSkillsPacket decode(FriendlyByteBuf buf) {
int size = buf.readInt();
Set<ResourceLocation> result = new HashSet<>();
for (int i = 0; i < size; i++) {
result.add(buf.readResourceLocation());
}
return new ClientboundSyncSkillsPacket(result);
}
public static void handle(ClientboundSyncSkillsPacket msg, Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
SkilltreeClientState.setUnlocked(msg.unlocked);
});
ctx.get().setPacketHandled(true);
}
}
@@ -0,0 +1,38 @@
package net.fretux.skillengine.network;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.network.NetworkDirection;
import net.minecraftforge.network.NetworkRegistry;
import net.fretux.skillengine.SkillEngine;
import net.minecraftforge.network.simple.SimpleChannel;
public class PacketHandler {
private static final String PROTOCOL = "1";
public static final SimpleChannel CHANNEL = NetworkRegistry.ChannelBuilder
.named(new ResourceLocation(SkillEngine.MODID, "network"))
.clientAcceptedVersions(PROTOCOL::equals)
.serverAcceptedVersions(PROTOCOL::equals)
.networkProtocolVersion(() -> PROTOCOL)
.simpleChannel();
private static int id = 0;
public static void register() {
CHANNEL.messageBuilder(ServerboundUnlockNodePacket.class, id++, NetworkDirection.PLAY_TO_SERVER)
.encoder(ServerboundUnlockNodePacket::encode)
.decoder(ServerboundUnlockNodePacket::decode)
.consumerMainThread(ServerboundUnlockNodePacket::handle)
.add();
CHANNEL.messageBuilder(ClientboundNodeUnlockedPacket.class, id++, NetworkDirection.PLAY_TO_CLIENT)
.encoder(ClientboundNodeUnlockedPacket::encode)
.decoder(ClientboundNodeUnlockedPacket::decode)
.consumerMainThread(ClientboundNodeUnlockedPacket::handle)
.add();
CHANNEL.messageBuilder(ClientboundSyncSkillsPacket.class, id++, NetworkDirection.PLAY_TO_CLIENT)
.encoder(ClientboundSyncSkillsPacket::encode)
.decoder(ClientboundSyncSkillsPacket::decode)
.consumerMainThread(ClientboundSyncSkillsPacket::handle)
.add();
}
}
@@ -0,0 +1,66 @@
package net.fretux.skillengine.network;
import net.fretux.skillengine.SkillEngine;
import net.fretux.skillengine.capability.SkillEngineCapabilities;
import net.fretux.skillengine.skilltree.SkillLogic;
import net.fretux.skillengine.skilltree.SkillNode;
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
import net.minecraftforge.network.PacketDistributor;
import java.util.function.Supplier;
public class ServerboundUnlockNodePacket {
private final ResourceLocation nodeId;
public ServerboundUnlockNodePacket(ResourceLocation nodeId) {
this.nodeId = nodeId;
}
public static void encode(ServerboundUnlockNodePacket msg, FriendlyByteBuf buf) {
buf.writeResourceLocation(msg.nodeId);
}
public static ServerboundUnlockNodePacket decode(FriendlyByteBuf buf) {
return new ServerboundUnlockNodePacket(buf.readResourceLocation());
}
public static void handle(ServerboundUnlockNodePacket msg, Supplier<NetworkEvent.Context> ctx) {
ctx.get().enqueueWork(() -> {
ServerPlayer player = ctx.get().getSender();
if (player == null) {
SkillEngine.LOGGER.warn("Unlock packet received but sender was null");
return;
}
SkillNode node = SkillNodeRegistry.get(msg.nodeId);
if (node == null) {
SkillEngine.LOGGER.warn("Unlock packet for unknown node {}", msg.nodeId);
return;
}
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> {
SkillEngine.LOGGER.info("Received unlock request for {} from {} (points={})",
msg.nodeId, player.getGameProfile().getName(), data.getSkillPoints());
if (SkillLogic.canUnlock(data, node)) {
data.unlockNode(node);
SkillEngine.LOGGER.info("Node {} unlocked for {}", msg.nodeId, player.getGameProfile().getName());
PacketHandler.CHANNEL.send(
PacketDistributor.PLAYER.with(() -> player),
new ClientboundNodeUnlockedPacket(node.getId())
);
} else {
SkillEngine.LOGGER.info("Cannot unlock node {} for {} (canUnlock=false)",
msg.nodeId, player.getGameProfile().getName());
}
});
});
ctx.get().setPacketHandled(true);
}
}
@@ -0,0 +1,17 @@
package net.fretux.skillengine.skilltree;
import net.fretux.skillengine.capability.PlayerSkillData;
import net.minecraft.resources.ResourceLocation;
public class SkillLogic {
public static boolean canUnlock(PlayerSkillData data, SkillNode node) {
if (data.isUnlocked(node.getId())) return false;
if (data.getSkillPoints() < node.getCost()) return false;
if (node.isRoot()) return true;
for (ResourceLocation neighborId : node.getLinks()) {
if (data.isUnlocked(neighborId)) return true;
}
return false;
}
}
@@ -0,0 +1,49 @@
package net.fretux.skillengine.skilltree;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import java.util.List;
public class SkillNode {
private final ResourceLocation id;
private final Component title;
private final Component description;
private final int cost;
private final float x;
private final float y;
private final List<ResourceLocation> links;
private final List<ResourceLocation> tags;
public SkillNode(ResourceLocation id,
Component title,
Component description,
int cost,
float x, float y,
List<ResourceLocation> links,
List<ResourceLocation> tags) {
this.id = id;
this.title = title;
this.description = description;
this.cost = cost;
this.x = x;
this.y = y;
this.links = List.copyOf(links);
this.tags = List.copyOf(tags);
}
public ResourceLocation getId() { return id; }
public Component getTitle() { return title; }
public Component getDescription() { return description; }
public int getCost() { return cost; }
public float getX() { return x; }
public float getY() { return y; }
public List<ResourceLocation> getLinks() { return links; }
public List<ResourceLocation> getTags() { return tags; }
public boolean isRoot() {
return links.isEmpty(); // or a dedicated flag if you want later
}
}
@@ -0,0 +1,73 @@
package net.fretux.skillengine.skilltree;
import com.google.gson.*;
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 SkillNodeLoader extends SimpleJsonResourceReloadListener {
private static final Gson GSON = new GsonBuilder().create();
public SkillNodeLoader() {
super(GSON, "skillnodes");
}
@SubscribeEvent
public static void onAddReloadListeners(AddReloadListenerEvent event) {
event.addListener(new SkillNodeLoader());
}
@Override
protected void apply(Map<ResourceLocation, JsonElement> jsons,
ResourceManager resourceManager,
ProfilerFiller profiler) {
SkillEngine.LOGGER.info("Loading skill nodes...");
SkillNodeRegistry.clear();
jsons.forEach((id, element) -> {
JsonObject obj = element.getAsJsonObject();
String title = obj.get("title").getAsString();
String description = obj.get("description").getAsString();
int cost = obj.get("cost").getAsInt();
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())));
SkillNode node = new SkillNode(
id,
Component.literal(title),
Component.literal(description),
cost,
x, y,
links,
tags
);
SkillNodeRegistry.put(node);
});
SkillEngine.LOGGER.info("Loaded {} skill nodes", SkillNodeRegistry.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 SkillNodeRegistry {
private static final Map<ResourceLocation, SkillNode> NODES = new LinkedHashMap<>();
public static void clear() {
NODES.clear();
}
public static void put(SkillNode node) {
NODES.put(node.getId(), node);
}
public static SkillNode get(ResourceLocation id) {
return NODES.get(id);
}
public static Collection<SkillNode> all() {
return NODES.values();
}
}