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();
}
}
+80
View File
@@ -0,0 +1,80 @@
# This is an example mods.toml file. It contains the data relating to the loading mods.
# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
# The overall format is standard TOML format, v0.5.0.
# Note that there are a couple of TOML lists in this file.
# Find more information on toml format here: https://github.com/toml-lang/toml
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
modLoader = "javafml" #mandatory
# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
loaderVersion = "${loader_version_range}" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
license = "${mod_license}"
# A URL to refer people to when problems occur with this mod
#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional
# If your mod is purely client-side and has no multiplayer functionality (be it dedicated servers or Open to LAN),
# set this to true, and Forge will set the correct displayTest for you and skip loading your mod on dedicated servers.
#clientSideOnly=true #optional - defaults to false if absent
# A list of mods - how many allowed here is determined by the individual mod loader
[[mods]] #mandatory
# The modid of the mod
modId = "${mod_id}" #mandatory
# The version number of the mod
version = "${mod_version}" #mandatory
# A display name for the mod
displayName = "${mod_name}" #mandatory
# A URL to query for updates for this mod. See the JSON update specification https://docs.minecraftforge.net/en/latest/misc/updatechecker/
#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
# A URL for the "homepage" for this mod, displayed in the mod UI
#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional
# A file name (in the root of the mod JAR) containing a logo for display
#logoFile="examplemod.png" #optional
# A text field displayed in the mod UI
#credits="" #optional
# A text field displayed in the mod UI
authors = "${mod_authors}" #optional
# Display Test controls the display for your mod in the server connection screen
# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod.
# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod.
# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component.
# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value.
# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself.
#displayTest="MATCH_VERSION" # if nothing is specified, MATCH_VERSION is the default when clientSideOnly=false, otherwise IGNORE_ALL_VERSION when clientSideOnly=true (#optional)
# The description text for the mod (multi line!) (#mandatory)
description = '''${mod_description}'''
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies.${ mod_id }]] #optional
# the modid of the dependency
modId = "forge" #mandatory
# Does this dependency have to exist - if not, ordering below must be specified
mandatory = true #mandatory
# The version range of the dependency
versionRange = "${forge_version_range}" #mandatory
# An ordering relationship for the dependency - BEFORE or AFTER required if the dependency is not mandatory
# BEFORE - This mod is loaded BEFORE the dependency
# AFTER - This mod is loaded AFTER the dependency
ordering = "NONE"
# Side this dependency is applied on - BOTH, CLIENT, or SERVER
side = "BOTH"
# Here's another dependency
[[dependencies.${ mod_id }]]
modId = "minecraft"
mandatory = true
# This version range declares a minimum of the current minecraft version up to but not including the next major version
versionRange = "${minecraft_version_range}"
ordering = "NONE"
side = "BOTH"
[[dependencies.${ mod_id }]]
modId = "ascend"
mandatory = true
versionRange = "[2.0.2,)"
ordering = "AFTER"
side = "BOTH"
# Features are specific properties of the game environment, that you may want to declare you require. This example declares
# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
# stop your mod loading on the server for example.
#[features.${mod_id}]
#openGLVersion="[3.2,)"
@@ -0,0 +1,10 @@
{
"title": "Arcane Spark",
"description": "A faint magical glow flickers.\nGrants a minor magic tag.",
"cost": 1,
"position": { "x": 150.0, "y": -50.0 },
"links": ["skillengine:welcome"],
"tags": [
"skillengine:magic_1"
]
}
@@ -0,0 +1,10 @@
{
"title": "Endurance Training",
"description": "Improves stamina regeneration slightly.",
"cost": 1,
"position": { "x": -300.0, "y": -50.0 },
"links": ["skillengine:strength_1"],
"tags": [
"skillengine:endurance"
]
}
@@ -0,0 +1,10 @@
{
"title": "Path of Resilience",
"description": "Your endurance increases.\nA defensive option.",
"cost": 2,
"position": { "x": -100.0, "y": 300.0 },
"links": ["skillengine:wanderer"],
"tags": [
"skillengine:resilience"
]
}
@@ -0,0 +1,10 @@
{
"title": "Path of Speed",
"description": "Your body learns to move quicker.",
"cost": 2,
"position": { "x": 100.0, "y": 300.0 },
"links": ["skillengine:wanderer"],
"tags": [
"skillengine:speed"
]
}
@@ -0,0 +1,10 @@
{
"title": "Basic Strength",
"description": "You feel your muscles tighten.\nGrants +1 Strength tag.",
"cost": 1,
"position": { "x": -150.0, "y": -50.0 },
"links": ["skillengine:welcome"],
"tags": [
"skillengine:strength_1"
]
}
@@ -0,0 +1,10 @@
{
"title": "Way of the Wanderer",
"description": "A flexible beginner skill.\nOpens alternate paths.",
"cost": 1,
"position": { "x": 0.0, "y": 150.0 },
"links": ["skillengine:welcome"],
"tags": [
"skillengine:wanderer"
]
}
@@ -0,0 +1,11 @@
{
"title": "Welcome to Ascend",
"description": "This is your first skill node.\nClick Unlock to test the system.",
"cost": 0,
"position": { "x": 0.0, "y": 0.0 },
"links": [],
"tags": [
"skillengine:welcome",
"skillengine:starter"
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"pack": {
"description": {
"text": "${mod_id} resources"
},
"pack_format": 15
}
}