-
Notifications
You must be signed in to change notification settings - Fork 71
Buff redirection, pathfinding, and branching projectiles #1075
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c890595
40f7021
90e1960
524ef0d
6e8b372
3a2e073
31a92a8
9f8ad17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| package com.nisovin.magicspells.castmodifiers.conditions; | ||
|
|
||
|
|
||
| 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.Name; | ||
|
|
||
| /** | ||
| * Usage: score <objective><operator><value> required/denied | ||
| * Example: score kills>3 required | ||
| */ | ||
| @Name("score") | ||
| public class ScoreCondition extends Condition { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to add the condition with |
||
|
|
||
| private String objectiveName; | ||
| private String operator; | ||
| private int value; | ||
|
|
||
| @Override | ||
| public boolean initialize(String var) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the IDE not highlighting this as |
||
| if (var == null) return false; | ||
| var = var.trim(); | ||
| String[] ops = {">=","<=","!=",">","<","="}; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of implementing operators like this, the condition should extend |
||
| for (String op : ops) { | ||
| int idx = var.indexOf(op); | ||
| if (idx > 0) { | ||
| objectiveName = var.substring(0, idx).trim(); | ||
| operator = op; | ||
| try { | ||
| value = Integer.parseInt(var.substring(idx + op.length()).trim()); | ||
| } catch (NumberFormatException e) { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean check(LivingEntity entity) { | ||
| return checkScore(entity); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean check(LivingEntity caster, LivingEntity target) { | ||
| return checkScore(target); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean check(LivingEntity caster, Location location) { | ||
| // Not applicable for locations, always false | ||
| return false; | ||
| } | ||
|
|
||
| private boolean checkScore(LivingEntity entity) { | ||
| Scoreboard scoreboard = Bukkit.getScoreboardManager().getMainScoreboard(); | ||
| Objective obj = scoreboard.getObjective(objectiveName); | ||
| if (obj == null) return false; | ||
| int score = obj.getScoreFor(entity).getScore(); | ||
| return compare(score, operator, value); | ||
| } | ||
|
|
||
| private boolean compare(int score, String op, int val) { | ||
| switch (op) { | ||
| case ">": return score > val; | ||
| case "<": return score < val; | ||
| case ">=": return score >= val; | ||
| case "<=": return score <= val; | ||
| case "=": return score == val; | ||
| case "!=": return score != val; | ||
| default: return false; | ||
| } | ||
| } | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace indents with tabs. |
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,132 @@ | ||||
| package com.nisovin.magicspells.spells.buff; | ||||
|
|
||||
| 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; | ||||
|
Comment on lines
+3
to
+23
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Styling: Group & sort imports. |
||||
|
|
||||
| public class ProxySpell extends BuffSpell implements Listener { | ||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
|
|
||||
| private final Set<UUID> redirecting = new HashSet<>(); | ||||
| private final Map<UUID, SpellData> proxies = new HashMap<>(); | ||||
|
|
||||
| public ProxySpell(MagicConfig config, String spellName) { | ||||
| super(config, spellName); | ||||
| } | ||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Broke your build because of this:
Suggested change
|
||||
| } | ||||
|
|
||||
| @Override | ||||
| public boolean castBuff(SpellData data) { | ||||
| proxies.put(data.target().getUniqueId(), data); | ||||
| return true; | ||||
| } | ||||
|
Comment on lines
+35
to
+39
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This spell should fail if the caster is equal to the target, it seems? |
||||
|
|
||||
| @Override | ||||
| public boolean recastBuff(SpellData data) { | ||||
| stopEffects(data.target()); | ||||
| return castBuff(data); | ||||
| } | ||||
|
|
||||
| @Override | ||||
| public boolean isActive(LivingEntity entity) { | ||||
| return proxies.containsKey(entity.getUniqueId()); | ||||
| } | ||||
|
|
||||
| @Override | ||||
| public void turnOffBuff(LivingEntity entity) { | ||||
| proxies.remove(entity.getUniqueId()); | ||||
| } | ||||
|
|
||||
| @Override | ||||
| protected @NotNull Collection<UUID> getActiveEntities() { | ||||
| return proxies.keySet(); | ||||
| } | ||||
|
|
||||
| @EventHandler(ignoreCancelled = true) | ||||
| public void onSpellTarget(SpellTargetEvent event) { | ||||
| LivingEntity target = event.getTarget(); | ||||
| if (target == null || !target.isValid()) return; | ||||
|
|
||||
| LivingEntity proxyTarget = getProxyTarget(target); | ||||
| if (proxyTarget == null) return; | ||||
|
|
||||
| event.setTarget(proxyTarget); | ||||
| playRedirectEffects(target, proxyTarget, event.getSpellData()); | ||||
|
|
||||
| addUseAndChargeCost(target); | ||||
| } | ||||
|
|
||||
| @EventHandler(ignoreCancelled = true) | ||||
| public void onSpellPreImpact(SpellPreImpactEvent event) { | ||||
| LivingEntity target = event.getTarget(); | ||||
| if (target == null || !target.isValid()) return; | ||||
|
|
||||
| LivingEntity proxyTarget = getProxyTarget(target); | ||||
| if (proxyTarget == null) return; | ||||
|
|
||||
| SpellData subData = new SpellData(event.getCaster(), proxyTarget, event.getPower()); | ||||
| playRedirectEffects(target, proxyTarget, subData); | ||||
|
|
||||
| event.setRedirected(true); | ||||
| addUseAndChargeCost(target); | ||||
| } | ||||
|
|
||||
| @EventHandler(ignoreCancelled = true) | ||||
| public void onEntityDamage(EntityDamageByEntityEvent event) { | ||||
| if (!(event.getEntity() instanceof LivingEntity target)) return; | ||||
|
|
||||
| LivingEntity proxyTarget = getProxyTarget(target); | ||||
| if (proxyTarget == null) return; | ||||
| if (!redirecting.add(proxyTarget.getUniqueId())) return; | ||||
|
|
||||
| SpellData subData = new SpellData(event.getDamager() instanceof LivingEntity damager ? damager : null, proxyTarget); | ||||
| playRedirectEffects(target, proxyTarget, subData); | ||||
|
|
||||
| event.setCancelled(true); | ||||
| try { | ||||
| proxyTarget.damage(event.getDamage(), event.getDamager()); | ||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should pass the |
||||
| addUseAndChargeCost(target); | ||||
| } finally { | ||||
| redirecting.remove(proxyTarget.getUniqueId()); | ||||
| } | ||||
| } | ||||
|
|
||||
| @EventHandler(ignoreCancelled = true) | ||||
| public void onLegacyDamage(MagicSpellsEntityDamageByEntityEvent event) { | ||||
| onEntityDamage(event); | ||||
| } | ||||
|
|
||||
| private LivingEntity getProxyTarget(LivingEntity target) { | ||||
| SpellData proxyData = proxies.get(target.getUniqueId()); | ||||
| if (proxyData == null) return null; | ||||
|
|
||||
| LivingEntity proxyTarget = proxyData.caster(); | ||||
| if (proxyTarget != null && proxyTarget.isValid()) return proxyTarget; | ||||
|
Comment on lines
+117
to
+121
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't be storing If an entity's chunk becomes unloaded, its |
||||
|
|
||||
| turnOff(target); | ||||
| return null; | ||||
| } | ||||
|
|
||||
| private void playRedirectEffects(LivingEntity target, LivingEntity proxyTarget, SpellData data) { | ||||
| playSpellEffects(EffectPosition.TARGET, target, data); | ||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Imo, this should be |
||||
| playSpellEffects(EffectPosition.END_POSITION, proxyTarget, data); | ||||
| } | ||||
|
|
||||
| } | ||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace indents with tabs. |
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| package com.nisovin.magicspells.spells.instant; | ||
|
|
||
| import java.util.*; | ||
|
|
||
| import org.bukkit.Location; | ||
| import org.bukkit.util.Vector; | ||
| import org.bukkit.entity.LivingEntity; | ||
| import org.bukkit.scheduler.BukkitRunnable; | ||
|
|
||
| import com.nisovin.magicspells.Subspell; | ||
| import com.nisovin.magicspells.MagicSpells; | ||
| import com.nisovin.magicspells.util.SpellData; | ||
| import com.nisovin.magicspells.util.CastResult; | ||
| import com.nisovin.magicspells.util.MagicConfig; | ||
| import com.nisovin.magicspells.spells.InstantSpell; | ||
| import com.nisovin.magicspells.util.config.ConfigData; | ||
| import com.nisovin.magicspells.spelleffects.EffectPosition; | ||
|
|
||
| public class BranchingProjectileSpell extends InstantSpell { | ||
|
|
||
| private final ConfigData<Double> maxDistance; | ||
| private final ConfigData<Double> hitRadius; | ||
| private final ConfigData<Double> stepLength; | ||
| private final ConfigData<Double> branchAngle; | ||
| private final ConfigData<Double> branchProbability; | ||
|
|
||
| private final ConfigData<Integer> maxDuration; | ||
| private final ConfigData<Integer> minBranchLength; | ||
| private final ConfigData<Integer> maxBranchLength; | ||
|
|
||
| private final ConfigData<String> spellToCastName; | ||
| private Subspell spellToCast; | ||
|
|
||
| public BranchingProjectileSpell(MagicConfig config, String spellName) { | ||
| super(config, spellName); | ||
|
|
||
| maxDuration = getConfigDataInt("max-duration", 40); | ||
|
|
||
| minBranchLength = getConfigDataInt("min-branch-length", 3); | ||
| maxBranchLength = getConfigDataInt("max-branch-length", 8); | ||
|
|
||
| hitRadius = getConfigDataDouble("hit-radius", 1.5); | ||
| stepLength = getConfigDataDouble("step-length", 0.8); | ||
| maxDistance = getConfigDataDouble("max-distance", 20.0); | ||
| branchAngle = getConfigDataDouble("branch-angle", 30.0); | ||
| branchProbability = getConfigDataDouble("branch-probability", 0.3); | ||
|
|
||
| spellToCastName = getConfigDataString("spell", ""); | ||
| } | ||
|
|
||
| @Override | ||
| public void initialize() { | ||
| super.initialize(); | ||
| spellToCast = initSubspell(spellToCastName.get(null), "BranchingProjectileSpell '" + internalName + "' has an invalid spell: '" + spellToCastName.get(null) + "' defined!"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's invalid to call |
||
| } | ||
|
|
||
| @Override | ||
| public CastResult cast(SpellData data) { | ||
| Location start = data.caster().getEyeLocation(); | ||
| Vector direction = start.getDirection().normalize(); | ||
|
DragonsAscent marked this conversation as resolved.
|
||
|
|
||
| double maxDist = maxDistance.get(data); | ||
| int maxTicks = maxDuration.get(data); | ||
|
Comment on lines
+62
to
+63
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| new BranchTask(start, direction, maxDist, maxTicks, data, 0, false).runTaskTimer(MagicSpells.plugin, 0, 1); | ||
| return new CastResult(PostCastAction.HANDLE_NORMALLY, data); | ||
|
JasperLorelai marked this conversation as resolved.
|
||
| } | ||
|
|
||
| private class BranchTask extends BukkitRunnable { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should extend |
||
|
|
||
| private final Location current; | ||
|
DragonsAscent marked this conversation as resolved.
|
||
| private final Vector direction; | ||
| private final double maxDist; | ||
| private final int maxTicks; | ||
| private final SpellData data; | ||
| private final int branchDepth; | ||
| private final boolean isBranch; | ||
| private double traveled = 0; | ||
| private int ticks = 0; | ||
| private int branchLength = 0; | ||
| private final int branchMaxLength; | ||
|
|
||
| BranchTask(Location start, Vector direction, double maxDist, int maxTicks, SpellData data, int branchDepth, boolean isBranch) { | ||
| this.current = start.clone(); | ||
| this.direction = direction.clone(); | ||
| this.maxDist = maxDist; | ||
| this.maxTicks = maxTicks; | ||
| this.data = data; | ||
| this.branchDepth = branchDepth; | ||
| this.isBranch = isBranch; | ||
| this.branchMaxLength = isBranch ? random.nextInt(minBranchLength.get(data), maxBranchLength.get(data)) : Integer.MAX_VALUE; | ||
| } | ||
|
|
||
| @Override | ||
| public void run() { | ||
| boolean finished = (traveled >= maxDist || ticks >= maxTicks || branchLength >= branchMaxLength); | ||
| if (finished) { | ||
| // Play DELAYED effect at the end of the branch/trunk | ||
| playSpellEffects(EffectPosition.DELAYED, current, data.location(current)); | ||
| cancel(); | ||
| return; | ||
| } | ||
| // Add a small random curve to the direction for organic movement (branches only) | ||
| if (isBranch) { | ||
| double curveStrength = 0.15; // tweak for more/less curve | ||
| Vector curve = new Vector( | ||
| (Math.random() - 0.5) * curveStrength, | ||
| (Math.random() - 0.5) * curveStrength, | ||
| (Math.random() - 0.5) * curveStrength | ||
| ); | ||
| direction.add(curve); | ||
| direction.normalize(); | ||
| } | ||
| // Move forward | ||
| current.add(direction.clone().multiply(stepLength.get(data))); | ||
| traveled += stepLength.get(data); | ||
| branchLength++; | ||
| ticks++; | ||
| // Play particle effect using appropriate EffectPosition | ||
| EffectPosition pos = isBranch ? EffectPosition.SPECIAL : EffectPosition.PROJECTILE; | ||
| playSpellEffects(pos, current, data.location(current)); | ||
| // Hit detection | ||
| for (LivingEntity entity : current.getNearbyLivingEntities(hitRadius.get(data))) { | ||
| if (entity.equals(data.caster())) continue; | ||
| if (spellToCast != null) spellToCast.subcast(data.retarget(entity, current)); | ||
| } | ||
| // 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); | ||
| } | ||
|
Comment on lines
+126
to
+130
|
||
| } | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These comments are a bit redundant. Ideally they should be suplimented/replaced by newlines. |
||
|
|
||
| private Vector getBranchDirection(Vector base, double angleDeg) { | ||
| double angleRad = Math.toRadians(angleDeg); | ||
| double yaw = Math.atan2(base.getZ(), base.getX()); | ||
| double pitch = Math.asin(base.getY()); | ||
| double branchYaw = yaw + (Math.random() - 0.5) * angleRad; | ||
| double branchPitch = pitch + (Math.random() - 0.5) * (angleRad / 2); | ||
| double x = Math.cos(branchYaw) * Math.cos(branchPitch); | ||
| double y = Math.sin(branchPitch); | ||
| double z = Math.sin(branchYaw) * Math.cos(branchPitch); | ||
| return new Vector(x, y, z).normalize(); | ||
| } | ||
|
|
||
|
|
||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace indents with tabs. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Styling: Group & sort imports.