For mod developers. In the Bukkit era a skill pack was a jar dropped in a folder, classloaded by hand, with one Java class per skill sharing a single instance across every player. In ReForged a skill pack is an ordinary NeoForge mod that depends on LegendQuest - so you get versioning, dependency checking and proper classloading for free, and per-player state is handled by the engine.
Start here
Before writing any Java, check whether composing the 14 built-in effect types gets you there. A skill is a data file, and the effects list is what makes it behave - most ideas are a combination of things that already exist.
Layer 1 - no code
Server owners, or your pack's bundled datapack, can define new skills by composing registered effect types. No jar required at all.
# config/legendquest/skills/fire_dash.yml (or JSON in any datapack)
name: Fire Dash
type: active # active | passive | triggered
cooldown: 8000 # all times in milliseconds
mana_cost: 12
effects:
- { type: "legendquest:leap", power: 2.2, lift: 0.3 }
- { type: "legendquest:ignite", duration: 2000,
target: { kind: nearby, radius: 3 } }
- { type: "legendquest:sound", sound: "minecraft:entity.blaze.shoot" }
Built-in effect types in the
legendquest: namespace: damage,
heal, potion_effect, leap,
teleport, lightning, summon,
message, ignite, give_item,
sound, particle_line,
projectile and run_command. Full options for each
are on the content reference.
Targets: self,
looking_at (with a range), nearby (with a radius),
trigger (the other party of a triggered skill's event) and
party.
Triggered skills add a trigger block:
trigger: { on: melee_hit, chance: 25.0 }, where
on is one of melee_hit, hurt,
kill or fall.
Races and classes then grant the skill by id - deliberately the easy part:
skills:
mypack:fire_dash: { level: 10, cost: 5 }
Layer 2 - a small mod
When a skill needs behaviour no existing effect covers, register a new effect type.
SkillEffectTypes.register() in your mod constructor is the entire API surface.
@Mod("firepack")
public class FirePack {
public FirePack(IEventBus modEventBus, ModContainer container) {
SkillEffectTypes.register(FlameRing.TYPE, FlameRing.CODEC);
}
}
public record FlameRing(double radius, int flames) implements SkillEffect {
public static final Identifier TYPE =
Identifier.fromNamespaceAndPath("firepack", "flame_ring");
public static final MapCodec<FlameRing> CODEC = RecordCodecBuilder.mapCodec(i -> i.group(
Codec.DOUBLE.optionalFieldOf("radius", 3.0).forGetter(FlameRing::radius),
Codec.INT.optionalFieldOf("flames", 8).forGetter(FlameRing::flames))
.apply(i, FlameRing::new));
@Override public Identifier type() { return TYPE; }
@Override
public void apply(SkillContext ctx) {
// Runs on the server thread. ctx.caster(), ctx.level(),
// ctx.skillLevel(), ctx.triggerTarget() are available.
}
}
type: in a data file produces a log message listing every
known effect type - tell your users to read the server log.
mods.toml, declare a required
dependency on legendquest so load order and version ranges are
enforced. The old pack system's "load order is pure luck" problem is gone.
Use the same ModDevGradle scaffold as LegendQuest itself, and add:
dependencies {
// consume the published LegendQuest jar (or a maven repo / jarInJar later)
implementation files("libs/legendquest-<version>.jar")
}
Before you ship it
A skill-pack jar must be installed on the server AND on every modded client. Miss one client and it fails registry sync with "Unknown skill effect type". Vanilla clients are unaffected - they never see the registry at all, which is the one case that keeps working no matter what you ship.
This is the difference between a skill pack and the genre packs: a datapack is server-side data and needs nothing from anybody, while a skill pack registers new types that a modded client must also know about. If your pack is aimed at a public server with mixed clients, prefer Layer 1 wherever the idea allows it.
A worked example ships on every release as
examplepack-1.0.0.jar. Drop it in mods/ next to
LegendQuest to see custom effect types working, or read it as a template. It also carries a
Stormcaller class under examples/ in the jar - deliberately outside
data/, so it never auto-loads; copy it into a datapack if you want it.