Skip to content
JasperLorelai edited this page May 19, 2024 · 14 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.
package com.example.instant;

import org.bukkit.entity.LivingEntity;

import com.nisovin.magicspells.util.MagicConfig;
import com.nisovin.magicspells.spells.InstantSpell;
import com.nisovin.magicspells.spelleffects.EffectPosition;

public class HelloWorldSpell extends InstantSpell {

	public HelloWorldSpell(MagicConfig config, String spellName) {
		super(config, spellName);
	}

	@Override
	public PostCastAction castSpell(LivingEntity caster, SpellCastState state, float power, String[] args) {
		if (state == SpellCastState.NORMAL) {
			caster.sendMessage("Hello World!");
                        // We should always play the correct effects in the spell.
			playSpellEffects(EffectPosition.CASTER, caster);
		}
		return PostCastAction.HANDLE_NORMALLY;
	}

}
hello_world:
    spell-class: "com.example.instant.HelloWorldSpell"
Here's how that code should look like since 4.0 Beta 13.
package com.example.instant;

import org.bukkit.entity.Player;

import com.nisovin.magicspells.util.SpellData;
import com.nisovin.magicspells.util.CastResult;
import com.nisovin.magicspells.util.MagicConfig;
import com.nisovin.magicspells.spells.InstantSpell;

public class HelloWorldSpell extends InstantSpell {

	public HelloWorldSpell(MagicConfig config, String spellName) {
		super(config, spellName);
	}

	@Override
	public CastResult cast(SpellData data) {
		// 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() instanceof Player caster)) return new CastResult(PostCastAction.ALREADY_HANDLED, data);
		
		caster.sendMessage("Hello World!");
		playSpellEffects(data);
		return new CastResult(PostCastAction.HANDLE_NORMALLY, data);
	}
	
}
This example displays a basic targeted spell, and how to handle targets and spell effects in targeted spells.
package com.example.targeted;

import org.bukkit.entity.LivingEntity;

import com.nisovin.magicspells.util.TargetInfo;
import com.nisovin.magicspells.util.MagicConfig;
import com.nisovin.magicspells.spells.TargetedSpell;
import com.nisovin.magicspells.spells.TargetedEntitySpell;
import com.nisovin.magicspells.spelleffects.EffectPosition;

public class HelloWorldSpell extends TargetedSpell implements TargetedEntitySpell {

	public HelloWorldSpell(MagicConfig config, String spellName) {
		super(config, spellName);
	}

	@Override
	public PostCastAction castSpell(LivingEntity caster, SpellCastState state, float power, String[] args) {
		if (state == SpellCastState.NORMAL) {
			TargetInfo<LivingEntity> targetInfo = getTargetedEntity(caster, power);
			if (targetInfo == null) return noTarget(caster);
			LivingEntity target = targetInfo.getTarget();
			hello(caster, target);
			sendMessages(caster, target);
			return PostCastAction.NO_MESSAGES;
		}
		return PostCastAction.HANDLE_NORMALLY;
	}

	@Override
	public boolean castAtEntity(LivingEntity caster, LivingEntity target, float power) {
		return hello(caster, target);
	}

	@Override
	public boolean castAtEntity(LivingEntity target, float power) {
		return hello(null, target);
	}

	private boolean hello(LivingEntity caster, LivingEntity target) {
		target.sendMessage("Hello World!");
		if (caster == null) playSpellEffects(EffectPosition.TARGET, target);
		else playSpellEffects(caster, target);
		return true;
	}

}
hello_world_targeted:
    spell-class: "com.example.targeted.HelloWorldSpell"
Here's how that code should look like since 4.0 Beta 13.
package com.example.targeted;

import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;

import com.nisovin.magicspells.util.CastResult;
import com.nisovin.magicspells.util.SpellData;
import com.nisovin.magicspells.util.TargetInfo;
import com.nisovin.magicspells.util.MagicConfig;
import com.nisovin.magicspells.spells.TargetedSpell;
import com.nisovin.magicspells.spells.TargetedEntitySpell;

public class HelloWorldSpell extends TargetedSpell implements TargetedEntitySpell {

	public HelloWorldSpell(MagicConfig config, String spellName) {
		super(config, spellName);
	}

	@Override
	public CastResult cast(SpellData data) {
		TargetInfo<LivingEntity> info = getTargetedEntity(data);
		if (info.noTarget()) return noTarget(info);
		return helloWorld(info.spellData());
	}

	@Override
	public CastResult castAtEntity(SpellData data) {
		return helloWorld(data);
	}

	private CastResult helloWorld(SpellData data) {
		if (!(data.target() instanceof Player target)) return noTarget(data);
		target.sendMessage("Hello World!");
		
		playSpellEffects(data);
		return new CastResult(PostCastAction.HANDLE_NORMALLY, data);
	}

}
This example includes configuration reading.
package com.example.instant;

import org.bukkit.entity.Player;
import org.bukkit.entity.LivingEntity;

import com.nisovin.magicspells.util.Util;
import com.nisovin.magicspells.util.MagicConfig;
import com.nisovin.magicspells.spells.InstantSpell;

public class RollDiceSpell extends InstantSpell {

	private int min;
	private int max;
	private final String strMessage;

	public RollDiceSpell(MagicConfig config, String spellName) {
		super(config, spellName);
		min = getConfigInt("min", 0);
		max = getConfigInt("max", 10);
		strMessage = getConfigString("message", "Dice: ");
	}

	@Override
	public PostCastAction castSpell(LivingEntity livingEntity, SpellCastState state, float power, String[] args) {
		// Here we are dealing with a spell that only works for Player casters.
		if (state == SpellCastState.NORMAL && livingEntity instanceof Player caster) {
			// We're using Util methods not to create duplicate code.
			int random = min + Util.getRandomInt(max - min + 1);
			caster.sendMessage(strMessage + random);
		}
		return PostCastAction.HANDLE_NORMALLY;
	}

}
roll_dice:
    spell-class: "com.example.instant.RollDiceSpell"
    min: 0
    max: 100
    message: "You rolled: "
Here's how that code should look like since 4.0 Beta 13.
package com.example.instant;

import org.bukkit.entity.Player;

import com.nisovin.magicspells.util.CastResult;
import com.nisovin.magicspells.util.SpellData;
import com.nisovin.magicspells.util.MagicConfig;
import com.nisovin.magicspells.spells.InstantSpell;

public class RollDiceSpell extends InstantSpell {

	private final int min;
	private final int max;
	private final String strMessage;

	public RollDiceSpell(MagicConfig config, String spellName) {
		super(config, spellName);
		
		min = getConfigInt("min", 0);
		max = getConfigInt("max", 10);
		strMessage = getConfigString("message", "Dice: ");
	}

	@Override
	public CastResult cast(SpellData data) {
		if (!(data.caster() instanceof Player caster)) return new CastResult(PostCastAction.ALREADY_HANDLED, data);
		
		playSpellEffects(data);
		caster.sendMessage(strMessage + random.nextInt(min, max));
		
		return new CastResult(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"
String key = getConfigString("key", "");
// Other config-reading cases:
ConfigurationSection config = /* */;
String key = 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:
ConfigurationSection config = /* */;
ConfigData<String> key = ConfigDataUtil.getString(config, "key", "");

With it, you can resolve its value during runtime:

@Override
public CastResult cast(SpellData data) {
	playSpellEffects(data);
	String key = this.key.get(data);
  . . .
	return new CastResult(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.
@EventHandler
public void onConditionLoad(ConditionsLoadingEvent event) {
  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")
public class AlwaysCondition extends Condition { . . . }
. . .
@EventHandler
public void onConditionLoad(ConditionsLoadingEvent event) {
	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:

@EventHandler
public void onMSLoading(MagicSpellsLoadingEvent event) {
	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")
public class NoMagicZoneCuboid extends NoMagicZone { . . . }
. . .
@EventHandler
public void onMSLoading(MagicSpellsLoadingEvent event) {
	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.

import com.nisovin.magicspells.spells.targeted.cleanse.util.Cleansers;
. . .
Cleansers.addCleanserClass(/* <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. Goals are

import com.nisovin.magicspells.util.ai.CustomGoals;
. . .
CustomGoals.addGoal(/* <Class which extends CustomGoal>*/);
Clone this wiki locally