Skip to content
JasperLorelai edited this page Jan 24, 2025 · 15 revisions

Description:

On this page, we will document how to add custom spells, modifier conditions, passive listeners, etc. to MagicSpells.

Jump to a section:

Adding the dependency

MagicSpells uses JitPack as its repository service. You can find guides on how to add MagicSpells as a dependency for your build environment here or use the examples below.

Gradle:

repositories {
maven {url "https://jitpack.io"}
}
dependencies {
implementation("com.github.TheComputerGeek2.MagicSpells:core:main-SNAPSHOT") {transitive =false}
}

Maven:

<repository>
<id>jitpack-repo</id>
<url>https://jitpack.io</url>
</repository>
<dependency>
<groupId>com.github.TheComputerGeek2.MagicSpells</groupId>
<artifactId>core</artifactId>
<version>main-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>

Custom Spells:

  • Custom spell classes can be placed in the root folder of the plugin or in any folder in the root starting with "classes".
  • MagicSpells load custom spell classes if a spell uses the classified path in its spell-class property. If the spell is in the com.nisovin.magicspells.spells package, it may be left out (e.g. spell-class: ".MultiSpell").
  • All you have to do is to extend either the Spell, CommandSpell, InstantSpell, TargetedSpell, or BuffSpell classes.
  • TargetedSpell classes may implement the following interfaces: TargetedEntitySpell, TargetedEntityFromLocationSpell, or TargetedLocationSpell.
  • Since 4.0 Beta 14. Spells may be annotated with @DependsOn which contains a plugin or an array of them which the spell class will depend on being enabled before being loaded.
This example displays a basic instant spell.
packagecom.example.instant;
importorg.bukkit.entity.LivingEntity;
importcom.nisovin.magicspells.util.MagicConfig;
importcom.nisovin.magicspells.spells.InstantSpell;
importcom.nisovin.magicspells.spelleffects.EffectPosition;
publicclassHelloWorldSpellextendsInstantSpell {
publicHelloWorldSpell(MagicConfigconfig, StringspellName) {
super(config, spellName);
}
@OverridepublicPostCastActioncastSpell(LivingEntitycaster, SpellCastStatestate, floatpower, String[] args) {
if (state == SpellCastState.NORMAL) {
caster.sendMessage("Hello World!");
// We should always play the correct effects in the spell.playSpellEffects(EffectPosition.CASTER, caster);
}
returnPostCastAction.HANDLE_NORMALLY;
}
}
hello_world:
spell-class: "com.example.instant.HelloWorldSpell"
Here's how that code should look like since 4.0 Beta 13.
packagecom.example.instant;
importorg.bukkit.entity.Player;
importcom.nisovin.magicspells.util.SpellData;
importcom.nisovin.magicspells.util.CastResult;
importcom.nisovin.magicspells.util.MagicConfig;
importcom.nisovin.magicspells.spells.InstantSpell;
publicclassHelloWorldSpellextendsInstantSpell {
publicHelloWorldSpell(MagicConfigconfig, StringspellName) {
super(config, spellName);
}
@OverridepublicCastResultcast(SpellDatadata) {
// Fail if the caster isn't a player.// In our case the message is only relevant if it was sent to a player.if (!(data.caster() instanceofPlayercaster)) returnnewCastResult(PostCastAction.ALREADY_HANDLED, data);
caster.sendMessage("Hello World!");
playSpellEffects(data);
returnnewCastResult(PostCastAction.HANDLE_NORMALLY, data);
}
}
This example displays a basic targeted spell, and how to handle targets and spell effects in targeted spells.
packagecom.example.targeted;
importorg.bukkit.entity.LivingEntity;
importcom.nisovin.magicspells.util.TargetInfo;
importcom.nisovin.magicspells.util.MagicConfig;
importcom.nisovin.magicspells.spells.TargetedSpell;
importcom.nisovin.magicspells.spells.TargetedEntitySpell;
importcom.nisovin.magicspells.spelleffects.EffectPosition;
publicclassHelloWorldSpellextendsTargetedSpellimplementsTargetedEntitySpell {
publicHelloWorldSpell(MagicConfigconfig, StringspellName) {
super(config, spellName);
}
@OverridepublicPostCastActioncastSpell(LivingEntitycaster, SpellCastStatestate, floatpower, String[] args) {
if (state == SpellCastState.NORMAL) {
TargetInfo<LivingEntity> targetInfo = getTargetedEntity(caster, power);
if (targetInfo == null) returnnoTarget(caster);
LivingEntitytarget = targetInfo.getTarget();
hello(caster, target);
sendMessages(caster, target);
returnPostCastAction.NO_MESSAGES;
}
returnPostCastAction.HANDLE_NORMALLY;
}
@OverridepublicbooleancastAtEntity(LivingEntitycaster, LivingEntitytarget, floatpower) {
returnhello(caster, target);
}
@OverridepublicbooleancastAtEntity(LivingEntitytarget, floatpower) {
returnhello(null, target);
}
privatebooleanhello(LivingEntitycaster, LivingEntitytarget) {
target.sendMessage("Hello World!");
if (caster == null) playSpellEffects(EffectPosition.TARGET, target);
elseplaySpellEffects(caster, target);
returntrue;
}
}
hello_world_targeted:
spell-class: "com.example.targeted.HelloWorldSpell"
Here's how that code should look like since 4.0 Beta 13.
packagecom.example.targeted;
importorg.bukkit.entity.LivingEntity;
importorg.bukkit.entity.Player;
importcom.nisovin.magicspells.util.CastResult;
importcom.nisovin.magicspells.util.SpellData;
importcom.nisovin.magicspells.util.TargetInfo;
importcom.nisovin.magicspells.util.MagicConfig;
importcom.nisovin.magicspells.spells.TargetedSpell;
importcom.nisovin.magicspells.spells.TargetedEntitySpell;
publicclassHelloWorldSpellextendsTargetedSpellimplementsTargetedEntitySpell {
publicHelloWorldSpell(MagicConfigconfig, StringspellName) {
super(config, spellName);
}
@OverridepublicCastResultcast(SpellDatadata) {
TargetInfo<LivingEntity> info = getTargetedEntity(data);
if (info.noTarget()) returnnoTarget(info);
returnhelloWorld(info.spellData());
}
@OverridepublicCastResultcastAtEntity(SpellDatadata) {
returnhelloWorld(data);
}
privateCastResulthelloWorld(SpellDatadata) {
if (!(data.target() instanceofPlayertarget)) returnnoTarget(data);
target.sendMessage("Hello World!");
playSpellEffects(data);
returnnewCastResult(PostCastAction.HANDLE_NORMALLY, data);
}
}
This example includes configuration reading.
packagecom.example.instant;
importorg.bukkit.entity.Player;
importorg.bukkit.entity.LivingEntity;
importcom.nisovin.magicspells.util.Util;
importcom.nisovin.magicspells.util.MagicConfig;
importcom.nisovin.magicspells.spells.InstantSpell;
publicclassRollDiceSpellextendsInstantSpell {
privateintmin;
privateintmax;
privatefinalStringstrMessage;
publicRollDiceSpell(MagicConfigconfig, StringspellName) {
super(config, spellName);
min = getConfigInt("min", 0);
max = getConfigInt("max", 10);
strMessage = getConfigString("message", "Dice: ");
}
@OverridepublicPostCastActioncastSpell(LivingEntitylivingEntity, SpellCastStatestate, floatpower, String[] args) {
// Here we are dealing with a spell that only works for Player casters.if (state == SpellCastState.NORMAL && livingEntityinstanceofPlayercaster) {
// We're using Util methods not to create duplicate code.intrandom = min + Util.getRandomInt(max - min + 1);
caster.sendMessage(strMessage + random);
}
returnPostCastAction.HANDLE_NORMALLY;
}
}
roll_dice:
spell-class: "com.example.instant.RollDiceSpell"min: 0max: 100message: "You rolled: "
Here's how that code should look like since 4.0 Beta 13.
packagecom.example.instant;
importorg.bukkit.entity.Player;
importcom.nisovin.magicspells.util.CastResult;
importcom.nisovin.magicspells.util.SpellData;
importcom.nisovin.magicspells.util.MagicConfig;
importcom.nisovin.magicspells.spells.InstantSpell;
publicclassRollDiceSpellextendsInstantSpell {
privatefinalintmin;
privatefinalintmax;
privatefinalStringstrMessage;
publicRollDiceSpell(MagicConfigconfig, StringspellName) {
super(config, spellName);
min = getConfigInt("min", 0);
max = getConfigInt("max", 10);
strMessage = getConfigString("message", "Dice: ");
}
@OverridepublicCastResultcast(SpellDatadata) {
if (!(data.caster() instanceofPlayercaster)) returnnewCastResult(PostCastAction.ALREADY_HANDLED, data);
playSpellEffects(data);
caster.sendMessage(strMessage + random.nextInt(min, max));
returnnewCastResult(PostCastAction.HANDLE_NORMALLY, data);
}
}

Expressions:

Warning

Since 4.0 Beta 13.

If you want an option to support variable replacement and other things expressions do, instead of grabbing the value using methods like:

// In classes extending "Spell"Stringkey = getConfigString("key", "");
// Other config-reading cases:ConfigurationSectionconfig = /* */;
Stringkey = config.getString("key", "");

You can use the Spell#getConfigDataX-variant methods or the ConfigDataUtil util class to get a ConfigData<T> wrapping object:

// In classes extending "Spell"ConfigData<String> key = getConfigDataString("key", "");
// Other config-reading cases:ConfigurationSectionconfig = /* */;
ConfigData<String> key = ConfigDataUtil.getString(config, "key", "");

With it, you can resolve its value during runtime:

@OverridepublicCastResultcast(SpellDatadata) {
playSpellEffects(data);
Stringkey = this.key.get(data);
. . .
returnnewCastResult(PostCastAction.HANDLE_NORMALLY, data);
}

Spell Addons:

  • This section includes the creation of custom modifier conditions, passive listeners, variables, and spell effects.
  • If you would like to add these features, you can use the API from your custom plugin. However, if you don't want to add a separate plugin, you could utilise a custom spell class to achieve this.
  • If you are using a custom plugin to load these modules, you could use this resource to load these classes more simply. It could also serve as an example of what follows. The plugin must "soft depend" on "MagicSpells".
  • You have to create an event handler for the module you want to add, then add it through its manager there. The events are: ConditionsLoadingEvent, PassiveListenersLoadingEvent, VariablesLoadingEvent, and SpellEffectsLoadingEvent. You can fetch the specific manager from static methods in the MagicSpells class or from the event getters. Each of these managers includes a method to add the module you want.
@EventHandlerpublicvoidonConditionLoad(ConditionsLoadingEventevent) {
MagicSpells.getConditionManager().addCondition("always", AlwaysCondition.class);
}
  • NOTE: Since 4.0 Beta 14:
    • You can annotate your class with @DependsOn, passing a plugin or an array of plugins required to be enabled before this addon is loaded.
    • You can alternatively annotate your custom class with @Name and add calling the add method without the name parameter.
@Name("always")
publicclassAlwaysConditionextendsCondition { . . . }
. . .
@EventHandlerpublicvoidonConditionLoad(ConditionsLoadingEventevent) {
MagicSpells.getConditionManager().addCondition(AlwaysCondition.class);
}

Custom No Magic Zone types:

A class extending NoMagicZone can be added to the no magic zone type list like this:

@EventHandlerpublicvoidonMSLoading(MagicSpellsLoadingEventevent) {
MagicSpells.getNoMagicZoneManager().addZoneType("cuboid", NoMagicZoneCuboid.class);
}

Since 4.0 Beta 14 the class may be annotated with @DependsOn listing required plugins that need to be loaded before the zone is and with the @Name annotation holding its name instead of it being passed by the add method:

@Name("cuboid")
publicclassNoMagicZoneCuboidextendsNoMagicZone { . . . }
. . .
@EventHandlerpublicvoidonMSLoading(MagicSpellsLoadingEventevent) {
MagicSpells.getNoMagicZoneManager().addZoneType(NoMagicZoneCuboid.class);
}

Custom Cleansers:

Warning

Since 4.0 Beta 14.

Registering custom cleansers for the Cleanse Spell to list under its remove option is possible. You can find some examples of how to implement a cleanser here.

Before 4.0 Beta 17:
importcom.nisovin.magicspells.spells.targeted.cleanse.util.Cleansers;
. . .
Cleansers.addCleanserClass(/* <Class which extends Cleanser>*/);
Since 4.0 Beta 17:
@EventHandlerpublicvoidonMSLoading(MagicSpellsLoadingEventevent) {
MagicSpells.getCleanserManager().addCleanser(/* <Class which extends Cleanser>*/);
}

Custom Mob Goals:

Warning

Since 4.0 Beta 14.

You can find some examples of how to implement a mob goal here. The difference between PaperMC goals is that you extend CustomGoal instead, implement its CustomGoal#initialize(ConfigurationSection) method, and annotate the goal with @Name holding its goal key.

Before 4.0 Beta 17:
importcom.nisovin.magicspells.util.ai.CustomGoals;
. . .
CustomGoals.addGoal(/* <Class which extends CustomGoal>*/);
Since 4.0 Beta 17:
@EventHandlerpublicvoidonMSLoading(MagicSpellsLoadingEventevent) {
MagicSpells.getCustomGoalsManager().addGoal(/* <Class which extends CustomGoal>*/);
}

Clone this wiki locally