Skip to content

Buff redirection, pathfinding, and branching projectiles#1075

Open
DragonsAscent wants to merge 8 commits intoTheComputerGeek2:mainfrom
DragonsAscent:main
Open

Buff redirection, pathfinding, and branching projectiles#1075
DragonsAscent wants to merge 8 commits intoTheComputerGeek2:mainfrom
DragonsAscent:main

Conversation

@DragonsAscent
Copy link
Copy Markdown
Collaborator

Branching Projectile Spell:

  • Added BranchingProjectileSpell, a new instant spell that creates a main projectile which can randomly branch into additional projectiles. The spell supports configurable parameters such as maximum distance, duration, branching probability, angles, and spell effects on hit entities. This allows for complex, visually dynamic spell effects with hit detection and subspell casting. (core/src/main/java/com/nisovin/magicspells/spells/instant/BranchingProjectileSpell.java)

Proxy Spell:

  • Introduced ProxySpell, a new buff spell that allows redirection of spell effects and damage from one entity to another (the proxy). It manages active proxies, handles spell target and damage events, and ensures effects and costs are properly redirected. This enables advanced spell interactions such as decoys or bodyguard mechanics. (core/src/main/java/com/nisovin/magicspells/spells/buff/ProxySpell.java)

Score Modifier:

  • Added ScoreCondition, a new cast modifier condition that checks a player's score in a specified scoreboard objective (core/src/main/java/com/nisovin/magicspells/castmodifiers/conditions/ScoreCondition.java)

Copilot AI review requested due to automatic review settings April 11, 2026 05:16
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds several new gameplay mechanics to the MagicSpells core: a targeted pathfinding spell, a branching projectile instant spell, a proxy/redirect buff spell, and a scoreboard-based cast modifier condition.

Changes:

  • Added PathfindSpell with a 3D A* path search and per-node subspell/effect execution.
  • Added BranchingProjectileSpell to simulate a projectile that can spawn branching sub-projectiles and cast a subspell on nearby hit entities.
  • Added ProxySpell to redirect spell targeting/pre-impact and damage events from a protected entity to a proxy entity, plus ScoreCondition for scoreboard objective comparisons.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
core/src/main/java/com/nisovin/magicspells/spells/targeted/PathfindSpell.java New targeted spell implementing A* pathfinding and traversal filtering.
core/src/main/java/com/nisovin/magicspells/spells/instant/BranchingProjectileSpell.java New instant spell that advances a projectile over time and spawns branch tasks probabilistically.
core/src/main/java/com/nisovin/magicspells/spells/buff/ProxySpell.java New buff spell that redirects spell target/pre-impact and damage from one entity to another.
core/src/main/java/com/nisovin/magicspells/castmodifiers/conditions/ScoreCondition.java New cast condition that compares a target’s scoreboard score against a configured value/operator.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +122 to +126
// Branching
if (!isBranch && Math.random() < branchProbability.get(data)) {
Vector branchDir = getBranchDirection(direction, branchAngle.get(data));
new BranchTask(current.clone(), branchDir, maxDist, maxTicks, data, branchDepth + 1, true).runTaskTimer(MagicSpells.plugin, 0, 1);
}
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Branching currently has no cap: the trunk can spawn a new BranchTask every tick while the random check passes, and branchDepth is incremented but never used to limit recursion/branch count. This can create many scheduled tasks and cause server lag; consider adding a configurable max branch count/depth (or ensure only one branch can spawn per trunk) and either remove or use branchDepth.

Copilot uses AI. Check for mistakes.
Comment thread core/src/main/java/com/nisovin/magicspells/spells/targeted/PathfindSpell.java Outdated
Comment on lines +245 to +251
for (int dy = -1; dy <= 2; dy++) {
int y = by + dy;
if (y < world.getMinHeight() || y > world.getMaxHeight()) continue;

Block feetBlock = world.getBlockAt(bx, y, bz);
if (isWalkable(feetBlock, throughBlocks)) {
return new Location(world, feetBlock.getX() + 0.5, feetBlock.getY(), feetBlock.getZ() + 0.5);
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

World Y upper bound check is off by one: valid block Y coordinates are < world.getMaxHeight(). The current y > world.getMaxHeight() allows y == getMaxHeight() which can produce out-of-range block queries. Use y >= world.getMaxHeight() instead.

Copilot uses AI. Check for mistakes.
DragonsAscent and others added 3 commits April 11, 2026 14:57
…chingProjectileSpell.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…tions/ScoreCondition.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…hfindSpell.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown

Copy link
Copy Markdown
Collaborator

@JasperLorelai JasperLorelai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked into the A* implementation too deeply. The rest of the stuff is closer to ready after the requested changes are made. Still not sure too about BranchingProjectileSpell just because it doesn't support our existing trackers or anything, but then again, those are very messy already.

import com.nisovin.magicspells.util.MagicConfig;
import com.nisovin.magicspells.util.SpellData;

public class ProxySpell extends BuffSpell implements Listener {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spell already implements Listener.

Comment on lines +3 to +23
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import org.jetbrains.annotations.NotNull;

import org.bukkit.entity.LivingEntity;
import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.entity.EntityDamageByEntityEvent;

import com.nisovin.magicspells.events.SpellTargetEvent;
import com.nisovin.magicspells.events.SpellPreImpactEvent;
import com.nisovin.magicspells.events.MagicSpellsEntityDamageByEntityEvent;
import com.nisovin.magicspells.spells.BuffSpell;
import com.nisovin.magicspells.spelleffects.EffectPosition;
import com.nisovin.magicspells.util.MagicConfig;
import com.nisovin.magicspells.util.SpellData;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Styling: Group & sort imports.

Comment on lines +3 to +18
import java.util.*;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.LivingEntity;
import org.bukkit.util.Vector;

import com.nisovin.magicspells.util.*;
import com.nisovin.magicspells.Subspell;
import com.nisovin.magicspells.spells.TargetedSpell;
import com.nisovin.magicspells.util.config.ConfigData;
import com.nisovin.magicspells.spelleffects.EffectPosition;
import com.nisovin.magicspells.spells.TargetedLocationSpell;
import com.nisovin.magicspells.spells.TargetedEntitySpell;
import com.nisovin.magicspells.events.SpellTargetLocationEvent;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Styling: Group & sort imports.

Comment on lines +4 to +12
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.LivingEntity;
import org.bukkit.scoreboard.Objective;
import org.bukkit.scoreboard.Scoreboard;

import com.nisovin.magicspells.castmodifiers.Condition;
import com.nisovin.magicspells.util.SpellData;
import com.nisovin.magicspells.util.Name;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Styling: Group & sort imports.

Comment on lines +58 to +59
double maxDist = maxDistance.get(data);
int maxTicks = maxDuration.get(data);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to pass these to the constructor since they are visible within BranchTask.

if (throughBlocks) return true;

// Without an allow-list, hide paths over air or water; show over normal solid blocks.
org.bukkit.Material mat = traversedBlock.getType();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use import instead of qualified type.

}

private Location findNearbyWalkable(Location loc, boolean throughBlocks) {
org.bukkit.World world = loc.getWorld();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use import instead of qualified type.

return x == n.x && y == n.y && z == n.z && world.equals(n.world);
}
@Override public int hashCode() { return Objects.hash(x, y, z, world); }
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this class local to the method? All the collapsing doesn't benefit readability. You also could just store the location. Every instance of Node seems to use a Location without its pitch/yaw already.

int maxNodes = 10000; // hard limit to prevent infinite loops
while (!open.isEmpty() && expanded < maxNodes) {
Node curr = open.poll();
if (curr.x == goalNode.x && curr.y == goalNode.y && curr.z == goalNode.z && curr.world.equals(goalNode.world)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already implemented an equals method for Node.

* Example: score kills>3 required
*/
@Name("score")
public class ScoreCondition extends Condition {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to add the condition with ConditionManager.

Co-authored-by: JasperLorelai <contact@jasperlorelai.eu>

public ProxySpell(MagicConfig config, String spellName) {
super(config, spellName);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broke your build because of this:

Suggested change
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants