Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 696bb716ee | |||
| 743cb506bc | |||
| cef5d40796 | |||
| f578d76142 | |||
| bd4f7f6bc6 | |||
| 27d80b099d | |||
| dd98336501 | |||
| fe40cdeeb1 | |||
| 3cd59f0475 | |||
| 7f5830d409 | |||
| 77fbada438 | |||
| f7332054ea | |||
| 40a30c1737 | |||
| 16ed986576 | |||
| 87a0a2177e | |||
| bf4452f6ac | |||
| 30310b6eb9 | |||
| fda16edcef | |||
| 2f93b27e7e | |||
| f2aa1e82cc | |||
| 3d54b3ffc1 | |||
| c8c2119fb2 | |||
| 66f7f76e01 | |||
| 0386027e99 | |||
| 089bcb4917 | |||
| 62325b5b34 | |||
| 56efa4d814 | |||
| 1a510253c6 | |||
| bffcafeebd | |||
| 4e33002ddb | |||
| 48eec87343 | |||
| a88364d190 | |||
| ac334ccd6d | |||
| 573e505e9f | |||
| 14e4b246cf | |||
| 4e8a868615 | |||
| 882026b4a3 | |||
| 8926aa7095 | |||
| c6cf504f67 | |||
| d4ad08a35a | |||
| 9ce984a54a | |||
| bb8bc7b479 | |||
| 555e166225 | |||
| 96ea5977e6 | |||
| d0755c893f | |||
| 9ff5d9f828 | |||
| 429bac1903 | |||
| 8bfca217aa | |||
| e510f06d7b | |||
| 77de701392 | |||
| 32273858de | |||
| caeed7c026 |
@@ -0,0 +1,139 @@
|
||||
# **Skill Engine - A Modular Skill Tree Framework for Forge Mods**
|
||||
|
||||
**Skill Engine** is **not** a standalone progression mod.
|
||||
It does **not** add gameplay, attributes, or abilities by itself.
|
||||
|
||||
Instead, **Skill Engine is a framework** designed to let other mods create their own **highly customizable skill trees**, complete with:
|
||||
|
||||
* Node unlocking
|
||||
* Attribute requirements
|
||||
* Custom icons
|
||||
* Skill point handling
|
||||
* Server synchronization
|
||||
* Requirements & tooltips
|
||||
* Interaction with other progression systems (like Ascend)
|
||||
|
||||
If you're looking for a mod that _adds_ skills, this is **not** it.
|
||||
If you are a developer who wants to **add skill trees to your mod without reinventing everything**, Skill Engine is exactly what you need.
|
||||
|
||||
***
|
||||
|
||||
# **Features**
|
||||
|
||||
### JSON-driven Skill Tree System
|
||||
|
||||
Mods can register their own skill nodes simply by adding JSON files under:
|
||||
|
||||
```
|
||||
data/<yourmodid>/skillnodes/*.json
|
||||
```
|
||||
|
||||
### Automatic Node Rendering
|
||||
|
||||
Nodes are positioned, displayed, zoomed, panned, and drawn automatically in the Skill Tree GUI.
|
||||
|
||||
### Icons Support
|
||||
|
||||
Use **your own textures** or **vanilla item textures** as node icons.
|
||||
|
||||
### Requirements System
|
||||
|
||||
Nodes can specify:
|
||||
|
||||
* Linked nodes
|
||||
* Prerequisite attributes
|
||||
* Skill costs
|
||||
* Custom tags
|
||||
|
||||
The UI automatically displays requirements in tooltips and overlays.
|
||||
|
||||
### Unlock Logic & Sync
|
||||
|
||||
Unlocking is fully handled server-side and synced to the client.
|
||||
|
||||
### Full API Support
|
||||
|
||||
Skill Engine includes a public API class that allows other mods to:
|
||||
|
||||
* Give skill points
|
||||
* Unlock skill nodes
|
||||
* Check unlock state
|
||||
* Access player skill data
|
||||
* Register or reference nodes
|
||||
|
||||
***
|
||||
|
||||
# **How to Use Skill Engine in Your Mod**
|
||||
|
||||
Skill Engine is designed to be extremely easy to integrate.
|
||||
|
||||
## **1\. Add Skill Engine as a Dependency**
|
||||
|
||||
In your `mods.toml`:
|
||||
|
||||
```toml
|
||||
[[dependencies.yourmodid]]
|
||||
modId="skillengine"
|
||||
type="required"
|
||||
versionRange="[1.0.0,)"
|
||||
```
|
||||
|
||||
In your Gradle build:
|
||||
|
||||
```gradle
|
||||
implementation fg.deobf("net.fretux.skillengine:SkillEngine:<version>")
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
# **2\. Create Your Skill Nodes (JSON)**
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Basic Strength",
|
||||
"description": "You feel your muscles tighten.",
|
||||
"cost": 1,
|
||||
"position": { "x": -150.0, "y": -50.0 },
|
||||
"links": ["skillengine:welcome"],
|
||||
"tags": ["yourmodid:strength_1"],
|
||||
"icons": "minecraft:textures/item/iron_sword.png",
|
||||
"prerequisites": {
|
||||
"strength": 10,
|
||||
"agility": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Place this in:
|
||||
|
||||
```
|
||||
data/yourmodid/skillnodes/basic_strength.json
|
||||
```
|
||||
|
||||
Skill Engine will automatically detect it and add it to the tree.
|
||||
|
||||
***
|
||||
|
||||
# **3\. Use the API in Your Code**
|
||||
|
||||
Skill Engine exposes a simple API:
|
||||
|
||||
```java
|
||||
SkillEngineAPI.unlockNode(player, SkillEngineAPI.rl("yourmodid", "basic_strength"));
|
||||
SkillEngineAPI.addSkillPoints(player, 3);
|
||||
boolean unlocked = SkillEngineAPI.isUnlocked(player, SkillEngineAPI.id("welcome"));
|
||||
```
|
||||
|
||||
You can interact with the system without touching internal classes.
|
||||
|
||||
# **Users: What Does This Mod Do for Me?**
|
||||
|
||||
Skill Engine **does nothing by itself**.
|
||||
It does _not_ add a skill tree or abilities on its own.
|
||||
|
||||
You only need this mod if:
|
||||
|
||||
* A mod you play **depends** on Skill Engine
|
||||
* You want to install addons that create skill trees
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
|
||||
Source installation information for modders
|
||||
-------------------------------------------
|
||||
This code follows the Minecraft Forge installation methodology. It will apply
|
||||
some small patches to the vanilla MCP source code, giving you and it access
|
||||
to some of the data and functions you need to build a successful mod.
|
||||
|
||||
Note also that the patches are built against "un-renamed" MCP source code (aka
|
||||
SRG Names) - this means that you will not be able to read them directly against
|
||||
normal code.
|
||||
|
||||
Setup Process:
|
||||
==============================
|
||||
|
||||
Step 1: Open your command-line and browse to the folder where you extracted the zip file.
|
||||
|
||||
Step 2: You're left with a choice.
|
||||
If you prefer to use Eclipse:
|
||||
1. Run the following command: `./gradlew genEclipseRuns`
|
||||
2. Open Eclipse, Import > Existing Gradle Project > Select Folder
|
||||
or run `gradlew eclipse` to generate the project.
|
||||
|
||||
If you prefer to use IntelliJ:
|
||||
1. Open IDEA, and import project.
|
||||
2. Select your build.gradle file and have it import.
|
||||
3. Run the following command: `./gradlew genIntellijRuns`
|
||||
4. Refresh the Gradle Project in IDEA if required.
|
||||
|
||||
If at any point you are missing libraries in your IDE, or you've run into problems you can
|
||||
run `gradlew --refresh-dependencies` to refresh the local cache. `gradlew clean` to reset everything
|
||||
(this does not affect your code) and then start the process again.
|
||||
|
||||
Mapping Names:
|
||||
=============================
|
||||
By default, the MDK is configured to use the official mapping names from Mojang for methods and fields
|
||||
in the Minecraft codebase. These names are covered by a specific license. All modders should be aware of this
|
||||
license, if you do not agree with it you can change your mapping names to other crowdsourced names in your
|
||||
build.gradle. For the latest license text, refer to the mapping file itself, or the reference copy here:
|
||||
https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md
|
||||
|
||||
Additional Resources:
|
||||
=========================
|
||||
Community Documentation: https://docs.minecraftforge.net/en/1.20.1/gettingstarted/
|
||||
LexManos' Install Video: https://youtu.be/8VEdtQLuLO0
|
||||
Forge Forums: https://forums.minecraftforge.net/
|
||||
Forge Discord: https://discord.minecraftforge.net/
|
||||
+1
-1
@@ -131,7 +131,7 @@ dependencies {
|
||||
// If the group id is "net.minecraft" and the artifact id is one of ["client", "server", "joined"],
|
||||
// then special handling is done to allow a setup of a vanilla dependency without the use of an external repository.
|
||||
minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}"
|
||||
implementation fg.deobf("net.fretux.ascend:Ascend:2.0.2")
|
||||
implementation fg.deobf("net.fretux.ascend:Ascend:3.1.1")
|
||||
|
||||
// Example mod dependency with JEI - using fg.deobf() ensures the dependency is remapped to your development mappings
|
||||
// The JEI API is declared for compile time use, while the full JEI artifact is used at runtime
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ mod_name=Fretux's Skill Engine
|
||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||
mod_license=All Rights Reserved
|
||||
# The mod version. See https://semver.org/
|
||||
mod_version=1.1.0
|
||||
mod_version=1.4.9
|
||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||
# This should match the base package used for the mod sources.
|
||||
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-1
@@ -1,6 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -83,7 +85,8 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -111,7 +114,7 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -130,10 +133,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
@@ -141,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -149,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -198,16 +204,16 @@ fi
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
Vendored
+14
-12
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package net.fretux.skillengine;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import net.fretux.skillengine.config.SkillEngineConfig;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.fretux.skillengine.network.PacketHandler;
|
||||
import net.minecraftforge.eventbus.api.IEventBus;
|
||||
import net.minecraftforge.fml.DistExecutor;
|
||||
import net.minecraftforge.fml.ModLoadingContext;
|
||||
import net.minecraftforge.fml.config.ModConfig;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||
import org.slf4j.Logger;
|
||||
@@ -15,6 +20,11 @@ public class SkillEngine {
|
||||
|
||||
public SkillEngine() {
|
||||
IEventBus modBus = FMLJavaModLoadingContext.get().getModEventBus();
|
||||
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, SkillEngineConfig.COMMON_SPEC);
|
||||
PacketHandler.register();
|
||||
DistExecutor.unsafeRunWhenOn(
|
||||
Dist.CLIENT,
|
||||
() -> () -> net.fretux.skillengine.client.ClientBootstrap.register(modBus)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.fretux.skillengine.abilities;
|
||||
|
||||
import net.fretux.skillengine.SkillEngine;
|
||||
import net.fretux.skillengine.capability.PlayerSkillData;
|
||||
import net.fretux.skillengine.capability.SkillEngineCapabilities;
|
||||
import net.fretux.skillengine.network.ClientboundSyncCooldownPacket;
|
||||
import net.fretux.skillengine.network.PacketHandler;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.network.PacketDistributor;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = SkillEngine.MODID)
|
||||
public class AbilityCooldownHandler {
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.PlayerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
if (event.player.level().isClientSide) return;
|
||||
if (!(event.player instanceof ServerPlayer player)) return;
|
||||
event.player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> {
|
||||
data.tickCooldowns();
|
||||
data.tickTagCooldowns();
|
||||
if (player.tickCount % 10 == 0) {
|
||||
for (int i = 1; i <= data.getAbilitySlots().length; i++) {
|
||||
PacketHandler.CHANNEL.send(
|
||||
PacketDistributor.PLAYER.with(() -> player),
|
||||
new ClientboundSyncCooldownPacket(i, data.getCooldown(i))
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package net.fretux.skillengine.api;
|
||||
|
||||
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AbilityHandler {
|
||||
boolean execute(ServerPlayer player, AbilityNode ability);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.fretux.skillengine.api;
|
||||
|
||||
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class AbilityHandlerRegistry {
|
||||
|
||||
private static final Map<ResourceLocation, AbilityHandler> BY_ID = new LinkedHashMap<>();
|
||||
private static final Map<ResourceLocation, AbilityHandler> BY_TAG = new LinkedHashMap<>();
|
||||
|
||||
private AbilityHandlerRegistry() {}
|
||||
|
||||
public static void register(ResourceLocation abilityId, AbilityHandler handler) {
|
||||
BY_ID.put(abilityId, handler);
|
||||
}
|
||||
|
||||
public static void registerForTag(ResourceLocation tag, AbilityHandler handler) {
|
||||
BY_TAG.put(tag, handler);
|
||||
}
|
||||
|
||||
public static boolean execute(ServerPlayer player, AbilityNode ability) {
|
||||
AbilityHandler direct = BY_ID.get(ability.getId());
|
||||
if (direct != null) {
|
||||
return direct.execute(player, ability);
|
||||
}
|
||||
for (ResourceLocation tag : ability.getTags()) {
|
||||
AbilityHandler tagged = BY_TAG.get(tag);
|
||||
if (tagged != null && tagged.execute(player, ability)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package net.fretux.skillengine.api;
|
||||
|
||||
import net.fretux.skillengine.skilltree.SkillNode;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface PrerequisiteProvider {
|
||||
boolean meets(ServerPlayer player, SkillNode node, String key, int requiredValue);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.fretux.skillengine.api;
|
||||
|
||||
import net.fretux.ascend.player.PlayerStatsProvider;
|
||||
import net.fretux.skillengine.skilltree.SkillNode;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class PrerequisiteProviderRegistry {
|
||||
|
||||
private static final Map<String, PrerequisiteProvider> PROVIDERS = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
register("ascend", (player, node, key, requiredValue) ->
|
||||
player.getCapability(PlayerStatsProvider.PLAYER_STATS)
|
||||
.map(stats -> stats.getAttributeLevel(key) >= requiredValue)
|
||||
.orElse(false));
|
||||
}
|
||||
|
||||
private PrerequisiteProviderRegistry() {}
|
||||
|
||||
public static void register(String namespace, PrerequisiteProvider provider) {
|
||||
PROVIDERS.put(namespace, provider);
|
||||
}
|
||||
|
||||
public static boolean meets(ServerPlayer player, SkillNode node, String key, int requiredValue) {
|
||||
int separator = key.indexOf(':');
|
||||
String namespace = separator > 0 ? key.substring(0, separator) : "ascend";
|
||||
String localKey = separator > 0 ? key.substring(separator + 1) : key;
|
||||
PrerequisiteProvider provider = PROVIDERS.get(namespace);
|
||||
return provider != null && provider.meets(player, node, localKey, requiredValue);
|
||||
}
|
||||
|
||||
public static boolean hasProvider(String namespace) {
|
||||
return PROVIDERS.containsKey(namespace);
|
||||
}
|
||||
}
|
||||
@@ -3,38 +3,62 @@ package net.fretux.skillengine.api;
|
||||
import net.fretux.skillengine.SkillEngine;
|
||||
import net.fretux.skillengine.capability.PlayerSkillData;
|
||||
import net.fretux.skillengine.capability.SkillEngineCapabilities;
|
||||
import net.fretux.skillengine.client.ClientSkillEngineBridge;
|
||||
import net.fretux.skillengine.events.SkillPointsChangedEvent;
|
||||
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||
import net.fretux.skillengine.skilltree.AbilityNodeRegistry;
|
||||
import net.fretux.skillengine.skilltree.SkillLogic;
|
||||
import net.fretux.skillengine.skilltree.SkillNode;
|
||||
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.fml.DistExecutor;
|
||||
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SkillEngineAPI {
|
||||
|
||||
private SkillEngineAPI() {}
|
||||
|
||||
public static ResourceLocation id(String path) {
|
||||
return new ResourceLocation(SkillEngine.MODID, path);
|
||||
return ResourceLocation.fromNamespaceAndPath(SkillEngine.MODID, path);
|
||||
}
|
||||
|
||||
public static ResourceLocation rl(String namespace, String path) {
|
||||
return new ResourceLocation(namespace, path);
|
||||
return ResourceLocation.fromNamespaceAndPath(namespace, path);
|
||||
}
|
||||
public static SkillNode getNode(ResourceLocation id) {
|
||||
|
||||
public static SkillNode getSkillNode(ResourceLocation id) {
|
||||
return SkillNodeRegistry.get(id);
|
||||
}
|
||||
public static boolean hasNode(ResourceLocation id) {
|
||||
|
||||
public static AbilityNode getAbilityNode(ResourceLocation id) {
|
||||
return AbilityNodeRegistry.get(id);
|
||||
}
|
||||
|
||||
public static boolean hasSkillNode(ResourceLocation id) {
|
||||
return SkillNodeRegistry.get(id) != null;
|
||||
}
|
||||
public static List<SkillNode> getAllNodes() {
|
||||
|
||||
public static boolean hasAbilityNode(ResourceLocation id) {
|
||||
return AbilityNodeRegistry.get(id) != null;
|
||||
}
|
||||
|
||||
public static List<SkillNode> getAllSkillNodes() {
|
||||
return List.copyOf(SkillNodeRegistry.all());
|
||||
}
|
||||
|
||||
public static List<AbilityNode> getAllAbilityNodes() {
|
||||
return List.copyOf(AbilityNodeRegistry.all());
|
||||
}
|
||||
|
||||
public static PlayerSkillData getPlayerData(Player player) {
|
||||
return player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).orElse(null);
|
||||
}
|
||||
public static Set<ResourceLocation> getUnlockedNodes(Player player) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
return data != null ? data.getUnlockedNodes() : Set.of();
|
||||
}
|
||||
|
||||
public static int getSkillPoints(Player player) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
@@ -45,38 +69,162 @@ public final class SkillEngineAPI {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
if (data != null) {
|
||||
data.addSkillPoints(amount);
|
||||
if (amount != 0) {
|
||||
MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent(
|
||||
player,
|
||||
Math.abs(amount),
|
||||
amount > 0 ? SkillPointsChangedEvent.Reason.GAINED : SkillPointsChangedEvent.Reason.SPENT,
|
||||
null
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean consumeSkillPoints(Player player, int amount) {
|
||||
if (amount <= 0) return false;
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
if (data != null && data.getSkillPoints() >= amount) {
|
||||
data.addSkillPoints(-amount);
|
||||
MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent(
|
||||
player,
|
||||
amount,
|
||||
SkillPointsChangedEvent.Reason.SPENT,
|
||||
null
|
||||
));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isUnlocked(Player player, ResourceLocation nodeId) {
|
||||
public static Set<ResourceLocation> getUnlockedSkillNodes(Player player) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
return data != null && data.isUnlocked(nodeId);
|
||||
return data != null ? data.getUnlockedNodes() : Set.of();
|
||||
}
|
||||
|
||||
public static boolean unlockNode(Player player, ResourceLocation nodeId) {
|
||||
public static boolean isSkillUnlocked(Player player, ResourceLocation id) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
return data != null && data.isUnlocked(id);
|
||||
}
|
||||
|
||||
public static boolean unlockSkill(Player player, ResourceLocation id) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
if (data == null) return false;
|
||||
|
||||
SkillNode node = SkillNodeRegistry.get(nodeId);
|
||||
SkillNode node = SkillNodeRegistry.get(id);
|
||||
if (node == null) return false;
|
||||
|
||||
if (!net.fretux.skillengine.skilltree.SkillLogic.canUnlock(data, player, node)) {
|
||||
return false;
|
||||
List<SkillNode> unlockPlan = SkillLogic.getUnlockPlan(data, player, node).orElse(null);
|
||||
if (unlockPlan == null) return false;
|
||||
for (SkillNode plannedNode : unlockPlan) {
|
||||
data.unlockNode(plannedNode);
|
||||
int cost = Math.max(0, plannedNode.getCost());
|
||||
if (cost > 0) {
|
||||
MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent(
|
||||
player,
|
||||
cost,
|
||||
SkillPointsChangedEvent.Reason.SPENT,
|
||||
plannedNode.getId()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
data.unlockNode(node);
|
||||
return true;
|
||||
}
|
||||
public static boolean clientHasNode(ResourceLocation nodeId) {
|
||||
return net.fretux.skillengine.client.SkilltreeClientState.isUnlocked(nodeId);
|
||||
|
||||
public static boolean refundSkill(Player player, ResourceLocation id) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
SkillNode node = SkillNodeRegistry.get(id);
|
||||
if (data == null || node == null || !data.refundNode(id)) return false;
|
||||
int refunded = Math.max(0, node.getCost());
|
||||
MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent(
|
||||
player,
|
||||
refunded,
|
||||
SkillPointsChangedEvent.Reason.REFUNDED,
|
||||
id
|
||||
));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static int refundBranch(Player player, ResourceLocation rootId) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
if (data == null) return 0;
|
||||
int refunded = data.refundBranch(rootId);
|
||||
if (refunded > 0) {
|
||||
MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent(
|
||||
player,
|
||||
refunded,
|
||||
SkillPointsChangedEvent.Reason.REFUNDED,
|
||||
rootId
|
||||
));
|
||||
}
|
||||
return refunded;
|
||||
}
|
||||
|
||||
public static int resetSkills(Player player) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
if (data == null) return 0;
|
||||
int refunded = data.resetAllNodesPreserveEarnedPoints();
|
||||
if (refunded > 0) {
|
||||
MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent(
|
||||
player,
|
||||
refunded,
|
||||
SkillPointsChangedEvent.Reason.REFUNDED,
|
||||
null
|
||||
));
|
||||
}
|
||||
return refunded;
|
||||
}
|
||||
|
||||
public static boolean isAbilityUnlocked(Player player, ResourceLocation id) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
return data != null && data.isAbilityUnlocked(id);
|
||||
}
|
||||
|
||||
public static Set<ResourceLocation> getUnlockedAbilities(Player player) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
return data != null ? data.getUnlockedAbilities() : Set.of();
|
||||
}
|
||||
|
||||
public static boolean unlockAbility(Player player, ResourceLocation id) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
if (data == null) return false;
|
||||
AbilityNode ability = AbilityNodeRegistry.get(id);
|
||||
if (ability == null) return false;
|
||||
data.unlockAbility(ability);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void bindAbility(Player player, int slot, ResourceLocation abilityId) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
if (data == null) return;
|
||||
data.bindAbility(slot, abilityId);
|
||||
}
|
||||
|
||||
public static ResourceLocation getAbilityInSlot(Player player, int slot) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
return data != null ? data.getAbilityInSlot(slot) : null;
|
||||
}
|
||||
|
||||
public static ResourceLocation[] getAbilitySlots(Player player) {
|
||||
PlayerSkillData data = getPlayerData(player);
|
||||
return data != null ? data.getAbilitySlots() : new ResourceLocation[3];
|
||||
}
|
||||
|
||||
public static boolean clientHasSkill(ResourceLocation id) {
|
||||
if (FMLEnvironment.dist != Dist.CLIENT) {
|
||||
return false;
|
||||
}
|
||||
return DistExecutor.unsafeCallWhenOn(Dist.CLIENT, () -> () -> ClientSkillEngineBridge.clientHasSkill(id));
|
||||
}
|
||||
|
||||
public static int clientGetAbilitySlot(ResourceLocation id) {
|
||||
if (FMLEnvironment.dist != Dist.CLIENT) {
|
||||
return -1;
|
||||
}
|
||||
return DistExecutor.unsafeCallWhenOn(Dist.CLIENT, () -> () -> ClientSkillEngineBridge.clientGetAbilitySlot(id));
|
||||
}
|
||||
|
||||
public static ResourceLocation[] clientGetAbilitySlots() {
|
||||
if (FMLEnvironment.dist != Dist.CLIENT) {
|
||||
return new ResourceLocation[3];
|
||||
}
|
||||
return DistExecutor.unsafeCallWhenOn(Dist.CLIENT, () -> ClientSkillEngineBridge::clientGetAbilitySlots);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,68 @@
|
||||
package net.fretux.skillengine.capability;
|
||||
|
||||
import net.fretux.skillengine.network.PacketHandler;
|
||||
import net.fretux.skillengine.config.SkillEngineConfig;
|
||||
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||
import net.fretux.skillengine.skilltree.SkillNode;
|
||||
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class PlayerSkillData {
|
||||
|
||||
private int skillPoints;
|
||||
private final Set<ResourceLocation> unlockedNodes = new HashSet<>();
|
||||
private final Set<ResourceLocation> unlockedAbilities = new HashSet<>();
|
||||
private final Set<ResourceLocation> activeTags = new HashSet<>();
|
||||
private ResourceLocation[] abilitySlots = new ResourceLocation[getConfiguredSlotCount()];
|
||||
private int[] abilityCooldowns = new int[abilitySlots.length];
|
||||
private final Map<ResourceLocation, Integer> tagCooldowns = new HashMap<>();
|
||||
|
||||
public int getSkillPoints() { return skillPoints; }
|
||||
public void addSkillPoints(int amount) { skillPoints += amount; }
|
||||
public boolean isOnCooldown(ResourceLocation tag) {
|
||||
return tagCooldowns.getOrDefault(tag, 0) > 0;
|
||||
}
|
||||
|
||||
public void setCooldown(ResourceLocation tag, int ticks) {
|
||||
tagCooldowns.put(tag, ticks);
|
||||
}
|
||||
|
||||
public void tickTagCooldowns() {
|
||||
tagCooldowns.replaceAll((k, v) -> Math.max(0, v - 1));
|
||||
}
|
||||
|
||||
|
||||
public int getSkillPoints() {
|
||||
return skillPoints;
|
||||
}
|
||||
|
||||
public void addSkillPoints(int amount) {
|
||||
skillPoints += amount;
|
||||
}
|
||||
|
||||
public boolean isUnlocked(ResourceLocation nodeId) {
|
||||
return unlockedNodes.contains(nodeId);
|
||||
}
|
||||
|
||||
public void unlockNode(SkillNode node) {
|
||||
if (node == null) return;
|
||||
if (unlockedNodes.add(node.getId())) {
|
||||
activeTags.addAll(node.getTags());
|
||||
skillPoints -= Math.max(0, node.getCost());
|
||||
}
|
||||
}
|
||||
|
||||
public Set<ResourceLocation> getUnlockedNodes() {
|
||||
return unlockedNodes;
|
||||
}
|
||||
|
||||
public boolean hasTag(ResourceLocation tag) {
|
||||
return activeTags.contains(tag);
|
||||
}
|
||||
@@ -31,65 +71,162 @@ public class PlayerSkillData {
|
||||
return activeTags;
|
||||
}
|
||||
|
||||
public void unlockNode(SkillNode node) {
|
||||
if (unlockedNodes.add(node.getId())) {
|
||||
activeTags.addAll(node.getTags());
|
||||
skillPoints -= node.getCost();
|
||||
public boolean isAbilityUnlocked(ResourceLocation id) {
|
||||
return unlockedAbilities.contains(id);
|
||||
}
|
||||
|
||||
public void unlockAbility(AbilityNode ability) {
|
||||
if (ability == null) return;
|
||||
unlockedAbilities.add(ability.getId());
|
||||
}
|
||||
|
||||
public Set<ResourceLocation> getUnlockedAbilities() {
|
||||
return unlockedAbilities;
|
||||
}
|
||||
|
||||
public void bindAbility(int slot, ResourceLocation abilityId) {
|
||||
ensureSlotCount();
|
||||
bindHelper(slot, abilityId, abilitySlots);
|
||||
}
|
||||
|
||||
public static void bindHelper(int slot, ResourceLocation abilityId, ResourceLocation[] slots) {
|
||||
int index = slot - 1;
|
||||
if (index < 0 || index >= slots.length) return;
|
||||
if (abilityId != null) {
|
||||
for (int i = 0; i < slots.length; i++) {
|
||||
if (abilityId.equals(slots[i])) {
|
||||
slots[i] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
slots[index] = abilityId;
|
||||
}
|
||||
|
||||
public ResourceLocation getAbilityInSlot(int slot) {
|
||||
ensureSlotCount();
|
||||
int index = slot - 1;
|
||||
if (index < 0 || index >= abilitySlots.length) return null;
|
||||
return abilitySlots[index];
|
||||
}
|
||||
|
||||
public ResourceLocation[] getAbilitySlots() {
|
||||
ensureSlotCount();
|
||||
return abilitySlots.clone();
|
||||
}
|
||||
|
||||
public void clearAbilitySlots() {
|
||||
ensureSlotCount();
|
||||
for (int i = 0; i < abilitySlots.length; i++) {
|
||||
abilitySlots[i] = null;
|
||||
abilityCooldowns[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void setCooldown(int slot, int ticks) {
|
||||
ensureSlotCount();
|
||||
int idx = slot - 1;
|
||||
if (idx < 0 || idx >= abilityCooldowns.length) return;
|
||||
abilityCooldowns[idx] = Math.max(0, ticks);
|
||||
}
|
||||
|
||||
public int getCooldown(int slot) {
|
||||
ensureSlotCount();
|
||||
int idx = slot - 1;
|
||||
if (idx < 0 || idx >= abilityCooldowns.length) return 0;
|
||||
return abilityCooldowns[idx];
|
||||
}
|
||||
|
||||
public void tickCooldowns() {
|
||||
ensureSlotCount();
|
||||
for (int i = 0; i < abilityCooldowns.length; i++) {
|
||||
if (abilityCooldowns[i] > 0) {
|
||||
abilityCooldowns[i]--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
tag.put("UnlockedNodes", unlockedList);
|
||||
ListTag tagsList = new ListTag();
|
||||
for (ResourceLocation id : activeTags) {
|
||||
CompoundTag t = new CompoundTag();
|
||||
t.putString("Tag", id.toString());
|
||||
tagList.add(t);
|
||||
tagsList.add(t);
|
||||
}
|
||||
tag.put("Tags", tagList);
|
||||
|
||||
tag.put("ActiveTags", tagsList);
|
||||
ListTag slotList = new ListTag();
|
||||
ensureSlotCount();
|
||||
for (ResourceLocation slot : abilitySlots) {
|
||||
CompoundTag t = new CompoundTag();
|
||||
t.putString("Id", slot == null ? "" : slot.toString());
|
||||
slotList.add(t);
|
||||
}
|
||||
tag.put("AbilitySlots", slotList);
|
||||
ListTag abilityList = new ListTag();
|
||||
for (ResourceLocation id : unlockedAbilities) {
|
||||
CompoundTag t = new CompoundTag();
|
||||
t.putString("Id", id.toString());
|
||||
abilityList.add(t);
|
||||
}
|
||||
tag.put("UnlockedAbilities", abilityList);
|
||||
ListTag cdList = new ListTag();
|
||||
for (int cd : abilityCooldowns) {
|
||||
CompoundTag t = new CompoundTag();
|
||||
t.putInt("CD", cd);
|
||||
cdList.add(t);
|
||||
}
|
||||
tag.put("AbilityCooldowns", cdList);
|
||||
return tag;
|
||||
}
|
||||
|
||||
public void load(CompoundTag tag) {
|
||||
skillPoints = tag.getInt("SkillPoints");
|
||||
unlockedNodes.clear();
|
||||
unlockedAbilities.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")));
|
||||
abilitySlots = new ResourceLocation[getConfiguredSlotCount()];
|
||||
abilityCooldowns = new int[abilitySlots.length];
|
||||
tagCooldowns.clear();
|
||||
ListTag nodeList = tag.getList("UnlockedNodes", Tag.TAG_COMPOUND);
|
||||
for (Tag t : nodeList) {
|
||||
ResourceLocation id = ResourceLocation.parse(((CompoundTag) t).getString("Id"));
|
||||
unlockedNodes.add(id);
|
||||
SkillNode node = SkillNodeRegistry.get(id);
|
||||
if (node != null) activeTags.addAll(node.getTags());
|
||||
}
|
||||
|
||||
ListTag tagList = tag.getList("Tags", Tag.TAG_COMPOUND);
|
||||
ListTag tagList = tag.getList("ActiveTags", Tag.TAG_COMPOUND);
|
||||
for (Tag t : tagList) {
|
||||
CompoundTag c = (CompoundTag) t;
|
||||
activeTags.add(new ResourceLocation(c.getString("Tag")));
|
||||
activeTags.add(ResourceLocation.parse(((CompoundTag) t).getString("Tag")));
|
||||
}
|
||||
ListTag slots = tag.getList("AbilitySlots", Tag.TAG_COMPOUND);
|
||||
for (int i = 0; i < abilitySlots.length && i < slots.size(); i++) {
|
||||
String id = slots.getCompound(i).getString("Id");
|
||||
abilitySlots[i] = id.isEmpty() ? null : ResourceLocation.parse(id);
|
||||
}
|
||||
ListTag abilityList = tag.getList("UnlockedAbilities", Tag.TAG_COMPOUND);
|
||||
for (Tag t : abilityList) {
|
||||
unlockedAbilities.add(ResourceLocation.parse(((CompoundTag) t).getString("Id")));
|
||||
}
|
||||
ListTag cdList = tag.getList("AbilityCooldowns", Tag.TAG_COMPOUND);
|
||||
for (int i = 0; i < abilityCooldowns.length && i < cdList.size(); i++) {
|
||||
CompoundTag t = cdList.getCompound(i);
|
||||
abilityCooldowns[i] = t.getInt("CD");
|
||||
}
|
||||
}
|
||||
|
||||
public Set<ResourceLocation> getUnlockedNodes() {
|
||||
return unlockedNodes;
|
||||
}
|
||||
|
||||
public int getTotalSkillCost() {
|
||||
int total = 0;
|
||||
for (ResourceLocation id : unlockedNodes) {
|
||||
SkillNode node = SkillNodeRegistry.get(id);
|
||||
if (node != null) {
|
||||
total += node.getCost();
|
||||
}
|
||||
if (node != null) total += Math.max(0, node.getCost());
|
||||
}
|
||||
return total;
|
||||
}
|
||||
@@ -97,5 +234,82 @@ public class PlayerSkillData {
|
||||
public void clearAllNodes() {
|
||||
unlockedNodes.clear();
|
||||
activeTags.clear();
|
||||
unlockedAbilities.clear();
|
||||
clearAbilitySlots();
|
||||
}
|
||||
|
||||
public boolean refundNode(ResourceLocation nodeId) {
|
||||
SkillNode node = SkillNodeRegistry.get(nodeId);
|
||||
if (node == null || !unlockedNodes.remove(nodeId)) return false;
|
||||
for (SkillNode other : SkillNodeRegistry.all()) {
|
||||
if (unlockedNodes.contains(other.getId()) && other.getLinks().contains(nodeId)) {
|
||||
unlockedNodes.add(nodeId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
skillPoints += Math.max(0, node.getCost());
|
||||
rebuildActiveTags();
|
||||
return true;
|
||||
}
|
||||
|
||||
public int refundBranch(ResourceLocation rootId) {
|
||||
int refunded = 0;
|
||||
Set<ResourceLocation> toRefund = new HashSet<>();
|
||||
collectChildren(rootId, toRefund);
|
||||
for (ResourceLocation id : toRefund) {
|
||||
SkillNode node = SkillNodeRegistry.get(id);
|
||||
if (node != null && unlockedNodes.remove(id)) {
|
||||
int cost = Math.max(0, node.getCost());
|
||||
skillPoints += cost;
|
||||
refunded += cost;
|
||||
}
|
||||
}
|
||||
rebuildActiveTags();
|
||||
return refunded;
|
||||
}
|
||||
|
||||
public int resetAllNodesPreserveEarnedPoints() {
|
||||
int refunded = getTotalSkillCost();
|
||||
skillPoints += refunded;
|
||||
clearAllNodes();
|
||||
return refunded;
|
||||
}
|
||||
|
||||
private void rebuildActiveTags() {
|
||||
activeTags.clear();
|
||||
for (ResourceLocation id : unlockedNodes) {
|
||||
SkillNode node = SkillNodeRegistry.get(id);
|
||||
if (node != null) activeTags.addAll(node.getTags());
|
||||
}
|
||||
}
|
||||
|
||||
private void collectChildren(ResourceLocation rootId, Set<ResourceLocation> out) {
|
||||
if (!out.add(rootId)) return;
|
||||
for (SkillNode node : SkillNodeRegistry.all()) {
|
||||
if (node.getLinks().contains(rootId)) {
|
||||
collectChildren(node.getId(), out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int getConfiguredSlotCount() {
|
||||
try {
|
||||
return SkillEngineConfig.ABILITY_SLOT_COUNT.get();
|
||||
} catch (IllegalStateException ignored) {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureSlotCount() {
|
||||
int configured = getConfiguredSlotCount();
|
||||
if (abilitySlots.length == configured) return;
|
||||
ResourceLocation[] oldSlots = abilitySlots;
|
||||
int[] oldCooldowns = abilityCooldowns;
|
||||
abilitySlots = Arrays.copyOf(oldSlots, configured);
|
||||
abilityCooldowns = Arrays.copyOf(oldCooldowns, configured);
|
||||
}
|
||||
|
||||
public void sync(ServerPlayer player) {
|
||||
PacketHandler.syncSkillsTo(player);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,7 @@ 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");
|
||||
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath("skillengine", "player_skill_data");
|
||||
private final PlayerSkillData backend = new PlayerSkillData();
|
||||
private final LazyOptional<PlayerSkillData> optional = LazyOptional.of(() -> backend);
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ public class SkillEngineCapabilities {
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onAttachCapabilities(AttachCapabilitiesEvent<Entity> event) {
|
||||
if (event.getObject() instanceof Player player) {
|
||||
if (event.getObject() instanceof Player) {
|
||||
SkillEngine.LOGGER.info("Attaching PlayerSkillData capability");
|
||||
event.addCapability(
|
||||
new ResourceLocation(SkillEngine.MODID, "player_skill_data"),
|
||||
ResourceLocation.fromNamespaceAndPath(SkillEngine.MODID, "player_skill_data"),
|
||||
new PlayerSkillProvider()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
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.network.chat.FormattedText;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AbilityBindingScreen extends Screen {
|
||||
|
||||
private static final int PANEL_WIDTH = 280;
|
||||
private static final int PANEL_HEIGHT = 204;
|
||||
private static final int PANEL_PADDING = 16;
|
||||
private static final int SLOT_BUTTON_WIDTH = 76;
|
||||
private static final int BUTTON_HEIGHT = 20;
|
||||
private static final int BUTTON_GAP = 6;
|
||||
|
||||
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 slotCount = SkilltreeClientState.getAbilitySlots().length;
|
||||
int visibleSlots = Math.min(slotCount, 3);
|
||||
int buttonRowWidth = SLOT_BUTTON_WIDTH * visibleSlots + BUTTON_GAP * Math.max(0, visibleSlots - 1);
|
||||
int buttonX = x + (PANEL_WIDTH - buttonRowWidth) / 2;
|
||||
int buttonY = y + PANEL_HEIGHT - 54;
|
||||
int boundSlot = SkilltreeClientState.getSlotOfAbility(ability.getId());
|
||||
for (int slot = 1; slot <= visibleSlots; slot++) {
|
||||
int currentSlot = slot;
|
||||
Button button = addRenderableWidget(SkillUi.button(Component.literal("Slot " + slot),
|
||||
b -> bind(currentSlot))
|
||||
.pos(buttonX + (SLOT_BUTTON_WIDTH + BUTTON_GAP) * (slot - 1), buttonY)
|
||||
.size(SLOT_BUTTON_WIDTH, BUTTON_HEIGHT)
|
||||
.build());
|
||||
if (boundSlot == slot) {
|
||||
button.active = false;
|
||||
button.setMessage(Component.literal("Bound " + slot));
|
||||
}
|
||||
}
|
||||
addRenderableWidget(SkillUi.button(Component.literal("Cancel"),
|
||||
b -> onClose()).pos(x + PANEL_WIDTH / 2 - 40, y + PANEL_HEIGHT - 28).size(80, BUTTON_HEIGHT).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(@NotNull 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);
|
||||
SkillUi.panel(gfx, x, y, PANEL_WIDTH, PANEL_HEIGHT);
|
||||
gfx.drawString(font, "ASSIGN ABILITY", x + 16, y + 14, SkillUi.ACCENT, false);
|
||||
ResourceLocation icon = ability.getIcon();
|
||||
if (icon != null) {
|
||||
gfx.blit(icon, x + 16, y + 34, 0, 0, 20, 20, 20, 20);
|
||||
}
|
||||
gfx.drawString(font, font.plainSubstrByWidth(ability.getTitle().getString(), PANEL_WIDTH - 64), x + 44, y + 40, SkillUi.TEXT, false);
|
||||
if (ability.getDescription() != null) {
|
||||
drawDescription(gfx, x + PANEL_PADDING, y + 66, y + PANEL_HEIGHT - 60);
|
||||
}
|
||||
|
||||
super.render(gfx, mouseX, mouseY, partialTick);
|
||||
}
|
||||
|
||||
private void drawDescription(GuiGraphics gfx, int x, int y, int bottomY) {
|
||||
int width = PANEL_WIDTH - PANEL_PADDING * 2;
|
||||
List<FormattedText> lines = font.getSplitter().splitLines(
|
||||
ability.getDescription(),
|
||||
width,
|
||||
ability.getDescription().getStyle()
|
||||
);
|
||||
int lineY = y;
|
||||
for (FormattedText line : lines) {
|
||||
if (lineY + font.lineHeight > bottomY) {
|
||||
break;
|
||||
}
|
||||
gfx.drawString(font, line.getString(), x, lineY, SkillUi.MUTED);
|
||||
lineY += font.lineHeight + 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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.resources.ResourceLocation;
|
||||
import net.minecraftforge.client.event.RenderGuiEvent;
|
||||
|
||||
public class AbilityHudOverlay {
|
||||
public static void render(RenderGuiEvent.Post event) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc.player == null) return;
|
||||
int xStart = event.getGuiGraphics().guiWidth() - 124;
|
||||
int yStart = 10;
|
||||
int slots = SkilltreeClientState.getAbilitySlots().length;
|
||||
for (int slot = 1; slot <= slots; slot++) {
|
||||
var abilityId = SkilltreeClientState.getAbilityInSlot(slot);
|
||||
if (abilityId == null) continue;
|
||||
AbilityNode ability = AbilityNodeRegistry.get(abilityId);
|
||||
if (ability == null) continue;
|
||||
int cd = SkilltreeClientState.getClientCooldown(slot);
|
||||
ResourceLocation icon = ability.getIcon();
|
||||
GuiGraphics gfx = event.getGuiGraphics();
|
||||
gfx.fill(xStart, yStart, xStart + 112, yStart + 26, SkillUi.BACKGROUND);
|
||||
gfx.fill(xStart, yStart, xStart + 2, yStart + 26, cd > 0 ? SkillUi.BORDER : SkillUi.ACCENT);
|
||||
if (icon != null) {
|
||||
gfx.blit(icon, xStart + 7, yStart + 5, 0, 0, 16, 16, 16, 16);
|
||||
}
|
||||
if (cd > 0) {
|
||||
String text = String.valueOf((cd + 19) / 20) + "s";
|
||||
gfx.drawString(mc.font, text, xStart + 84, yStart + 9, SkillUi.ACCENT);
|
||||
}
|
||||
gfx.drawString(mc.font, "Ability " + slot, xStart + 30, yStart + 9, SkillUi.TEXT);
|
||||
yStart += 30;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.fretux.skillengine.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.fretux.skillengine.network.PacketHandler;
|
||||
import net.fretux.skillengine.network.ServerboundActivateAbilityPacket;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraftforge.client.event.InputEvent;
|
||||
import net.minecraftforge.client.settings.KeyConflictContext;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
public class AbilityKeybindHandler {
|
||||
|
||||
public static final int MAX_REGISTERED_ABILITY_SLOTS = 9;
|
||||
public static final KeyMapping[] ABILITY_KEYS = new KeyMapping[MAX_REGISTERED_ABILITY_SLOTS];
|
||||
|
||||
static {
|
||||
for (int slot = 1; slot <= MAX_REGISTERED_ABILITY_SLOTS; slot++) {
|
||||
ABILITY_KEYS[slot - 1] = new KeyMapping(
|
||||
"key.skillengine.ability" + slot,
|
||||
KeyConflictContext.IN_GAME,
|
||||
InputConstants.Type.KEYSYM,
|
||||
defaultKeyForSlot(slot),
|
||||
"key.categories.gameplay"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static void onInput(InputEvent.Key event) {
|
||||
int activeSlots = SkilltreeClientState.getAbilitySlots().length;
|
||||
int handledSlots = Math.min(activeSlots, ABILITY_KEYS.length);
|
||||
for (int slot = 1; slot <= handledSlots; slot++) {
|
||||
if (ABILITY_KEYS[slot - 1].consumeClick()) {
|
||||
if (SkilltreeClientState.getClientCooldown(slot) <= 0) {
|
||||
PacketHandler.CHANNEL.sendToServer(new ServerboundActivateAbilityPacket(slot));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int defaultKeyForSlot(int slot) {
|
||||
return switch (slot) {
|
||||
case 1 -> GLFW.GLFW_KEY_Z;
|
||||
case 2 -> GLFW.GLFW_KEY_X;
|
||||
case 3 -> GLFW.GLFW_KEY_C;
|
||||
default -> InputConstants.UNKNOWN.getValue();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AbilitySelectScreen extends Screen {
|
||||
private final Screen parent;
|
||||
private final int slot;
|
||||
private final List<AbilityNode> unlockedAbilities = new ArrayList<>();
|
||||
private int page = 0;
|
||||
private int pageSize() {
|
||||
return Math.max(1, Math.min(8, (height - 108) / 28));
|
||||
}
|
||||
|
||||
private int panelHeight() { return 92 + pageSize() * 28; }
|
||||
private static final int PANEL_WIDTH = 280;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
page = Math.min(page, Math.max(0, (unlockedAbilities.size() - 1) / pageSize()));
|
||||
buildPageButtons();
|
||||
}
|
||||
|
||||
private void buildPageButtons() {
|
||||
clearWidgets();
|
||||
int x = (width - PANEL_WIDTH) / 2;
|
||||
int y = (height - panelHeight()) / 2;
|
||||
|
||||
int start = page * pageSize();
|
||||
int end = Math.min(unlockedAbilities.size(), start + pageSize());
|
||||
int rowY = y + 52;
|
||||
for (int i = start; i < end; i++) {
|
||||
AbilityNode node = unlockedAbilities.get(i);
|
||||
String label = node.getTitle().getString();
|
||||
boolean alreadyInThisSlot = SkilltreeClientState.getSlotOfAbility(node.getId()) == slot;
|
||||
Button btn = SkillUi.button(Component.literal(label), b -> select(node))
|
||||
.pos(x + 16, rowY)
|
||||
.size(PANEL_WIDTH - 32, 24)
|
||||
.build();
|
||||
if (alreadyInThisSlot) {
|
||||
btn.active = false;
|
||||
btn.setMessage(Component.literal(label + " (bound)"));
|
||||
}
|
||||
addRenderableWidget(btn);
|
||||
rowY += 28;
|
||||
}
|
||||
if (page > 0) {
|
||||
addRenderableWidget(SkillUi.button(Component.literal("< Prev"), b -> {
|
||||
page--;
|
||||
buildPageButtons();
|
||||
}).pos(x + 16, y + panelHeight() - 28).size(60, 20).build());
|
||||
}
|
||||
if ((page + 1) * pageSize() < unlockedAbilities.size()) {
|
||||
addRenderableWidget(SkillUi.button(Component.literal("Next >"), b -> {
|
||||
page++;
|
||||
buildPageButtons();
|
||||
}).pos(x + 80, y + panelHeight() - 28).size(60, 20).build());
|
||||
}
|
||||
addRenderableWidget(SkillUi.button(Component.literal("Back"), b -> back())
|
||||
.pos(x + PANEL_WIDTH - 70, y + panelHeight() - 28).size(60, 20).build());
|
||||
}
|
||||
|
||||
private void select(AbilityNode node) {
|
||||
ResourceLocation id = node.getId();
|
||||
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(@NotNull GuiGraphics gfx, int mouseX, int mouseY, float partialTick) {
|
||||
this.renderBackground(gfx);
|
||||
int x = (width - PANEL_WIDTH) / 2;
|
||||
int y = (height - panelHeight()) / 2;
|
||||
gfx.fill(0, 0, width, height, 0xAA000000);
|
||||
SkillUi.panel(gfx, x, y, PANEL_WIDTH, panelHeight());
|
||||
SkillUi.heading(gfx, font, "CHOOSE ABILITY", "Slot " + slot + " / Page " + (page + 1)
|
||||
+ " of " + Math.max(1, (unlockedAbilities.size() + pageSize() - 1) / pageSize()), x, y);
|
||||
if (unlockedAbilities.isEmpty()) {
|
||||
gfx.drawString(font, "Unlock an ability in the skill tree.", x + 16, y + 58, SkillUi.MUTED, false);
|
||||
}
|
||||
super.render(gfx, mouseX, mouseY, partialTick);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
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.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class AbilitySlotsScreen extends Screen {
|
||||
private static final int PANEL_WIDTH = 280;
|
||||
private static final int MIN_PANEL_HEIGHT = 204;
|
||||
private static final int SLOT_TEXT_RIGHT_PADDING = 8;
|
||||
private static final int REBIND_BUTTON_WIDTH = 62;
|
||||
public AbilitySlotsScreen() {
|
||||
super(Component.literal("Abilities"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
int x = (width - PANEL_WIDTH) / 2;
|
||||
int panelHeight = panelHeight();
|
||||
int y = (height - panelHeight) / 2;
|
||||
int rowY = y + 52;
|
||||
int buttonH = 28;
|
||||
int buttonX = x + PANEL_WIDTH - REBIND_BUTTON_WIDTH - 16;
|
||||
int slots = SkilltreeClientState.getAbilitySlots().length;
|
||||
for (int slot = 1; slot <= slots; slot++) {
|
||||
int currentSlot = slot;
|
||||
addRenderableWidget(SkillUi.button(Component.literal("Rebind"), b -> openSelect(currentSlot))
|
||||
.pos(buttonX, rowY + (slot - 1) * 40).size(REBIND_BUTTON_WIDTH, buttonH).build());
|
||||
}
|
||||
addRenderableWidget(SkillUi.button(Component.literal("Close"), b -> onClose())
|
||||
.pos(x + PANEL_WIDTH - 70, y + panelHeight - 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(@NotNull GuiGraphics gfx, int mouseX, int mouseY, float partialTick) {
|
||||
this.renderBackground(gfx);
|
||||
int x = (width - PANEL_WIDTH) / 2;
|
||||
int panelHeight = panelHeight();
|
||||
int y = (height - panelHeight) / 2;
|
||||
gfx.fill(0, 0, width, height, 0xAA000000);
|
||||
SkillUi.panel(gfx, x, y, PANEL_WIDTH, panelHeight);
|
||||
SkillUi.heading(gfx, font, "ABILITY LOADOUT", "Prepare your active skills", x, y);
|
||||
int rowY = y + 52;
|
||||
int slots = SkilltreeClientState.getAbilitySlots().length;
|
||||
for (int slot = 1; slot <= slots; slot++) {
|
||||
drawSlotRow(gfx, x + 16, rowY + (slot - 1) * 40, slot, "Slot " + slot);
|
||||
}
|
||||
super.render(gfx, mouseX, mouseY, partialTick);
|
||||
}
|
||||
|
||||
private void drawSlotRow(GuiGraphics gfx, int x, int y, int slot, String label) {
|
||||
gfx.fill(x, y - 2, x + PANEL_WIDTH - 32, y + 32, SkillUi.SURFACE);
|
||||
gfx.drawString(font, label.toUpperCase(java.util.Locale.ROOT), x + 8, y + 2, SkillUi.MUTED, false);
|
||||
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) {
|
||||
int iconX = x + 8;
|
||||
gfx.blit(icon, iconX, y + 13, 0, 0, 16, 16, 16, 16);
|
||||
int textX = iconX + 22;
|
||||
gfx.drawString(font, fitTitle(title, textX), textX, y + 17, SkillUi.TEXT);
|
||||
} else {
|
||||
int textX = x + 8;
|
||||
gfx.drawString(font, fitTitle(title, textX), textX, y + 17, SkillUi.MUTED);
|
||||
}
|
||||
}
|
||||
|
||||
private String fitTitle(String title, int textX) {
|
||||
int panelX = (width - PANEL_WIDTH) / 2;
|
||||
int buttonX = panelX + PANEL_WIDTH - REBIND_BUTTON_WIDTH - 16;
|
||||
int availableWidth = buttonX - SLOT_TEXT_RIGHT_PADDING - textX;
|
||||
if (availableWidth <= 0) return "";
|
||||
if (font.width(title) <= availableWidth) {
|
||||
return title;
|
||||
}
|
||||
String suffix = "...";
|
||||
int suffixWidth = font.width(suffix);
|
||||
if (availableWidth <= suffixWidth) {
|
||||
return "";
|
||||
}
|
||||
return font.plainSubstrByWidth(title, availableWidth - suffixWidth) + suffix;
|
||||
}
|
||||
|
||||
private int panelHeight() {
|
||||
int slots = SkilltreeClientState.getAbilitySlots().length;
|
||||
return Math.max(MIN_PANEL_HEIGHT, 92 + slots * 40);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.fretux.skillengine.client;
|
||||
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.eventbus.api.IEventBus;
|
||||
|
||||
public final class ClientBootstrap {
|
||||
|
||||
private ClientBootstrap() {}
|
||||
|
||||
public static void register(IEventBus modBus) {
|
||||
modBus.addListener(ClientKeybinds::registerKeys);
|
||||
MinecraftForge.EVENT_BUS.addListener(ClientEvents::onKeyInput);
|
||||
MinecraftForge.EVENT_BUS.addListener(AbilityKeybindHandler::onInput);
|
||||
MinecraftForge.EVENT_BUS.addListener(ClientCooldownTicker::onClientTick);
|
||||
MinecraftForge.EVENT_BUS.addListener(AbilityHudOverlay::render);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.fretux.skillengine.client;
|
||||
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
|
||||
public class ClientCooldownTicker {
|
||||
public static void onClientTick(TickEvent.ClientTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
int slots = SkilltreeClientState.getAbilitySlots().length;
|
||||
for (int slot = 1; slot <= slots; slot++) {
|
||||
int cd = SkilltreeClientState.getClientCooldown(slot);
|
||||
if (cd > 0) {
|
||||
SkilltreeClientState.updateCooldown(slot, cd - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,18 @@ 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());
|
||||
}
|
||||
if (ClientKeybinds.OPEN_ABILITIES != null &&
|
||||
ClientKeybinds.OPEN_ABILITIES.consumeClick()) {
|
||||
Minecraft.getInstance().setScreen(new AbilitySlotsScreen());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,13 @@ 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;
|
||||
public static KeyMapping OPEN_ABILITIES;
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerKeys(RegisterKeyMappingsEvent event) {
|
||||
OPEN_SKILLTREE = new KeyMapping(
|
||||
"key.skillengine.openskilltree",
|
||||
@@ -24,5 +16,16 @@ public class ClientKeybinds {
|
||||
"key.categories.ui"
|
||||
);
|
||||
event.register(OPEN_SKILLTREE);
|
||||
|
||||
OPEN_ABILITIES = new KeyMapping(
|
||||
"key.skillengine.openabilities",
|
||||
GLFW.GLFW_KEY_M,
|
||||
"key.categories.ui"
|
||||
);
|
||||
event.register(OPEN_ABILITIES);
|
||||
|
||||
for (KeyMapping keyMapping : AbilityKeybindHandler.ABILITY_KEYS) {
|
||||
event.register(keyMapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package net.fretux.skillengine.client;
|
||||
|
||||
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||
import net.fretux.skillengine.skilltree.AbilityNodeRegistry;
|
||||
import net.fretux.skillengine.skilltree.SkillNode;
|
||||
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public final class ClientSkillEngineBridge {
|
||||
|
||||
private ClientSkillEngineBridge() {}
|
||||
|
||||
public static boolean clientHasSkill(ResourceLocation id) {
|
||||
return SkilltreeClientState.isUnlocked(id);
|
||||
}
|
||||
|
||||
public static int clientGetAbilitySlot(ResourceLocation id) {
|
||||
return SkilltreeClientState.getSlotOfAbility(id);
|
||||
}
|
||||
|
||||
public static ResourceLocation[] clientGetAbilitySlots() {
|
||||
return SkilltreeClientState.getAbilitySlots();
|
||||
}
|
||||
|
||||
public static void handleNodeUnlocked(ResourceLocation id, int newSkillPoints, boolean isAbility) {
|
||||
if (isAbility) {
|
||||
SkilltreeClientState.unlockAbility(id);
|
||||
} else {
|
||||
SkilltreeClientState.unlockNode(id);
|
||||
}
|
||||
SkilltreeClientState.setCurrentSkillPoints(newSkillPoints);
|
||||
}
|
||||
|
||||
public static void handleSkillsSync(Iterable<ResourceLocation> unlocked,
|
||||
Iterable<ResourceLocation> unlockedAbilities,
|
||||
int skillPoints,
|
||||
ResourceLocation[] abilitySlots) {
|
||||
SkilltreeClientState.setUnlocked(unlocked);
|
||||
SkilltreeClientState.setUnlockedAbilities(unlockedAbilities);
|
||||
SkilltreeClientState.setCurrentSkillPoints(skillPoints);
|
||||
SkilltreeClientState.setAbilitySlots(abilitySlots);
|
||||
}
|
||||
|
||||
public static void handleSkillDefinitionsSync(Iterable<SkillNode> skillNodes,
|
||||
Iterable<AbilityNode> abilityNodes) {
|
||||
SkillNodeRegistry.clear();
|
||||
for (SkillNode node : skillNodes) {
|
||||
SkillNodeRegistry.put(node);
|
||||
}
|
||||
|
||||
AbilityNodeRegistry.clear();
|
||||
for (AbilityNode node : abilityNodes) {
|
||||
AbilityNodeRegistry.put(node);
|
||||
}
|
||||
}
|
||||
|
||||
public static void handleCooldownSync(int slot, int cooldown) {
|
||||
SkilltreeClientState.updateCooldown(slot, cooldown);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.fretux.skillengine.client;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** Shared charcoal and iron surfaces for the skill interface. */
|
||||
final class SkillUi {
|
||||
static final int BACKGROUND = 0xFF181A1D;
|
||||
static final int SURFACE = 0xFF22252A;
|
||||
static final int HEADER = 0xFF292D32;
|
||||
static final int BORDER = 0xFF555B63;
|
||||
static final int ACCENT = 0xFFD0A447;
|
||||
static final int TEXT = 0xFFE7E7E7;
|
||||
static final int MUTED = 0xFFA9ADB2;
|
||||
|
||||
private SkillUi() {}
|
||||
|
||||
static void panel(GuiGraphics gfx, int x, int y, int width, int height) {
|
||||
gfx.fill(x + 3, y + 4, x + width + 3, y + height + 4, 0x66000000);
|
||||
gfx.fill(x - 1, y - 1, x + width + 1, y + height + 1, BORDER);
|
||||
gfx.fill(x, y, x + width, y + height, BACKGROUND);
|
||||
}
|
||||
|
||||
static void heading(GuiGraphics gfx, Font font, String title, String subtitle, int x, int y) {
|
||||
gfx.drawString(font, title, x + 16, y + 14, TEXT, false);
|
||||
gfx.drawString(font, subtitle, x + 16, y + 29, MUTED, false);
|
||||
}
|
||||
|
||||
static Builder button(Component label, Button.OnPress action) {
|
||||
return new Builder(label, action);
|
||||
}
|
||||
|
||||
static final class Builder {
|
||||
private final Component label;
|
||||
private final Button.OnPress action;
|
||||
private int x, y, width = 80, height = 20;
|
||||
|
||||
Builder(Component label, Button.OnPress action) {
|
||||
this.label = label;
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
Builder pos(int x, int y) { this.x = x; this.y = y; return this; }
|
||||
Builder size(int width, int height) { this.width = width; this.height = height; return this; }
|
||||
Button build() { return new IronButton(x, y, width, height, label, action); }
|
||||
}
|
||||
|
||||
private static final class IronButton extends Button {
|
||||
IronButton(int x, int y, int width, int height, Component label, OnPress action) {
|
||||
super(x, y, width, height, label, action, DEFAULT_NARRATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderWidget(GuiGraphics gfx, int mouseX, int mouseY, float partialTick) {
|
||||
boolean focus = active && isHoveredOrFocused();
|
||||
int x = getX(), y = getY();
|
||||
gfx.fill(x, y, x + width, y + height, focus ? MUTED : BORDER);
|
||||
gfx.fill(x + 1, y + 1, x + width - 1, y + height - 1,
|
||||
active ? (focus ? HEADER : SURFACE) : BACKGROUND);
|
||||
renderScrollingString(gfx, Minecraft.getInstance().font, 6, active ? TEXT : MUTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,61 @@
|
||||
package net.fretux.skillengine.client;
|
||||
|
||||
import net.fretux.skillengine.SkillEngine;
|
||||
import net.fretux.skillengine.capability.PlayerSkillData;
|
||||
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.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class SkilltreeClientState {
|
||||
private static final Set<ResourceLocation> unlocked = new HashSet<>();
|
||||
private static final Set<ResourceLocation> unlockedAbilities = new HashSet<>();
|
||||
private static int currentSkillPoints = 0;
|
||||
private static ResourceLocation[] abilitySlots = new ResourceLocation[3];
|
||||
private static int[] clientCooldowns = new int[3];
|
||||
|
||||
public static void updateCooldown(int slot, int cd) {
|
||||
ensureSlot(slot);
|
||||
int index = slot - 1;
|
||||
if (index < 0 || index >= clientCooldowns.length) return;
|
||||
clientCooldowns[index] = Math.max(0, cd);
|
||||
}
|
||||
|
||||
public static int getClientCooldown(int slot) {
|
||||
ensureSlot(slot);
|
||||
int index = slot - 1;
|
||||
if (index < 0 || index >= clientCooldowns.length) return 0;
|
||||
return clientCooldowns[index];
|
||||
}
|
||||
|
||||
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) {
|
||||
public static void setUnlocked(Iterable<ResourceLocation> ids) {
|
||||
unlocked.clear();
|
||||
unlocked.addAll(ids);
|
||||
for (ResourceLocation id : ids) {
|
||||
unlocked.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setUnlockedAbilities(Iterable<ResourceLocation> abilities) {
|
||||
unlockedAbilities.clear();
|
||||
for (ResourceLocation ability : abilities) {
|
||||
unlockedAbilities.add(ability);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setCurrentSkillPoints(int points) {
|
||||
@@ -50,4 +65,51 @@ public class SkilltreeClientState {
|
||||
public static int getCurrentSkillPoints() {
|
||||
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) {
|
||||
ensureSlot(slot);
|
||||
PlayerSkillData.bindHelper(slot, abilityId, abilitySlots);
|
||||
}
|
||||
|
||||
public static void setAbilitySlots(ResourceLocation[] slots) {
|
||||
abilitySlots = new ResourceLocation[slots != null ? slots.length : 3];
|
||||
clientCooldowns = Arrays.copyOf(clientCooldowns, abilitySlots.length);
|
||||
for (int i = 0; i < abilitySlots.length; i++) {
|
||||
abilitySlots[i] = (slots != null && i < slots.length) ? slots[i] : null;
|
||||
}
|
||||
}
|
||||
|
||||
public static ResourceLocation getAbilityInSlot(int slot) {
|
||||
ensureSlot(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;
|
||||
}
|
||||
|
||||
public static ResourceLocation[] getAbilitySlots() {
|
||||
return Arrays.copyOf(abilitySlots, abilitySlots.length);
|
||||
}
|
||||
|
||||
private static void ensureSlot(int slot) {
|
||||
if (slot <= abilitySlots.length) return;
|
||||
abilitySlots = Arrays.copyOf(abilitySlots, slot);
|
||||
clientCooldowns = Arrays.copyOf(clientCooldowns, slot);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
package net.fretux.skillengine.commands;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import net.fretux.skillengine.SkillEngine;
|
||||
import net.fretux.skillengine.skilltree.SkillTreeValidator;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraftforge.event.RegisterCommandsEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = SkillEngine.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
|
||||
public final class SkillEngineCommands {
|
||||
|
||||
private SkillEngineCommands() {}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void register(RegisterCommandsEvent event) {
|
||||
register(event.getDispatcher());
|
||||
}
|
||||
|
||||
private static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
|
||||
dispatcher.register(Commands.literal("skillengine")
|
||||
.requires(source -> source.hasPermission(2))
|
||||
.then(Commands.literal("validate")
|
||||
.executes(context -> validate(context.getSource()))));
|
||||
}
|
||||
|
||||
private static int validate(CommandSourceStack source) {
|
||||
List<String> issues = SkillTreeValidator.validate();
|
||||
if (issues.isEmpty()) {
|
||||
source.sendSuccess(() -> Component.literal("Skill Engine validation passed."), false);
|
||||
return 1;
|
||||
}
|
||||
source.sendFailure(Component.literal("Skill Engine validation found " + issues.size() + " issue(s):"));
|
||||
for (String issue : issues) {
|
||||
SkillEngine.LOGGER.warn("[SkillEngine validate] {}", issue);
|
||||
source.sendFailure(Component.literal("- " + issue));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package net.fretux.skillengine.compat;
|
||||
import net.fretux.ascend.player.PlayerStatsProvider;
|
||||
import net.fretux.skillengine.SkillEngine;
|
||||
import net.fretux.skillengine.capability.SkillEngineCapabilities;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
@@ -33,6 +34,12 @@ public class AscendCompatHandler {
|
||||
initAscendLevel(event.getEntity());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) {
|
||||
if (event.getEntity().level().isClientSide) return;
|
||||
LAST_ASCEND_LEVEL.remove(event.getEntity().getUUID());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onPlayerClone(PlayerEvent.Clone event) {
|
||||
if (event.getEntity().level().isClientSide) return;
|
||||
@@ -61,6 +68,7 @@ public class AscendCompatHandler {
|
||||
int pointsToGive = levelsGained * SKILL_POINTS_PER_ASCEND_LEVEL;
|
||||
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(skillData -> {
|
||||
skillData.addSkillPoints(pointsToGive);
|
||||
skillData.sync((ServerPlayer) player);
|
||||
});
|
||||
}
|
||||
LAST_ASCEND_LEVEL.put(player.getUUID(), currentLevel);
|
||||
|
||||
@@ -8,6 +8,7 @@ import net.fretux.skillengine.network.PacketHandler;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
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;
|
||||
|
||||
@@ -16,9 +17,8 @@ import java.util.*;
|
||||
@Mod.EventBusSubscriber(modid = SkillEngine.MODID)
|
||||
public class AscendResetDetector {
|
||||
private static final String[] ASCEND_ATTRIBUTES = new String[]{
|
||||
"strength", "agility", "fortitude", "intelligence",
|
||||
"willpower", "charisma",
|
||||
"light_scaling", "medium_scaling", "heavy_scaling", "magic_scaling"
|
||||
"vigor", "mind", "strength", "agility", "intelligence",
|
||||
"willpower", "charisma", "arcane"
|
||||
};
|
||||
private static final Map<UUID, Integer> LAST_ATTRIBUTE_SUM = new HashMap<>();
|
||||
private static final Map<UUID, Integer> LAST_UNSPENT = new HashMap<>();
|
||||
@@ -51,6 +51,14 @@ public class AscendResetDetector {
|
||||
});
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) {
|
||||
if (event.getEntity().level().isClientSide) return;
|
||||
UUID id = event.getEntity().getUUID();
|
||||
LAST_ATTRIBUTE_SUM.remove(id);
|
||||
LAST_UNSPENT.remove(id);
|
||||
}
|
||||
|
||||
private static void resetSkillEngine(PlayerSkillData data, Player player) {
|
||||
int refund = data.getTotalSkillCost();
|
||||
data.addSkillPoints(refund);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.fretux.skillengine.config;
|
||||
|
||||
import net.minecraftforge.common.ForgeConfigSpec;
|
||||
|
||||
public final class SkillEngineConfig {
|
||||
|
||||
public static final ForgeConfigSpec COMMON_SPEC;
|
||||
public static final ForgeConfigSpec.IntValue ABILITY_SLOT_COUNT;
|
||||
|
||||
static {
|
||||
ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder();
|
||||
ABILITY_SLOT_COUNT = builder
|
||||
.comment("Number of ability slots available to each player.")
|
||||
.defineInRange("abilitySlotCount", 3, 1, 9);
|
||||
COMMON_SPEC = builder.build();
|
||||
}
|
||||
|
||||
private SkillEngineConfig() {}
|
||||
}
|
||||
@@ -14,12 +14,16 @@ import net.minecraftforge.network.PacketDistributor;
|
||||
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(), data.getSkillPoints())
|
||||
);
|
||||
});
|
||||
PacketHandler.syncSkillDefinitionsTo(player);
|
||||
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> PacketHandler.CHANNEL.send(
|
||||
PacketDistributor.PLAYER.with(() -> player),
|
||||
new ClientboundSyncSkillsPacket(
|
||||
data.getUnlockedNodes(),
|
||||
data.getUnlockedAbilities(),
|
||||
data.getSkillPoints(),
|
||||
data.getAbilitySlots()
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
@@ -29,6 +33,22 @@ public class SkillEnginePlayerEvents {
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onClone(PlayerEvent.Clone event) {
|
||||
if (event.getEntity() instanceof ServerPlayer newPlayer && event.getOriginal() instanceof ServerPlayer oldPlayer) {
|
||||
oldPlayer.reviveCaps();
|
||||
try {
|
||||
oldPlayer.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(oldData ->
|
||||
newPlayer.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(newData ->
|
||||
newData.load(oldData.save())
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
oldPlayer.invalidateCaps();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onRespawn(PlayerEvent.PlayerRespawnEvent event) {
|
||||
if (event.getEntity() instanceof ServerPlayer player) {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package net.fretux.skillengine.events;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
|
||||
public class SkillPointsChangedEvent extends Event {
|
||||
|
||||
public enum Reason {
|
||||
GAINED,
|
||||
SPENT,
|
||||
REFUNDED
|
||||
}
|
||||
|
||||
private final Player player;
|
||||
private final int amount;
|
||||
private final Reason reason;
|
||||
private final ResourceLocation nodeId;
|
||||
|
||||
public SkillPointsChangedEvent(Player player, int amount, Reason reason, ResourceLocation nodeId) {
|
||||
this.player = player;
|
||||
this.amount = amount;
|
||||
this.reason = reason;
|
||||
this.nodeId = nodeId;
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
public int getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public ResourceLocation getNodeId() {
|
||||
return nodeId;
|
||||
}
|
||||
}
|
||||
@@ -2,30 +2,46 @@ package net.fretux.skillengine.network;
|
||||
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.fml.DistExecutor;
|
||||
import net.minecraftforge.network.NetworkEvent;
|
||||
import net.fretux.skillengine.client.SkilltreeClientState;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ClientboundNodeUnlockedPacket {
|
||||
|
||||
private final ResourceLocation id;
|
||||
private final int newSkillPoints;
|
||||
private final boolean ability;
|
||||
|
||||
public ClientboundNodeUnlockedPacket(ResourceLocation id) {
|
||||
public ClientboundNodeUnlockedPacket(ResourceLocation id, int newSkillPoints, boolean ability) {
|
||||
this.id = id;
|
||||
this.newSkillPoints = newSkillPoints;
|
||||
this.ability = ability;
|
||||
}
|
||||
|
||||
public static void encode(ClientboundNodeUnlockedPacket msg, FriendlyByteBuf buf) {
|
||||
buf.writeResourceLocation(msg.id);
|
||||
buf.writeInt(msg.newSkillPoints);
|
||||
buf.writeBoolean(msg.ability);
|
||||
}
|
||||
|
||||
public static ClientboundNodeUnlockedPacket decode(FriendlyByteBuf buf) {
|
||||
return new ClientboundNodeUnlockedPacket(buf.readResourceLocation());
|
||||
ResourceLocation id = buf.readResourceLocation();
|
||||
int points = buf.readInt();
|
||||
boolean ability = buf.readBoolean();
|
||||
return new ClientboundNodeUnlockedPacket(id, points, ability);
|
||||
}
|
||||
|
||||
public static void handle(ClientboundNodeUnlockedPacket msg, Supplier<NetworkEvent.Context> ctx) {
|
||||
ctx.get().enqueueWork(() -> {
|
||||
SkilltreeClientState.unlockNode(msg.id);
|
||||
DistExecutor.unsafeRunWhenOn(
|
||||
Dist.CLIENT,
|
||||
() -> () -> net.fretux.skillengine.client.ClientSkillEngineBridge.handleNodeUnlocked(
|
||||
msg.id,
|
||||
msg.newSkillPoints,
|
||||
msg.ability
|
||||
)
|
||||
);
|
||||
});
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package net.fretux.skillengine.network;
|
||||
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.fml.DistExecutor;
|
||||
import net.minecraftforge.network.NetworkEvent;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ClientboundSyncCooldownPacket {
|
||||
|
||||
private final int slot;
|
||||
private final int cooldown;
|
||||
|
||||
public ClientboundSyncCooldownPacket(int slot, int cooldown) {
|
||||
this.slot = slot;
|
||||
this.cooldown = cooldown;
|
||||
}
|
||||
|
||||
public static void encode(ClientboundSyncCooldownPacket msg, FriendlyByteBuf buf) {
|
||||
buf.writeInt(msg.slot);
|
||||
buf.writeInt(msg.cooldown);
|
||||
}
|
||||
|
||||
public static ClientboundSyncCooldownPacket decode(FriendlyByteBuf buf) {
|
||||
return new ClientboundSyncCooldownPacket(buf.readInt(), buf.readInt());
|
||||
}
|
||||
|
||||
public static void handle(ClientboundSyncCooldownPacket msg, Supplier<NetworkEvent.Context> ctx) {
|
||||
ctx.get().enqueueWork(() ->
|
||||
DistExecutor.unsafeRunWhenOn(
|
||||
Dist.CLIENT,
|
||||
() -> () -> net.fretux.skillengine.client.ClientSkillEngineBridge.handleCooldownSync(
|
||||
msg.slot,
|
||||
msg.cooldown
|
||||
)
|
||||
)
|
||||
);
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
package net.fretux.skillengine.network;
|
||||
|
||||
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||
import net.fretux.skillengine.skilltree.AbilityNodeRegistry;
|
||||
import net.fretux.skillengine.skilltree.SkillNode;
|
||||
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.fml.DistExecutor;
|
||||
import net.minecraftforge.network.NetworkEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ClientboundSyncSkillDefinitionsPacket {
|
||||
|
||||
private static final int MAX_SYNCED_NODES = 4096;
|
||||
private static final int MAX_LIST_ENTRIES = 512;
|
||||
private static final int MAX_PREREQUISITES = 256;
|
||||
|
||||
private final List<SkillNode> skillNodes;
|
||||
private final List<AbilityNode> abilityNodes;
|
||||
|
||||
public ClientboundSyncSkillDefinitionsPacket(Collection<SkillNode> skillNodes,
|
||||
Collection<AbilityNode> abilityNodes) {
|
||||
this.skillNodes = List.copyOf(skillNodes);
|
||||
this.abilityNodes = List.copyOf(abilityNodes);
|
||||
}
|
||||
|
||||
public static void encode(ClientboundSyncSkillDefinitionsPacket msg, FriendlyByteBuf buf) {
|
||||
buf.writeInt(msg.skillNodes.size());
|
||||
for (SkillNode node : msg.skillNodes) {
|
||||
writeSkillNode(buf, node);
|
||||
}
|
||||
|
||||
buf.writeInt(msg.abilityNodes.size());
|
||||
for (AbilityNode node : msg.abilityNodes) {
|
||||
writeAbilityNode(buf, node);
|
||||
}
|
||||
}
|
||||
|
||||
public static ClientboundSyncSkillDefinitionsPacket decode(FriendlyByteBuf buf) {
|
||||
int skillNodeCount = readBoundedCount(buf, MAX_SYNCED_NODES, "skill node definitions");
|
||||
List<SkillNode> skillNodes = new ArrayList<>(skillNodeCount);
|
||||
for (int i = 0; i < skillNodeCount; i++) {
|
||||
skillNodes.add(readSkillNode(buf));
|
||||
}
|
||||
|
||||
int abilityNodeCount = readBoundedCount(buf, MAX_SYNCED_NODES, "ability node definitions");
|
||||
List<AbilityNode> abilityNodes = new ArrayList<>(abilityNodeCount);
|
||||
for (int i = 0; i < abilityNodeCount; i++) {
|
||||
abilityNodes.add(readAbilityNode(buf));
|
||||
}
|
||||
|
||||
return new ClientboundSyncSkillDefinitionsPacket(skillNodes, abilityNodes);
|
||||
}
|
||||
|
||||
public static void handle(ClientboundSyncSkillDefinitionsPacket msg, Supplier<NetworkEvent.Context> ctx) {
|
||||
ctx.get().enqueueWork(() -> DistExecutor.unsafeRunWhenOn(
|
||||
Dist.CLIENT,
|
||||
() -> () -> net.fretux.skillengine.client.ClientSkillEngineBridge.handleSkillDefinitionsSync(
|
||||
msg.skillNodes,
|
||||
msg.abilityNodes
|
||||
)
|
||||
));
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
|
||||
private static void writeSkillNode(FriendlyByteBuf buf, SkillNode node) {
|
||||
buf.writeResourceLocation(node.getId());
|
||||
buf.writeComponent(node.getTitle());
|
||||
buf.writeComponent(node.getDescription());
|
||||
buf.writeInt(node.getCost());
|
||||
buf.writeFloat(node.getX());
|
||||
buf.writeFloat(node.getY());
|
||||
writeResourceLocationList(buf, node.getLinks());
|
||||
writeResourceLocationList(buf, node.getTags());
|
||||
buf.writeResourceLocation(node.getTree());
|
||||
buf.writeUtf(node.getCategory());
|
||||
buf.writeUtf(node.getLayer());
|
||||
buf.writeResourceLocation(node.getIcons());
|
||||
writePrerequisites(buf, node.getPrereqAttributes());
|
||||
writeResourceLocationList(buf, node.getExclusiveWith());
|
||||
}
|
||||
|
||||
private static SkillNode readSkillNode(FriendlyByteBuf buf) {
|
||||
ResourceLocation id = buf.readResourceLocation();
|
||||
Component title = buf.readComponent();
|
||||
Component description = buf.readComponent();
|
||||
int cost = buf.readInt();
|
||||
float x = buf.readFloat();
|
||||
float y = buf.readFloat();
|
||||
List<ResourceLocation> links = readResourceLocationList(buf);
|
||||
List<ResourceLocation> tags = readResourceLocationList(buf);
|
||||
ResourceLocation tree = buf.readResourceLocation();
|
||||
String category = buf.readUtf();
|
||||
String layer = buf.readUtf();
|
||||
ResourceLocation icon = buf.readResourceLocation();
|
||||
Map<String, Integer> prerequisites = readPrerequisites(buf);
|
||||
List<ResourceLocation> exclusiveWith = readResourceLocationList(buf);
|
||||
return new SkillNode(id, title, description, cost, x, y, links, tags, tree, category, layer, icon, prerequisites, exclusiveWith);
|
||||
}
|
||||
|
||||
private static void writeAbilityNode(FriendlyByteBuf buf, AbilityNode node) {
|
||||
buf.writeResourceLocation(node.getId());
|
||||
buf.writeComponent(node.getTitle());
|
||||
buf.writeComponent(node.getDescription());
|
||||
buf.writeFloat(node.getX());
|
||||
buf.writeFloat(node.getY());
|
||||
writeResourceLocationList(buf, node.getLinks());
|
||||
writeResourceLocationList(buf, node.getTags());
|
||||
buf.writeResourceLocation(node.getIcon());
|
||||
buf.writeInt(node.getCooldown());
|
||||
}
|
||||
|
||||
private static AbilityNode readAbilityNode(FriendlyByteBuf buf) {
|
||||
ResourceLocation id = buf.readResourceLocation();
|
||||
Component title = buf.readComponent();
|
||||
Component description = buf.readComponent();
|
||||
float x = buf.readFloat();
|
||||
float y = buf.readFloat();
|
||||
List<ResourceLocation> links = readResourceLocationList(buf);
|
||||
List<ResourceLocation> tags = readResourceLocationList(buf);
|
||||
ResourceLocation icon = buf.readResourceLocation();
|
||||
int cooldown = buf.readInt();
|
||||
return new AbilityNode(id, title, description, x, y, links, tags, icon, cooldown);
|
||||
}
|
||||
|
||||
private static void writeResourceLocationList(FriendlyByteBuf buf, List<ResourceLocation> ids) {
|
||||
buf.writeInt(ids.size());
|
||||
for (ResourceLocation id : ids) {
|
||||
buf.writeResourceLocation(id);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ResourceLocation> readResourceLocationList(FriendlyByteBuf buf) {
|
||||
int size = readBoundedCount(buf, MAX_LIST_ENTRIES, "resource location list");
|
||||
List<ResourceLocation> ids = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
ids.add(buf.readResourceLocation());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static void writePrerequisites(FriendlyByteBuf buf, Map<String, Integer> prerequisites) {
|
||||
buf.writeInt(prerequisites.size());
|
||||
for (Map.Entry<String, Integer> entry : prerequisites.entrySet()) {
|
||||
buf.writeUtf(entry.getKey());
|
||||
buf.writeInt(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Integer> readPrerequisites(FriendlyByteBuf buf) {
|
||||
int size = readBoundedCount(buf, MAX_PREREQUISITES, "prerequisites");
|
||||
Map<String, Integer> prerequisites = new HashMap<>(size);
|
||||
for (int i = 0; i < size; i++) {
|
||||
prerequisites.put(buf.readUtf(), buf.readInt());
|
||||
}
|
||||
return prerequisites;
|
||||
}
|
||||
|
||||
private static int readBoundedCount(FriendlyByteBuf buf, int max, String fieldName) {
|
||||
int count = buf.readInt();
|
||||
if (count < 0 || count > max) {
|
||||
throw new IllegalArgumentException("Invalid " + fieldName + " count: " + count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package net.fretux.skillengine.network;
|
||||
|
||||
import net.fretux.skillengine.client.SkilltreeClientState;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.fml.DistExecutor;
|
||||
import net.minecraftforge.network.NetworkEvent;
|
||||
|
||||
import java.util.HashSet;
|
||||
@@ -11,37 +12,79 @@ import java.util.function.Supplier;
|
||||
|
||||
public class ClientboundSyncSkillsPacket {
|
||||
|
||||
private final Set<ResourceLocation> unlocked;
|
||||
private final int skillPoints;
|
||||
private static final int MAX_SYNCED_NODES = 4096;
|
||||
private static final int MAX_ABILITY_SLOTS = 64;
|
||||
|
||||
public ClientboundSyncSkillsPacket(Set<ResourceLocation> unlocked, int skillPoints) {
|
||||
private final Set<ResourceLocation> unlocked;
|
||||
private final Set<ResourceLocation> unlockedAbilities;
|
||||
private final int skillPoints;
|
||||
private final ResourceLocation[] abilitySlots;
|
||||
|
||||
public ClientboundSyncSkillsPacket(Set<ResourceLocation> unlocked,
|
||||
Set<ResourceLocation> unlockedAbilities,
|
||||
int skillPoints,
|
||||
ResourceLocation[] abilitySlots) {
|
||||
this.unlocked = unlocked;
|
||||
this.unlockedAbilities = unlockedAbilities;
|
||||
this.skillPoints = skillPoints;
|
||||
this.abilitySlots = abilitySlots;
|
||||
}
|
||||
|
||||
public static void encode(ClientboundSyncSkillsPacket msg, FriendlyByteBuf buf) {
|
||||
buf.writeInt(msg.unlocked.size());
|
||||
for (ResourceLocation id : msg.unlocked) {
|
||||
buf.writeResourceLocation(id);
|
||||
}
|
||||
for (ResourceLocation id : msg.unlocked) buf.writeResourceLocation(id);
|
||||
buf.writeInt(msg.unlockedAbilities.size());
|
||||
for (ResourceLocation id : msg.unlockedAbilities) buf.writeResourceLocation(id);
|
||||
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) {
|
||||
int size = buf.readInt();
|
||||
Set<ResourceLocation> result = new HashSet<>();
|
||||
for (int i = 0; i < size; i++) {
|
||||
result.add(buf.readResourceLocation());
|
||||
}
|
||||
int size = readBoundedCount(buf, MAX_SYNCED_NODES, "unlocked skill nodes");
|
||||
Set<ResourceLocation> unlocked = new HashSet<>();
|
||||
for (int i = 0; i < size; i++) unlocked.add(buf.readResourceLocation());
|
||||
int abilitySize = readBoundedCount(buf, MAX_SYNCED_NODES, "unlocked abilities");
|
||||
Set<ResourceLocation> unlockedAbilities = new HashSet<>();
|
||||
for (int i = 0; i < abilitySize; i++) unlockedAbilities.add(buf.readResourceLocation());
|
||||
int points = buf.readInt();
|
||||
return new ClientboundSyncSkillsPacket(result, points);
|
||||
int slotsCount = readBoundedCount(buf, MAX_ABILITY_SLOTS, "ability slots");
|
||||
ResourceLocation[] slots = new ResourceLocation[slotsCount];
|
||||
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) {
|
||||
ctx.get().enqueueWork(() -> {
|
||||
SkilltreeClientState.setUnlocked(msg.unlocked);
|
||||
SkilltreeClientState.setCurrentSkillPoints(msg.skillPoints);
|
||||
DistExecutor.unsafeRunWhenOn(
|
||||
Dist.CLIENT,
|
||||
() -> () -> net.fretux.skillengine.client.ClientSkillEngineBridge.handleSkillsSync(
|
||||
msg.unlocked,
|
||||
msg.unlockedAbilities,
|
||||
msg.skillPoints,
|
||||
msg.abilitySlots
|
||||
)
|
||||
);
|
||||
});
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
|
||||
private static int readBoundedCount(FriendlyByteBuf buf, int max, String fieldName) {
|
||||
int count = buf.readInt();
|
||||
if (count < 0 || count > max) {
|
||||
throw new IllegalArgumentException("Invalid " + fieldName + " count: " + count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.fretux.skillengine.network;
|
||||
|
||||
import net.fretux.skillengine.capability.SkillEngineCapabilities;
|
||||
import net.fretux.skillengine.skilltree.AbilityNodeRegistry;
|
||||
import net.fretux.skillengine.skilltree.SkillNodeRegistry;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraftforge.network.NetworkDirection;
|
||||
@@ -11,9 +13,9 @@ import net.minecraftforge.network.simple.SimpleChannel;
|
||||
|
||||
public class PacketHandler {
|
||||
|
||||
private static final String PROTOCOL = "1";
|
||||
private static final String PROTOCOL = "4";
|
||||
public static final SimpleChannel CHANNEL = NetworkRegistry.ChannelBuilder
|
||||
.named(new ResourceLocation(SkillEngine.MODID, "network"))
|
||||
.named(ResourceLocation.fromNamespaceAndPath(SkillEngine.MODID, "network"))
|
||||
.clientAcceptedVersions(PROTOCOL::equals)
|
||||
.serverAcceptedVersions(PROTOCOL::equals)
|
||||
.networkProtocolVersion(() -> PROTOCOL)
|
||||
@@ -37,13 +39,48 @@ public class PacketHandler {
|
||||
.decoder(ClientboundSyncSkillsPacket::decode)
|
||||
.consumerMainThread(ClientboundSyncSkillsPacket::handle)
|
||||
.add();
|
||||
CHANNEL.messageBuilder(ClientboundSyncSkillDefinitionsPacket.class, id++, NetworkDirection.PLAY_TO_CLIENT)
|
||||
.encoder(ClientboundSyncSkillDefinitionsPacket::encode)
|
||||
.decoder(ClientboundSyncSkillDefinitionsPacket::decode)
|
||||
.consumerMainThread(ClientboundSyncSkillDefinitionsPacket::handle)
|
||||
.add();
|
||||
CHANNEL.messageBuilder(ServerboundBindAbilityPacket.class, id++, NetworkDirection.PLAY_TO_SERVER)
|
||||
.encoder(ServerboundBindAbilityPacket::encode)
|
||||
.decoder(ServerboundBindAbilityPacket::decode)
|
||||
.consumerMainThread(ServerboundBindAbilityPacket::handle)
|
||||
.add();
|
||||
CHANNEL.messageBuilder(ClientboundSyncCooldownPacket.class, id++, NetworkDirection.PLAY_TO_CLIENT)
|
||||
.encoder(ClientboundSyncCooldownPacket::encode)
|
||||
.decoder(ClientboundSyncCooldownPacket::decode)
|
||||
.consumerMainThread(ClientboundSyncCooldownPacket::handle)
|
||||
.add();
|
||||
CHANNEL.messageBuilder(ServerboundActivateAbilityPacket.class, id++, NetworkDirection.PLAY_TO_SERVER)
|
||||
.encoder(ServerboundActivateAbilityPacket::encode)
|
||||
.decoder(ServerboundActivateAbilityPacket::decode)
|
||||
.consumerMainThread(ServerboundActivateAbilityPacket::handle)
|
||||
.add();
|
||||
}
|
||||
|
||||
public static void syncSkillsTo(ServerPlayer player) {
|
||||
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
public static void syncSkillDefinitionsTo(ServerPlayer player) {
|
||||
CHANNEL.send(
|
||||
PacketDistributor.PLAYER.with(() -> player),
|
||||
new ClientboundSyncSkillDefinitionsPacket(
|
||||
SkillNodeRegistry.all(),
|
||||
AbilityNodeRegistry.all()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package net.fretux.skillengine.network;
|
||||
|
||||
import net.fretux.skillengine.capability.SkillEngineCapabilities;
|
||||
import net.fretux.skillengine.skilltree.AbilityNode;
|
||||
import net.fretux.skillengine.skilltree.AbilityNodeRegistry;
|
||||
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 ServerboundActivateAbilityPacket {
|
||||
|
||||
private final int slot;
|
||||
|
||||
public ServerboundActivateAbilityPacket(int slot) {
|
||||
this.slot = slot;
|
||||
}
|
||||
|
||||
public static void encode(ServerboundActivateAbilityPacket msg, FriendlyByteBuf buf) {
|
||||
buf.writeInt(msg.slot);
|
||||
}
|
||||
|
||||
public static ServerboundActivateAbilityPacket decode(FriendlyByteBuf buf) {
|
||||
return new ServerboundActivateAbilityPacket(buf.readInt());
|
||||
}
|
||||
|
||||
public static void handle(ServerboundActivateAbilityPacket msg, Supplier<NetworkEvent.Context> ctx) {
|
||||
ctx.get().enqueueWork(() -> {
|
||||
|
||||
ServerPlayer player = ctx.get().getSender();
|
||||
if (player == null) return;
|
||||
|
||||
player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> {
|
||||
if (msg.slot < 1 || msg.slot > data.getAbilitySlots().length) return;
|
||||
|
||||
ResourceLocation abilityId = data.getAbilityInSlot(msg.slot);
|
||||
if (abilityId == null) return;
|
||||
if (!data.isAbilityUnlocked(abilityId)) return;
|
||||
|
||||
AbilityNode ability = AbilityNodeRegistry.get(abilityId);
|
||||
if (ability == null) return;
|
||||
|
||||
int cd = data.getCooldown(msg.slot);
|
||||
if (cd > 0) return;
|
||||
ability.execute(player);
|
||||
int cooldown = Math.max(0, ability.getCooldown());
|
||||
data.setCooldown(msg.slot, cooldown);
|
||||
PacketHandler.CHANNEL.send(
|
||||
PacketDistributor.PLAYER.with(() -> player),
|
||||
new ClientboundSyncCooldownPacket(msg.slot, cooldown)
|
||||
);
|
||||
});
|
||||
});
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.fretux.skillengine.network;
|
||||
|
||||
import net.fretux.skillengine.capability.SkillEngineCapabilities;
|
||||
import net.fretux.skillengine.skilltree.AbilityNodeRegistry;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
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 -> {
|
||||
if (msg.slot < 1 || msg.slot > data.getAbilitySlots().length) return;
|
||||
if (AbilityNodeRegistry.get(msg.abilityId) == null) return;
|
||||
if (!data.isAbilityUnlocked(msg.abilityId)) return;
|
||||
data.bindAbility(msg.slot, msg.abilityId);
|
||||
});
|
||||
PacketHandler.syncSkillsTo(player);
|
||||
});
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
package net.fretux.skillengine.network;
|
||||
|
||||
import net.fretux.skillengine.SkillEngine;
|
||||
import net.fretux.skillengine.capability.PlayerSkillData;
|
||||
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.fretux.skillengine.events.SkillPointsChangedEvent;
|
||||
import net.fretux.skillengine.skilltree.*;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.network.NetworkEvent;
|
||||
import net.minecraftforge.network.PacketDistributor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ServerboundUnlockNodePacket {
|
||||
|
||||
private final ResourceLocation nodeId;
|
||||
|
||||
public ServerboundUnlockNodePacket(ResourceLocation nodeId) {
|
||||
@@ -32,33 +33,59 @@ public class ServerboundUnlockNodePacket {
|
||||
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;
|
||||
}
|
||||
if (player == null) return;
|
||||
SkillNode node = SkillNodeRegistry.get(msg.nodeId);
|
||||
if (node == null) {
|
||||
SkillEngine.LOGGER.warn("Unlock packet for unknown node {}", msg.nodeId);
|
||||
return;
|
||||
}
|
||||
AbilityNode ability = AbilityNodeRegistry.get(msg.nodeId);
|
||||
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, player, 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());
|
||||
if (node != null) {
|
||||
List<SkillNode> unlockPlan = SkillLogic.getUnlockPlan(data, player, node).orElse(null);
|
||||
if (unlockPlan != null) {
|
||||
for (SkillNode plannedNode : unlockPlan) {
|
||||
data.unlockNode(plannedNode);
|
||||
int cost = Math.max(0, plannedNode.getCost());
|
||||
if (cost > 0) {
|
||||
MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent(
|
||||
player,
|
||||
cost,
|
||||
SkillPointsChangedEvent.Reason.SPENT,
|
||||
plannedNode.getId()
|
||||
));
|
||||
}
|
||||
}
|
||||
PacketHandler.syncSkillsTo(player);
|
||||
PacketHandler.CHANNEL.send(
|
||||
PacketDistributor.PLAYER.with(() -> player),
|
||||
new ClientboundNodeUnlockedPacket(node.getId(), data.getSkillPoints(), false)
|
||||
);
|
||||
} else {
|
||||
SkillEngine.LOGGER.info("[SKILLENGINE] Rejecting unlock for {}", node.getId());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ability != null) {
|
||||
if (canUnlockAbility(player, data, ability)) {
|
||||
data.unlockAbility(ability);
|
||||
PacketHandler.syncSkillsTo(player);
|
||||
PacketHandler.CHANNEL.send(
|
||||
PacketDistributor.PLAYER.with(() -> player),
|
||||
new ClientboundNodeUnlockedPacket(ability.getId(), data.getSkillPoints(), true)
|
||||
);
|
||||
} else {
|
||||
SkillEngine.LOGGER.info("[SKILLENGINE] Rejecting ability unlock for {}", ability.getId());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
|
||||
|
||||
private static boolean canUnlockAbility(ServerPlayer player, PlayerSkillData data, AbilityNode ability) {
|
||||
if (data.isAbilityUnlocked(ability.getId())) return false;
|
||||
for (ResourceLocation parent : ability.getLinks()) {
|
||||
boolean ok = data.isUnlocked(parent) || data.isAbilityUnlocked(parent);
|
||||
if (!ok) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package net.fretux.skillengine.skilltree;
|
||||
|
||||
import net.fretux.skillengine.api.AbilityHandlerRegistry;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
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; }
|
||||
|
||||
public void execute(ServerPlayer player) {
|
||||
AbilityHandlerRegistry.execute(player, this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
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,
|
||||
@NotNull ResourceManager resourceManager,
|
||||
@NotNull 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(ResourceLocation.parse(e.getAsString())));
|
||||
List<ResourceLocation> tags = new ArrayList<>();
|
||||
obj.getAsJsonArray("tags").forEach(e ->
|
||||
tags.add(ResourceLocation.parse(e.getAsString())));
|
||||
ResourceLocation icon = ResourceLocation.parse(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,35 @@
|
||||
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<>();
|
||||
private static final Map<ResourceLocation, Integer> DUPLICATE_COUNTS = new LinkedHashMap<>();
|
||||
|
||||
public static void clear() {
|
||||
ABILITIES.clear();
|
||||
DUPLICATE_COUNTS.clear();
|
||||
}
|
||||
|
||||
public static void put(AbilityNode node) {
|
||||
if (ABILITIES.put(node.getId(), node) != null) {
|
||||
DUPLICATE_COUNTS.merge(node.getId(), 1, Integer::sum);
|
||||
}
|
||||
}
|
||||
|
||||
public static AbilityNode get(ResourceLocation id) {
|
||||
return ABILITIES.get(id);
|
||||
}
|
||||
|
||||
public static Collection<AbilityNode> all() {
|
||||
return ABILITIES.values();
|
||||
}
|
||||
|
||||
public static Map<ResourceLocation, Integer> duplicateCounts() {
|
||||
return Map.copyOf(DUPLICATE_COUNTS);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,113 @@
|
||||
package net.fretux.skillengine.skilltree;
|
||||
|
||||
import net.fretux.ascend.player.PlayerStatsProvider;
|
||||
import net.fretux.skillengine.api.PrerequisiteProviderRegistry;
|
||||
import net.fretux.skillengine.capability.PlayerSkillData;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public class SkillLogic {
|
||||
|
||||
public static boolean canUnlock(PlayerSkillData data, Player player, SkillNode node) {
|
||||
if (data.isUnlocked(node.getId())) return false;
|
||||
if (data.getSkillPoints() < node.getCost()) return false;
|
||||
if (node.isRoot()) return true;
|
||||
if (!meetsAscendPrerequisites(player, node)) return false;
|
||||
for (ResourceLocation neighborId : node.getLinks()) {
|
||||
if (data.isUnlocked(neighborId)) return true;
|
||||
return getUnlockPlan(data, player, node).isPresent();
|
||||
}
|
||||
|
||||
public static Optional<List<SkillNode>> getUnlockPlan(PlayerSkillData data, Player player, SkillNode node) {
|
||||
if (data == null || player == null || node == null || data.isUnlocked(node.getId())) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
List<SkillNode> plan = new ArrayList<>();
|
||||
Set<ResourceLocation> planned = new HashSet<>();
|
||||
Set<ResourceLocation> visiting = new HashSet<>();
|
||||
if (!collectUnlockPlan(data, player, node, plan, planned, visiting)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
int totalCost = 0;
|
||||
for (SkillNode plannedNode : plan) {
|
||||
totalCost += Math.max(0, plannedNode.getCost());
|
||||
}
|
||||
if (data.getSkillPoints() < totalCost) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(List.copyOf(plan));
|
||||
}
|
||||
|
||||
public static boolean meetsAscendPrerequisites(Player player, SkillNode node) {
|
||||
if (node.getPrereqAttributes().isEmpty()) return true;
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) return true;
|
||||
for (Map.Entry<String, Integer> req : node.getPrereqAttributes().entrySet()) {
|
||||
if (!PrerequisiteProviderRegistry.meets(serverPlayer, node, req.getKey(), req.getValue())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean collectUnlockPlan(PlayerSkillData data,
|
||||
Player player,
|
||||
SkillNode node,
|
||||
List<SkillNode> plan,
|
||||
Set<ResourceLocation> planned,
|
||||
Set<ResourceLocation> visiting) {
|
||||
ResourceLocation id = node.getId();
|
||||
if (data.isUnlocked(id) || planned.contains(id)) {
|
||||
return true;
|
||||
}
|
||||
if (!visiting.add(id)) {
|
||||
return false;
|
||||
}
|
||||
if (!meetsAscendPrerequisites(player, node) || conflictsWithUnlocked(data, node)) {
|
||||
visiting.remove(id);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (ResourceLocation parentId : node.getLinks()) {
|
||||
if (data.isUnlocked(parentId)) {
|
||||
continue;
|
||||
}
|
||||
SkillNode parent = SkillNodeRegistry.get(parentId);
|
||||
if (parent == null || !collectUnlockPlan(data, player, parent, plan, planned, visiting)) {
|
||||
visiting.remove(id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (conflictsWithPlanned(node, planned)) {
|
||||
visiting.remove(id);
|
||||
return false;
|
||||
}
|
||||
|
||||
visiting.remove(id);
|
||||
planned.add(id);
|
||||
plan.add(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean conflictsWithUnlocked(PlayerSkillData data, SkillNode node) {
|
||||
for (ResourceLocation excl : node.getExclusiveWith()) {
|
||||
if (data.isUnlocked(excl)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public static boolean meetsAscendPrerequisites(Player player, SkillNode node) {
|
||||
return player.getCapability(PlayerStatsProvider.PLAYER_STATS).map(stats -> {
|
||||
for (Map.Entry<String, Integer> req : node.getPrereqAttributes().entrySet()) {
|
||||
int current = stats.getAttributeLevel(req.getKey());
|
||||
if (current < req.getValue()) return false;
|
||||
}
|
||||
return true;
|
||||
}).orElse(false);
|
||||
}
|
||||
|
||||
private static boolean conflictsWithPlanned(SkillNode node, Set<ResourceLocation> planned) {
|
||||
for (ResourceLocation excl : node.getExclusiveWith()) {
|
||||
if (planned.contains(excl)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,12 @@ public class SkillNode {
|
||||
private final float y;
|
||||
private final List<ResourceLocation> links;
|
||||
private final List<ResourceLocation> tags;
|
||||
private final ResourceLocation tree;
|
||||
private final String category;
|
||||
private final String layer;
|
||||
private final ResourceLocation icons;
|
||||
private final Map<String, Integer> prereqAttributes;
|
||||
private final List<ResourceLocation> exclusiveWith;
|
||||
|
||||
public SkillNode(ResourceLocation id,
|
||||
Component title,
|
||||
@@ -26,8 +30,12 @@ public class SkillNode {
|
||||
float x, float y,
|
||||
List<ResourceLocation> links,
|
||||
List<ResourceLocation> tags,
|
||||
ResourceLocation tree,
|
||||
String category,
|
||||
String layer,
|
||||
ResourceLocation icons,
|
||||
Map<String, Integer> prereqAttributes) {
|
||||
Map<String, Integer> prereqAttributes,
|
||||
List<ResourceLocation> exclusiveWith){
|
||||
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
@@ -37,8 +45,12 @@ public class SkillNode {
|
||||
this.y = y;
|
||||
this.links = List.copyOf(links);
|
||||
this.tags = List.copyOf(tags);
|
||||
this.tree = tree;
|
||||
this.category = category;
|
||||
this.layer = layer;
|
||||
this.icons = icons;
|
||||
this.prereqAttributes = Map.copyOf(prereqAttributes);
|
||||
this.exclusiveWith = List.copyOf(exclusiveWith);
|
||||
}
|
||||
|
||||
public ResourceLocation getId() {
|
||||
@@ -73,6 +85,18 @@ public class SkillNode {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public ResourceLocation getTree() {
|
||||
return tree;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public String getLayer() {
|
||||
return layer;
|
||||
}
|
||||
|
||||
public ResourceLocation getIcons() {
|
||||
return icons;
|
||||
}
|
||||
@@ -81,6 +105,10 @@ public class SkillNode {
|
||||
return prereqAttributes;
|
||||
}
|
||||
|
||||
public List<ResourceLocation> getExclusiveWith() {
|
||||
return exclusiveWith;
|
||||
}
|
||||
|
||||
public boolean isRoot() {
|
||||
return links.isEmpty();
|
||||
}
|
||||
|
||||
@@ -10,16 +10,22 @@ import net.minecraft.util.profiling.ProfilerFiller;
|
||||
import net.minecraftforge.event.AddReloadListenerEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = SkillEngine.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE)
|
||||
public class SkillNodeLoader extends SimpleJsonResourceReloadListener {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().create();
|
||||
private static final ResourceLocation DEFAULT_TREE =
|
||||
ResourceLocation.fromNamespaceAndPath(SkillEngine.MODID, "main");
|
||||
private static final Pattern TRANSLATION_KEY =
|
||||
Pattern.compile("[a-z0-9_.-]+\\.[a-z0-9_.-]+");
|
||||
|
||||
public SkillNodeLoader() {
|
||||
super(GSON, "skillnodes");
|
||||
@@ -32,25 +38,35 @@ public class SkillNodeLoader extends SimpleJsonResourceReloadListener {
|
||||
|
||||
@Override
|
||||
protected void apply(Map<ResourceLocation, JsonElement> jsons,
|
||||
ResourceManager resourceManager,
|
||||
ProfilerFiller profiler) {
|
||||
@NotNull ResourceManager resourceManager,
|
||||
@NotNull 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();
|
||||
ResourceLocation nodeId = obj.has("id")
|
||||
? ResourceLocation.parse(obj.get("id").getAsString())
|
||||
: id;
|
||||
Component title = readComponent(obj, "title");
|
||||
Component description = readComponent(obj, "description");
|
||||
int cost = obj.get("cost").getAsInt();
|
||||
JsonObject pos = obj.getAsJsonObject("position");
|
||||
float x = pos.get("x").getAsFloat();
|
||||
float y = pos.get("y").getAsFloat();
|
||||
List<ResourceLocation> links = new ArrayList<>();
|
||||
obj.getAsJsonArray("links").forEach(e ->
|
||||
links.add(new ResourceLocation(e.getAsString())));
|
||||
links.add(ResourceLocation.parse(e.getAsString())));
|
||||
List<ResourceLocation> tags = new ArrayList<>();
|
||||
obj.getAsJsonArray("tags").forEach(e ->
|
||||
tags.add(new ResourceLocation(e.getAsString())));
|
||||
ResourceLocation icons = new ResourceLocation(obj.get("icons").getAsString());
|
||||
if (obj.has("tags")) {
|
||||
obj.getAsJsonArray("tags").forEach(e ->
|
||||
tags.add(ResourceLocation.parse(e.getAsString())));
|
||||
}
|
||||
ResourceLocation tree = obj.has("tree")
|
||||
? ResourceLocation.parse(obj.get("tree").getAsString())
|
||||
: ResourceLocation.fromNamespaceAndPath(nodeId.getNamespace(), DEFAULT_TREE.getPath());
|
||||
String category = obj.has("category") ? obj.get("category").getAsString() : "";
|
||||
String layer = obj.has("layer") ? obj.get("layer").getAsString() : "";
|
||||
ResourceLocation icons = ResourceLocation.parse(obj.get("icons").getAsString());
|
||||
Map<String, Integer> prereqAttributes = new HashMap<>();
|
||||
if (obj.has("prerequisites")) {
|
||||
JsonObject prereqObj = obj.getAsJsonObject("prerequisites");
|
||||
@@ -58,19 +74,49 @@ public class SkillNodeLoader extends SimpleJsonResourceReloadListener {
|
||||
prereqAttributes.put(entry.getKey(), entry.getValue().getAsInt());
|
||||
}
|
||||
}
|
||||
List<ResourceLocation> exclusiveWith = new ArrayList<>();
|
||||
if (obj.has("exclusive_with")) {
|
||||
obj.getAsJsonArray("exclusive_with").forEach(e ->
|
||||
exclusiveWith.add(ResourceLocation.parse(e.getAsString())));
|
||||
}
|
||||
SkillNode node = new SkillNode(
|
||||
id,
|
||||
Component.literal(title),
|
||||
Component.literal(description),
|
||||
nodeId,
|
||||
title,
|
||||
description,
|
||||
cost,
|
||||
x, y,
|
||||
links,
|
||||
tags,
|
||||
tree,
|
||||
category,
|
||||
layer,
|
||||
icons,
|
||||
prereqAttributes
|
||||
prereqAttributes,
|
||||
exclusiveWith
|
||||
);
|
||||
|
||||
SkillNodeRegistry.put(node);
|
||||
});
|
||||
SkillEngine.LOGGER.info("Loaded {} skill nodes", SkillNodeRegistry.all().size());
|
||||
}
|
||||
|
||||
private static Component readComponent(JsonObject obj, String key) {
|
||||
if (!obj.has(key)) {
|
||||
return Component.empty();
|
||||
}
|
||||
JsonElement element = obj.get(key);
|
||||
if (element.isJsonObject()) {
|
||||
JsonObject text = element.getAsJsonObject();
|
||||
if (text.has("translate")) {
|
||||
return Component.translatable(text.get("translate").getAsString());
|
||||
}
|
||||
if (text.has("text")) {
|
||||
return Component.literal(text.get("text").getAsString());
|
||||
}
|
||||
}
|
||||
String value = element.getAsString();
|
||||
return TRANSLATION_KEY.matcher(value).matches()
|
||||
? Component.translatable(value)
|
||||
: Component.literal(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,36 @@ package net.fretux.skillengine.skilltree;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SkillNodeRegistry {
|
||||
|
||||
private static final Map<ResourceLocation, SkillNode> NODES = new LinkedHashMap<>();
|
||||
private static final Map<ResourceLocation, Map<ResourceLocation, SkillNode>> NODES_BY_TREE = new LinkedHashMap<>();
|
||||
private static final Map<ResourceLocation, Integer> DUPLICATE_COUNTS = new LinkedHashMap<>();
|
||||
|
||||
public static void clear() {
|
||||
NODES.clear();
|
||||
NODES_BY_TREE.clear();
|
||||
DUPLICATE_COUNTS.clear();
|
||||
}
|
||||
|
||||
public static void put(SkillNode node) {
|
||||
NODES.put(node.getId(), node);
|
||||
SkillNode previous = NODES.put(node.getId(), node);
|
||||
if (previous != null) {
|
||||
DUPLICATE_COUNTS.merge(node.getId(), 1, Integer::sum);
|
||||
Map<ResourceLocation, SkillNode> previousTree = NODES_BY_TREE.get(previous.getTree());
|
||||
if (previousTree != null) {
|
||||
previousTree.remove(previous.getId());
|
||||
if (previousTree.isEmpty()) {
|
||||
NODES_BY_TREE.remove(previous.getTree());
|
||||
}
|
||||
}
|
||||
}
|
||||
NODES_BY_TREE.computeIfAbsent(node.getTree(), ignored -> new LinkedHashMap<>())
|
||||
.put(node.getId(), node);
|
||||
}
|
||||
|
||||
public static SkillNode get(ResourceLocation id) {
|
||||
@@ -25,4 +42,17 @@ public class SkillNodeRegistry {
|
||||
public static Collection<SkillNode> all() {
|
||||
return NODES.values();
|
||||
}
|
||||
|
||||
public static List<ResourceLocation> trees() {
|
||||
return List.copyOf(NODES_BY_TREE.keySet());
|
||||
}
|
||||
|
||||
public static Collection<SkillNode> byTree(ResourceLocation tree) {
|
||||
Map<ResourceLocation, SkillNode> nodes = NODES_BY_TREE.get(tree);
|
||||
return nodes != null ? nodes.values() : List.of();
|
||||
}
|
||||
|
||||
public static Map<ResourceLocation, Integer> duplicateCounts() {
|
||||
return Map.copyOf(DUPLICATE_COUNTS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package net.fretux.skillengine.skilltree;
|
||||
|
||||
import net.fretux.skillengine.api.PrerequisiteProviderRegistry;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SkillTreeValidator {
|
||||
|
||||
private SkillTreeValidator() {}
|
||||
|
||||
public static List<String> validate() {
|
||||
List<String> issues = new ArrayList<>();
|
||||
SkillNodeRegistry.duplicateCounts().forEach((id, count) ->
|
||||
issues.add(id + " was loaded " + (count + 1) + " times; node ids must be unique"));
|
||||
AbilityNodeRegistry.duplicateCounts().forEach((id, count) ->
|
||||
issues.add(id + " was loaded " + (count + 1) + " times; ability ids must be unique"));
|
||||
for (SkillNode node : SkillNodeRegistry.all()) {
|
||||
validateNode(node, issues);
|
||||
}
|
||||
for (AbilityNode ability : AbilityNodeRegistry.all()) {
|
||||
validateAbility(ability, issues);
|
||||
}
|
||||
detectSkillCycles(issues);
|
||||
detectAbilityCycles(issues);
|
||||
return issues;
|
||||
}
|
||||
|
||||
private static void validateNode(SkillNode node, List<String> issues) {
|
||||
if (!isPngTexture(node.getIcons())) {
|
||||
issues.add(node.getId() + " has invalid icon path " + node.getIcons() + " (expected a .png texture)");
|
||||
}
|
||||
if (node.getCost() < 0) {
|
||||
issues.add(node.getId() + " has negative cost " + node.getCost());
|
||||
}
|
||||
for (ResourceLocation parent : node.getLinks()) {
|
||||
if (SkillNodeRegistry.get(parent) == null) {
|
||||
issues.add(node.getId() + " links to missing skill node " + parent);
|
||||
}
|
||||
}
|
||||
for (ResourceLocation exclusive : node.getExclusiveWith()) {
|
||||
SkillNode other = SkillNodeRegistry.get(exclusive);
|
||||
if (other == null) {
|
||||
issues.add(node.getId() + " is exclusive with missing node " + exclusive);
|
||||
} else if (exclusive.equals(node.getId())) {
|
||||
issues.add(node.getId() + " is exclusive with itself");
|
||||
} else if (!other.getExclusiveWith().contains(node.getId())) {
|
||||
issues.add(node.getId() + " is exclusive with " + exclusive + " but the reverse entry is missing");
|
||||
}
|
||||
}
|
||||
Set<ResourceLocation> requiredSkillNodes = collectRequiredSkillNodes(node);
|
||||
for (ResourceLocation exclusive : node.getExclusiveWith()) {
|
||||
if (requiredSkillNodes.contains(exclusive)) {
|
||||
issues.add(node.getId() + " is impossible to unlock because required ancestor " + exclusive + " is mutually exclusive");
|
||||
}
|
||||
}
|
||||
List<ResourceLocation> requiredList = new ArrayList<>(requiredSkillNodes);
|
||||
for (int i = 0; i < requiredList.size(); i++) {
|
||||
SkillNode first = SkillNodeRegistry.get(requiredList.get(i));
|
||||
if (first == null) continue;
|
||||
for (int j = i + 1; j < requiredList.size(); j++) {
|
||||
ResourceLocation second = requiredList.get(j);
|
||||
if (first.getExclusiveWith().contains(second)) {
|
||||
issues.add(node.getId() + " is impossible to unlock because required nodes "
|
||||
+ first.getId() + " and " + second + " are mutually exclusive");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String key : node.getPrereqAttributes().keySet()) {
|
||||
int separator = key.indexOf(':');
|
||||
if (separator >= 0 && (separator == 0 || separator == key.length() - 1)) {
|
||||
issues.add(node.getId() + " has malformed prerequisite key " + key);
|
||||
continue;
|
||||
}
|
||||
String namespace = separator > 0 ? key.substring(0, separator) : "ascend";
|
||||
if (!PrerequisiteProviderRegistry.hasProvider(namespace)) {
|
||||
issues.add(node.getId() + " requires " + key + " but no prerequisite provider is registered for " + namespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateAbility(AbilityNode ability, List<String> issues) {
|
||||
if (!isPngTexture(ability.getIcon())) {
|
||||
issues.add(ability.getId() + " has invalid icon path " + ability.getIcon() + " (expected a .png texture)");
|
||||
}
|
||||
if (ability.getCooldown() < 0) {
|
||||
issues.add(ability.getId() + " has negative cooldown " + ability.getCooldown());
|
||||
}
|
||||
for (ResourceLocation parent : ability.getLinks()) {
|
||||
if (SkillNodeRegistry.get(parent) == null && AbilityNodeRegistry.get(parent) == null) {
|
||||
issues.add(ability.getId() + " links to missing node or ability " + parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPngTexture(ResourceLocation id) {
|
||||
return id != null && id.getPath().endsWith(".png");
|
||||
}
|
||||
|
||||
private static void detectSkillCycles(List<String> issues) {
|
||||
Set<ResourceLocation> visited = new HashSet<>();
|
||||
Set<ResourceLocation> visiting = new HashSet<>();
|
||||
for (SkillNode node : SkillNodeRegistry.all()) {
|
||||
visit(node, visited, visiting, issues);
|
||||
}
|
||||
}
|
||||
|
||||
private static void detectAbilityCycles(List<String> issues) {
|
||||
Set<ResourceLocation> visited = new HashSet<>();
|
||||
Set<ResourceLocation> visiting = new HashSet<>();
|
||||
for (AbilityNode ability : AbilityNodeRegistry.all()) {
|
||||
visitAbility(ability, visited, visiting, issues);
|
||||
}
|
||||
}
|
||||
|
||||
private static void visit(SkillNode node,
|
||||
Set<ResourceLocation> visited,
|
||||
Set<ResourceLocation> visiting,
|
||||
List<String> issues) {
|
||||
ResourceLocation id = node.getId();
|
||||
if (visited.contains(id)) return;
|
||||
if (!visiting.add(id)) {
|
||||
issues.add("Cycle detected at skill node " + id);
|
||||
return;
|
||||
}
|
||||
for (ResourceLocation parentId : node.getLinks()) {
|
||||
SkillNode parent = SkillNodeRegistry.get(parentId);
|
||||
if (parent != null) {
|
||||
visit(parent, visited, visiting, issues);
|
||||
}
|
||||
}
|
||||
visiting.remove(id);
|
||||
visited.add(id);
|
||||
}
|
||||
|
||||
private static void visitAbility(AbilityNode ability,
|
||||
Set<ResourceLocation> visited,
|
||||
Set<ResourceLocation> visiting,
|
||||
List<String> issues) {
|
||||
ResourceLocation id = ability.getId();
|
||||
if (visited.contains(id)) return;
|
||||
if (!visiting.add(id)) {
|
||||
issues.add("Cycle detected at ability node " + id);
|
||||
return;
|
||||
}
|
||||
for (ResourceLocation parentId : ability.getLinks()) {
|
||||
AbilityNode parent = AbilityNodeRegistry.get(parentId);
|
||||
if (parent != null) {
|
||||
visitAbility(parent, visited, visiting, issues);
|
||||
}
|
||||
}
|
||||
visiting.remove(id);
|
||||
visited.add(id);
|
||||
}
|
||||
|
||||
private static Set<ResourceLocation> collectRequiredSkillNodes(SkillNode node) {
|
||||
Set<ResourceLocation> required = new HashSet<>();
|
||||
collectRequiredSkillNodes(node, required, new HashSet<>());
|
||||
required.remove(node.getId());
|
||||
return required;
|
||||
}
|
||||
|
||||
private static void collectRequiredSkillNodes(SkillNode node,
|
||||
Set<ResourceLocation> required,
|
||||
Set<ResourceLocation> visiting) {
|
||||
if (!visiting.add(node.getId())) return;
|
||||
for (ResourceLocation parentId : node.getLinks()) {
|
||||
SkillNode parent = SkillNodeRegistry.get(parentId);
|
||||
if (parent != null && required.add(parentId)) {
|
||||
collectRequiredSkillNodes(parent, required, visiting);
|
||||
}
|
||||
}
|
||||
visiting.remove(node.getId());
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ displayName = "${mod_name}" #mandatory
|
||||
# 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
|
||||
logoFile="skillengine.png" #optional
|
||||
# A text field displayed in the mod UI
|
||||
#credits="" #optional
|
||||
# A text field displayed in the mod UI
|
||||
@@ -69,7 +69,7 @@ side = "BOTH"
|
||||
[[dependencies.${ mod_id }]]
|
||||
modId = "ascend"
|
||||
mandatory = true
|
||||
versionRange = "[2.0.2,)"
|
||||
versionRange = "[3.0.0,)"
|
||||
ordering = "AFTER"
|
||||
side = "BOTH"
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"key.skillengine.openskilltree": "Open Skilltree",
|
||||
"key.skillengine.openabilities": "Open Ability Screen",
|
||||
"key.skillengine.ability1": "Use Skill in first Skilltree Slot",
|
||||
"key.skillengine.ability2": "Use Skill in second Skilltree Slot",
|
||||
"key.skillengine.ability3": "Use Skill in third Skilltree Slot",
|
||||
"key.skillengine.ability4": "Use Skill in fourth Skilltree Slot",
|
||||
"key.skillengine.ability5": "Use Skill in fifth Skilltree Slot",
|
||||
"key.skillengine.ability6": "Use Skill in sixth Skilltree Slot",
|
||||
"key.skillengine.ability7": "Use Skill in seventh Skilltree Slot",
|
||||
"key.skillengine.ability8": "Use Skill in eighth Skilltree Slot",
|
||||
"key.skillengine.ability9": "Use Skill in ninth Skilltree Slot"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"title": "Basic Strength",
|
||||
"description": "You feel your muscles tighten.\nGrants +1 Strength.",
|
||||
"cost": 1,
|
||||
"position": { "x": -150.0, "y": -50.0 },
|
||||
"links": ["skillengine:welcome"],
|
||||
"tags": ["skillengine:strength_1"],
|
||||
"icons": "minecraft:textures/item/iron_sword.png",
|
||||
"prerequisites": {
|
||||
"strength": 20
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"icons": "minecraft:textures/item/leather_boots.png",
|
||||
"prerequisites": {
|
||||
"agility": 20
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"id": "skillengine:welcome",
|
||||
"title": "Welcome to Ascend",
|
||||
"description": "This is your first skill node.\nClick Unlock to test the system.",
|
||||
"cost": 0,
|
||||
|
||||
Reference in New Issue
Block a user