Compare commits

...
1 Commits
Author SHA1 Message Date
dmg d8bbe78a08 feat(frost-shard): use fast arrow flight
Release / release (push) Successful in 9m56s
2026-09-21 16:23:46 -04:00
6 changed files with 54 additions and 19 deletions

No files matched your search

@@ -15,11 +15,13 @@ import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.damage.DamageSource;
import org.bukkit.damage.DamageType;
import org.bukkit.entity.AbstractArrow;
import org.bukkit.entity.Arrow;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Snowball;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.ProjectileHitEvent;
import org.bukkit.potion.PotionEffect;
@@ -45,12 +47,12 @@ public final class FrostShard implements AbilityEffect, Listener, AutoCloseable
}
public static Settings defaults() {
return new Settings(BigInteger.valueOf(4), Duration.ofSeconds(5), 32, Duration.ofSeconds(5),
1, 0, Duration.ofSeconds(2));
3, 0, Duration.ofSeconds(2));
}
}
private final class State implements AutoCloseable {
final Snowball projectile;
final Arrow projectile;
final AbilityExecution execution;
final Location origin;
final BigInteger intelligence;
@@ -58,7 +60,7 @@ public final class FrostShard implements AbilityEffect, Listener, AutoCloseable
LivingEntity target;
boolean accepted;
boolean finished;
State(Snowball projectile, AbilityExecution execution, Location origin, BigInteger intelligence, Settings settings) {
State(Arrow projectile, AbilityExecution execution, Location origin, BigInteger intelligence, Settings settings) {
this.projectile = projectile; this.execution = execution; this.origin = origin;
this.intelligence = intelligence; this.settings = settings;
}
@@ -144,7 +146,9 @@ public final class FrostShard implements AbilityEffect, Listener, AutoCloseable
Vector direction = origin.getDirection();
if (!Double.isFinite(direction.getX()) || !Double.isFinite(direction.getY()) || !Double.isFinite(direction.getZ())
|| direction.lengthSquared() == 0) throw new IllegalStateException("Frost Shard direction is unavailable");
var projectile = player.launchProjectile(Snowball.class, direction.normalize().multiply(current.speedPerTick()));
var projectile = player.launchProjectile(Arrow.class, direction.normalize().multiply(current.speedPerTick()));
projectile.setPickupStatus(AbstractArrow.PickupStatus.DISALLOWED);
projectile.setDamage(0); projectile.setKnockbackStrength(0); projectile.setPierceLevel(0); projectile.setCritical(false);
var state = new State(projectile, execution, origin, execution.character().stats().get(Stat.INT), current);
if (projectiles.putIfAbsent(projectile.getUniqueId(), state) != null) {
projectile.remove(); throw new IllegalStateException("Duplicate Frost Shard projectile identity");
@@ -160,6 +164,13 @@ public final class FrostShard implements AbilityEffect, Listener, AutoCloseable
player.getWorld().spawnParticle(Particle.SNOWFLAKE, origin, 8, 0.15, 0.15, 0.15, 0.01);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void containNativeDamage(EntityDamageByEntityEvent event) {
if (!(event.getDamager() instanceof Arrow arrow)) return;
var state = projectiles.get(arrow.getUniqueId());
if (state != null && damaging != state) event.setCancelled(true);
}
@EventHandler(priority = EventPriority.MONITOR)
public void hit(ProjectileHitEvent event) {
var state = projectiles.remove(event.getEntity().getUniqueId());
@@ -36,7 +36,7 @@ public final class FrostShardSettings implements AutoCloseable {
public static final String SLOWNESS = "frost-shard-slowness-duration-seconds";
private static final List<String> KEYS = List.of(BASE_DAMAGE, COOLDOWN, LIFETIME, SPEED, RANGE, AMPLIFIER, SLOWNESS);
private static final Map<String, String> DEFAULTS = Map.of(BASE_DAMAGE, "4", COOLDOWN, "5", RANGE, "32",
LIFETIME, "5", SPEED, "1", AMPLIFIER, "0", SLOWNESS, "2");
LIFETIME, "5", SPEED, "3", AMPLIFIER, "0", SLOWNESS, "2");
@FunctionalInterface interface Replacement { void move(Path from, Path to) throws IOException; }
public record Update(String key, String value, FrostShard.Settings settings,
+1 -1
View File
@@ -77,7 +77,7 @@ frost-shard-base-damage: 4
frost-shard-cooldown-seconds: 5
frost-shard-range-blocks: 32
frost-shard-lifetime-seconds: 5
frost-shard-projectile-speed-blocks-per-tick: 1
frost-shard-projectile-speed-blocks-per-tick: 3
frost-shard-slowness-amplifier: 0
frost-shard-slowness-duration-seconds: 2
@@ -25,6 +25,7 @@ class FrostShardNativeTest {
String book = PartyBookNativeTest.text(a).replaceAll("\\s+", "");
assertTrue(book.contains("FrostShard"), book); assertTrue(book.contains("INT12"), book);
assertTrue(book.contains("WIS10"), book); assertTrue(book.contains("SlownessI"), book);
assertTrue(book.contains("projectile-speed=3"), book);
var learn = abilityButton(a, "[Learn]", "frost_shard");
PartyBookNativeTest.click(f, a, learn); PartyBookNativeTest.click(f, a, learn);
PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
@@ -41,17 +42,23 @@ class FrostShardNativeTest {
eye.setDirection(new org.bukkit.util.Vector(1, 0, 0));
doReturn(eye).when(a.player).getEyeLocation();
var projectile = projectile(a, new org.bukkit.Location(a.world, 10, 65.6, 0));
var velocity = new java.util.concurrent.atomic.AtomicReference<org.bukkit.util.Vector>();
doAnswer(call -> {
velocity.set(call.getArgument(1));
f.dispatcher.accept(new org.bukkit.event.entity.ProjectileLaunchEvent(projectile));
return projectile;
}).when(a.player).launchProjectile(eq(org.bukkit.entity.Snowball.class), any(org.bukkit.util.Vector.class));
}).when(a.player).launchProjectile(eq(org.bukkit.entity.Arrow.class), any(org.bukkit.util.Vector.class));
a.core.inventory.getInventory().setSelectedSlot(2);
var cast = new org.bukkit.event.player.PlayerInteractEvent(a.player,
org.bukkit.event.block.Action.RIGHT_CLICK_AIR, a.core.inventory.getItem(2), null,
org.bukkit.block.BlockFace.SELF, org.bukkit.inventory.EquipmentSlot.HAND);
f.dispatcher.accept(cast); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
assertEquals(org.bukkit.event.Event.Result.DENY, cast.useItemInHand());
verify(a.player).launchProjectile(eq(org.bukkit.entity.Snowball.class), any(org.bukkit.util.Vector.class));
verify(a.player).launchProjectile(eq(org.bukkit.entity.Arrow.class), any(org.bukkit.util.Vector.class));
assertEquals(new org.bukkit.util.Vector(3, 0, 0), velocity.get());
verify(projectile).setPickupStatus(org.bukkit.entity.AbstractArrow.PickupStatus.DISALLOWED);
verify(projectile).setDamage(0); verify(projectile).setCritical(false);
verify(projectile).setPierceLevel(0); verify(projectile).setKnockbackStrength(0);
var frost = FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "frostShard");
assertEquals(1, ((java.util.Map<?, ?>) FantasyStickLifecycleNativeTest.Fixture.field(frost, "projectiles")).size());
@@ -60,6 +67,9 @@ class FrostShardNativeTest {
when(target.getType()).thenReturn(org.bukkit.entity.EntityType.ZOMBIE);
when(target.getLocation()).thenReturn(new org.bukkit.Location(a.world, 10, 65.6, 0));
when(target.addPotionEffect(any())).thenReturn(true);
var vanilla = new org.bukkit.event.entity.EntityDamageByEntityEvent(projectile, target,
org.bukkit.event.entity.EntityDamageEvent.DamageCause.PROJECTILE, mock(org.bukkit.damage.DamageSource.class), 9);
f.dispatcher.accept(vanilla); assertTrue(vanilla.isCancelled(), "Owned arrow native damage is contained");
var damages = new java.util.ArrayList<org.bukkit.event.entity.EntityDamageByEntityEvent>();
doAnswer(call -> {
var source = (org.bukkit.damage.DamageSource) call.getArgument(1);
@@ -184,13 +194,13 @@ class FrostShardNativeTest {
verify(projectile, atLeastOnce()).remove();
learnAndAssign(f, a); cast(f, a);
verify(a.player, times(1)).launchProjectile(eq(org.bukkit.entity.Snowball.class), any(org.bukkit.util.Vector.class));
verify(a.player, times(1)).launchProjectile(eq(org.bukkit.entity.Arrow.class), any(org.bukkit.util.Vector.class));
assertTrue(a.messages.stream().anyMatch(message -> message.contains("ready in")));
var sheet = a.core.inventory.getItemInOffHand().clone();
a.core.inventory.setItemInOffHand(null); f.changed(a); PartyBookNativeTest.drain(f); f.awaitCharacterAndReceipt(a);
var b = new FantasyStickLifecycleNativeTest.Actor(data); b.core.inventory.setItemInOffHand(sheet);
f.players.put(b.core.id, b.player); f.activate(b); cast(f, b);
verify(b.player, never()).launchProjectile(eq(org.bukkit.entity.Snowball.class), any(org.bukkit.util.Vector.class));
verify(b.player, never()).launchProjectile(eq(org.bukkit.entity.Arrow.class), any(org.bukkit.util.Vector.class));
assertTrue(b.messages.stream().anyMatch(message -> message.contains("ready in")));
assertEquals(deadline, storage.find(a.core.record.id()).get().orElseThrow().cooldowns().get("frost_shard"));
}
@@ -225,10 +235,22 @@ class FrostShardNativeTest {
engine.accept(new games.dmg.fantasystick.session.ControlInput(player, active, 2, "frost_shard",
games.dmg.fantasystick.session.ControlInput.Kind.ABILITY,
games.dmg.fantasystick.session.ControlInput.Gesture.USE, null, 0, 0, () -> true));
verify(player, never()).launchProjectile(eq(org.bukkit.entity.Snowball.class), any(org.bukkit.util.Vector.class));
verify(player, never()).launchProjectile(eq(org.bukkit.entity.Arrow.class), any(org.bukkit.util.Vector.class));
assertEquals(1, failures.size()); engine.close(); frost.close(); parties.close();
}
@Test void retainedProjectileSpeedIsHonoredWithoutRewritingConfiguration() throws Exception {
String retained = "frost-shard-projectile-speed-blocks-per-tick: 1\n";
java.nio.file.Files.writeString(data.resolve("config.yml"), retained);
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
f.plugin.setEnabled(true);
var definition = ((games.dmg.fantasystick.ability.AbilityRegistry)
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "abilityRegistry")).find("frost_shard").orElseThrow();
assertEquals(0, new java.math.BigDecimal("1").compareTo(definition.parameters().get("projectile-speed")));
assertEquals(retained, java.nio.file.Files.readString(data.resolve("config.yml")));
}
}
@Test void configuredDefinitionAndFormulaPreserveReviewedFiniteUncappedScaling() throws Exception {
java.nio.file.Files.writeString(data.resolve("config.yml"),
"frost-shard-base-damage: 9\nfrost-shard-cooldown-seconds: 2.5\n"
@@ -286,7 +308,7 @@ class FrostShardNativeTest {
doReturn(eye).when(a.player).getEyeLocation();
var projectile = projectile(a, new org.bukkit.Location(a.world, 2, 65.6, 0));
doAnswer(call -> { f.dispatcher.accept(new org.bukkit.event.entity.ProjectileLaunchEvent(projectile)); return projectile; })
.when(a.player).launchProjectile(eq(org.bukkit.entity.Snowball.class), any(org.bukkit.util.Vector.class));
.when(a.player).launchProjectile(eq(org.bukkit.entity.Arrow.class), any(org.bukkit.util.Vector.class));
var target = mock(org.bukkit.entity.LivingEntity.class);
when(target.getUniqueId()).thenReturn(java.util.UUID.randomUUID());
when(target.getLocation()).thenReturn(new org.bukkit.Location(a.world, 2, 65.6, 0));
@@ -458,14 +480,14 @@ class FrostShardNativeTest {
private static void prepareLaunches(FantasyStickLifecycleNativeTest.Fixture fixture,
FantasyStickLifecycleNativeTest.Actor actor,
org.bukkit.entity.Snowball... projectiles) {
org.bukkit.entity.Arrow... projectiles) {
var eye = new org.bukkit.Location(actor.world, 0, 65.6, 0);
eye.setDirection(new org.bukkit.util.Vector(1, 0, 0)); doReturn(eye).when(actor.player).getEyeLocation();
var queue = new java.util.ArrayDeque<>(java.util.List.of(projectiles));
doAnswer(call -> {
var projectile = queue.removeFirst(); fixture.dispatcher.accept(new org.bukkit.event.entity.ProjectileLaunchEvent(projectile));
return projectile;
}).when(actor.player).launchProjectile(eq(org.bukkit.entity.Snowball.class), any(org.bukkit.util.Vector.class));
}).when(actor.player).launchProjectile(eq(org.bukkit.entity.Arrow.class), any(org.bukkit.util.Vector.class));
}
private static net.minecraft.network.chat.ClickEvent.Custom abilityButton(
@@ -518,9 +540,9 @@ class FrostShardNativeTest {
old.learned(), old.loadout(), old.cooldowns(), old.restrictedKills(), false, null, null), old.revision());
}
private static org.bukkit.entity.Snowball projectile(FantasyStickLifecycleNativeTest.Actor actor,
org.bukkit.Location location) {
var projectile = mock(org.bukkit.entity.Snowball.class);
private static org.bukkit.entity.Arrow projectile(FantasyStickLifecycleNativeTest.Actor actor,
org.bukkit.Location location) {
var projectile = mock(org.bukkit.entity.Arrow.class);
when(projectile.getUniqueId()).thenReturn(java.util.UUID.randomUUID());
when(projectile.getShooter()).thenReturn(actor.player);
when(projectile.getLocation()).thenAnswer(ignored -> location.clone());
@@ -52,7 +52,7 @@ class DistributionTest {
assertTrue(defaults.contains("great-sword-recovery-seconds: 1.2"));
for (String setting : java.util.List.of("frost-shard-base-damage: 4",
"frost-shard-cooldown-seconds: 5", "frost-shard-range-blocks: 32",
"frost-shard-lifetime-seconds: 5", "frost-shard-projectile-speed-blocks-per-tick: 1",
"frost-shard-lifetime-seconds: 5", "frost-shard-projectile-speed-blocks-per-tick: 3",
"frost-shard-slowness-amplifier: 0", "frost-shard-slowness-duration-seconds: 2")) {
assertTrue(defaults.contains(setting), "Missing packaged Frost Shard default: " + setting);
}
@@ -36,7 +36,9 @@ class FrostShardSettingsTest {
}
@Test void rejectsMalformedAndOutOfRangeValuesBeforeQueueingOrChangingEffectiveState() {
assertEquals("3", FrostShardSettings.defaultValue(FrostShardSettings.SPEED));
try (var settings = new FrostShardSettings(data, defaults())) {
assertEquals(3, settings.settings().speedPerTick());
for (var entry : Map.of(
FrostShardSettings.BASE_DAMAGE, "-1",
FrostShardSettings.COOLDOWN, "31536001",