305 lines
21 KiB
Java
305 lines
21 KiB
Java
package games.dmg.fantasystick;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.*;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
import games.dmg.fantasystick.persistence.CharacterRecord;
|
|
import games.dmg.fantasystick.persistence.CharacterStorage;
|
|
import games.dmg.fantasystick.progression.Stat;
|
|
import java.math.BigInteger;
|
|
import java.nio.file.Path;
|
|
import java.util.EnumMap;
|
|
import java.util.List;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import net.minecraft.network.chat.ClickEvent;
|
|
import org.bukkit.Location;
|
|
import org.bukkit.event.player.PlayerInteractEvent;
|
|
import org.bukkit.event.player.PlayerMoveEvent;
|
|
import org.bukkit.inventory.EquipmentSlot;
|
|
import org.bukkit.util.Vector;
|
|
import org.junit.jupiter.api.BeforeAll;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.io.TempDir;
|
|
|
|
class QuickstepNativeTest {
|
|
@TempDir Path data;
|
|
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
|
|
|
|
@Test void bookAcquisitionEquippedUseAppliesAndStopsOwnedHorizontalVelocityAtFourBlocks() throws Exception {
|
|
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
|
f.plugin.setEnabled(true);
|
|
var a = new FantasyStickLifecycleNativeTest.Actor(data); withStats(data, a, 12, false);
|
|
f.players.put(a.core.id, a.player); f.activate(a);
|
|
a.core.inventory.getInventory().setSelectedSlot(0); PartyBookNativeTest.open(f, a);
|
|
String text = PartyBookNativeTest.text(a).replaceAll("\\s+", "");
|
|
assertTrue(text.contains("Quickstep")); assertTrue(text.contains("DEX12"));
|
|
assertFalse(text.contains("Cost:")); assertTrue(text.contains("4")); assertTrue(text.contains("5s"));
|
|
PartyBookNativeTest.click(f, a, abilityButton(a, "[Learn]", "quickstep"));
|
|
PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
|
var storage = (CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
|
assertEquals(BigInteger.valueOf(39), storage.find(a.core.record.id()).get().orElseThrow().unspentPoints());
|
|
|
|
var velocity = prepareMovement(a, new Vector(0, 0.25, 0));
|
|
use(f, a); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
|
assertEquals(new Vector(1, 0.25, 0), velocity.get());
|
|
verify(a.player, never()).setFallDistance(anyFloat());
|
|
assertNotNull(storage.find(a.core.record.id()).get().orElseThrow().cooldowns().get("quickstep"));
|
|
var from = new Location(a.world, 0, 64, 0); var to = new Location(a.world, 4, 64, 0);
|
|
f.dispatcher.accept(new PlayerMoveEvent(a.player, from, to));
|
|
assertEquals(new Vector(0, 0.25, 0), velocity.get(), "Only remaining owned horizontal motion is removed");
|
|
}
|
|
}
|
|
|
|
@Test void airborneFlyingGlidingOrMountedUseRefusesBeforeCooldownAndMovement() throws Exception {
|
|
for (int route = 0; route < 4; route++) try (var f = new FantasyStickLifecycleNativeTest.Fixture(data.resolve(Integer.toString(route)))) {
|
|
f.plugin.setEnabled(true);
|
|
var a = new FantasyStickLifecycleNativeTest.Actor(data.resolve(Integer.toString(route)));
|
|
withStats(data.resolve(Integer.toString(route)), a, 12, true);
|
|
f.players.put(a.core.id, a.player); f.activate(a);
|
|
var velocity = prepareMovement(a, new Vector(0.3, 0.4, 0.5));
|
|
doReturn(route != 0).when(a.player).isOnGround();
|
|
doReturn(route == 1).when(a.player).isFlying();
|
|
doReturn(route == 2).when(a.player).isGliding();
|
|
doReturn(route == 3).when(a.player).isInsideVehicle();
|
|
use(f, a);
|
|
assertEquals(new Vector(0.3, 0.4, 0.5), velocity.get());
|
|
var storage = (CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
|
assertFalse(storage.find(a.core.record.id()).get().orElseThrow().cooldowns().containsKey("quickstep"));
|
|
assertTrue(a.messages.stream().anyMatch(message -> message.contains("not eligible")));
|
|
}
|
|
}
|
|
|
|
@Test void velocityCancellationObstacleAndUnrelatedReplacementAreAuthoritative() throws Exception {
|
|
for (int route = 0; route < 3; route++) try (var f = new FantasyStickLifecycleNativeTest.Fixture(data.resolve("motion-" + route))) {
|
|
f.plugin.setEnabled(true);
|
|
var a = new FantasyStickLifecycleNativeTest.Actor(data.resolve("motion-" + route));
|
|
withStats(data.resolve("motion-" + route), a, 12, true);
|
|
f.players.put(a.core.id, a.player); f.activate(a);
|
|
var velocity = prepareMovement(a, new Vector(0, 0.3, 0));
|
|
if (route == 0) {
|
|
class Veto implements org.bukkit.event.Listener {
|
|
@org.bukkit.event.EventHandler public void velocity(org.bukkit.event.player.PlayerVelocityEvent event) {
|
|
if (event.getPlayer() == a.player) event.setCancelled(true);
|
|
}
|
|
}
|
|
f.dispatcher.register(new Veto(), f.plugin);
|
|
}
|
|
use(f, a); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
|
var storage = (CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
|
assertNotNull(storage.find(a.core.record.id()).get().orElseThrow().cooldowns().get("quickstep"));
|
|
if (route == 0) {
|
|
assertEquals(new Vector(0, 0.3, 0), velocity.get(), "Cancelled velocity applies no movement");
|
|
} else if (route == 1) {
|
|
velocity.set(new Vector(0, 0.3, 0));
|
|
f.dispatcher.accept(new PlayerMoveEvent(a.player, new Location(a.world, 0, 64, 0),
|
|
new Location(a.world, 0.2, 64, 0)));
|
|
assertEquals(new Vector(0, 0.3, 0), velocity.get(), "Native collision stops movement early");
|
|
} else {
|
|
velocity.set(new Vector(0, 0.6, 2));
|
|
f.dispatcher.accept(new PlayerMoveEvent(a.player, new Location(a.world, 0, 64, 0),
|
|
new Location(a.world, 0.2, 64, 0)));
|
|
assertEquals(new Vector(0, 0.6, 2), velocity.get(), "Replacement movement relinquishes ownership");
|
|
}
|
|
verify(a.player, never()).setFallDistance(anyFloat());
|
|
}
|
|
}
|
|
|
|
@Test void unlearnRelearnAndTransferRetainCooldownAndClearOnlyOwnedHorizontalMotion() throws Exception {
|
|
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
|
f.plugin.setEnabled(true);
|
|
var a = new FantasyStickLifecycleNativeTest.Actor(data); withStats(data, a, 12, true);
|
|
f.players.put(a.core.id, a.player); f.activate(a); var velocity = prepareMovement(a, new Vector(0, 0.4, 0));
|
|
use(f, a); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
|
var storage = (CharacterStorage) FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "characters");
|
|
var deadline = storage.find(a.core.record.id()).get().orElseThrow().cooldowns().get("quickstep"); assertNotNull(deadline);
|
|
a.core.inventory.getInventory().setSelectedSlot(0); PartyBookNativeTest.open(f, a);
|
|
var unlearn = abilityButton(a, "[Unlearn]", "quickstep");
|
|
PartyBookNativeTest.click(f, a, unlearn); PartyBookNativeTest.click(f, a, unlearn);
|
|
PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
|
var refunded = storage.find(a.core.record.id()).get().orElseThrow();
|
|
assertEquals(BigInteger.valueOf(40), refunded.unspentPoints()); assertEquals(deadline, refunded.cooldowns().get("quickstep"));
|
|
assertEquals(new Vector(0, 0.4, 0), velocity.get());
|
|
learnAndAssign(f, a); use(f, a);
|
|
assertTrue(a.messages.stream().anyMatch(message -> message.contains("ready in")));
|
|
assertEquals(new Vector(0, 0.4, 0), velocity.get());
|
|
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); prepareMovement(b, new Vector(0.2, 0.3, 0.4)); use(f, b);
|
|
assertTrue(b.messages.stream().anyMatch(message -> message.contains("ready in")));
|
|
}
|
|
}
|
|
|
|
@Test void failedCooldownPersistenceAppliesNoVelocity() {
|
|
var storage = mock(CharacterStorage.class);
|
|
var sessions = mock(games.dmg.fantasystick.session.EquipmentSessions.class);
|
|
var parties = new games.dmg.fantasystick.party.PartyRoster(java.time.Clock.systemUTC(), ignored -> true);
|
|
var registry = new games.dmg.fantasystick.ability.AbilityRegistry();
|
|
var quickstep = new games.dmg.fantasystick.ability.Quickstep(
|
|
games.dmg.fantasystick.ability.Quickstep.Settings.defaults(), ignored -> {});
|
|
registry.register(quickstep.definition());
|
|
var base = new games.dmg.fantasystick.crafting.CharacterFactory().create(
|
|
games.dmg.fantasystick.progression.ProgressionRules.defaults());
|
|
var stats = new EnumMap<>(base.stats()); stats.put(Stat.DEX, BigInteger.valueOf(12));
|
|
var record = new CharacterRecord(base.id(), base.name(), base.revision(), stats, base.level(), base.experience(),
|
|
base.unspentPoints(), base.health(), java.util.Set.of("quickstep"),
|
|
List.of("quickstep", "", "", "", "", "", ""), base.cooldowns(), base.restrictedKills(), false, null, null);
|
|
var active = new games.dmg.fantasystick.session.EquipmentSessions.Active(java.util.UUID.randomUUID(),
|
|
java.util.UUID.randomUUID(), record.id());
|
|
var player = mock(org.bukkit.entity.Player.class);
|
|
when(sessions.current(active)).thenReturn(true); when(sessions.currentRecord(active)).thenReturn(java.util.Optional.of(record));
|
|
doReturn(true).when(player).isOnGround(); doReturn(false).when(player).isFlying();
|
|
doReturn(false).when(player).isGliding(); doReturn(false).when(player).isInsideVehicle();
|
|
var eye = new Location(mock(org.bukkit.World.class), 0, 0, 0); eye.setDirection(new Vector(1, 0, 0));
|
|
doReturn(eye).when(player).getEyeLocation();
|
|
var failed = new java.util.concurrent.CompletableFuture<CharacterStorage.AbilityMutation>();
|
|
failed.completeExceptionally(new java.io.IOException("simulated Quickstep cooldown failure"));
|
|
when(storage.commitAbilityCooldown(eq(record.id()), eq(record.revision()), eq("quickstep"), any())).thenReturn(failed);
|
|
var failures = new java.util.ArrayList<games.dmg.fantasystick.ability.AbilityEngine.Failure>();
|
|
var engine = new games.dmg.fantasystick.ability.AbilityEngine(registry, storage, sessions, parties,
|
|
java.time.Clock.systemUTC(), Runnable::run, (delay, action) -> () -> {}, ignored -> {}, failures::add);
|
|
engine.accept(new games.dmg.fantasystick.session.ControlInput(player, active, 2, "quickstep",
|
|
games.dmg.fantasystick.session.ControlInput.Kind.ABILITY,
|
|
games.dmg.fantasystick.session.ControlInput.Gesture.USE, null, 0, 0, () -> true));
|
|
verify(player, never()).setVelocity(any(Vector.class)); assertEquals(1, failures.size());
|
|
engine.close(); quickstep.close(); parties.close();
|
|
}
|
|
|
|
@Test void acknowledgedUpdatesAffectFutureDashesWhileActiveDashKeepsCapturedBounds() throws Exception {
|
|
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
|
f.plugin.setEnabled(true);
|
|
var a = new FantasyStickLifecycleNativeTest.Actor(data); withStats(data, a, 12, true);
|
|
f.players.put(a.core.id, a.player); f.activate(a); var oldVelocity = prepareMovement(a, new Vector(0, 0.2, 0));
|
|
use(f, a); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
|
var sender = new EquipmentAdminCommandNativeTest.Sender();
|
|
f.command.execute(sender.port, "fantasystick", new String[]{"config", "quickstep-distance-blocks", "7"});
|
|
f.command.execute(sender.port, "fantasystick", new String[]{"config", "quickstep-speed-blocks-per-tick", "2"});
|
|
for (int i = 0; i < 2; i++) {
|
|
var response = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(response); response.run();
|
|
}
|
|
f.dispatcher.accept(new PlayerMoveEvent(a.player, new Location(a.world, 0, 64, 0),
|
|
new Location(a.world, 4, 64, 0)));
|
|
assertEquals(new Vector(0, 0.2, 0), oldVelocity.get(), "Active dash retains its four-block snapshot");
|
|
var b = new FantasyStickLifecycleNativeTest.Actor(data); withStats(data, b, 12, true);
|
|
f.players.put(b.core.id, b.player); f.activate(b); var newVelocity = prepareMovement(b, new Vector(0, 0.3, 0));
|
|
use(f, b); PartyBookNativeTest.drain(f); PartyBookNativeTest.drain(f);
|
|
assertEquals(new Vector(2, 0.3, 0), newVelocity.get(), "Future dash uses acknowledged speed");
|
|
}
|
|
}
|
|
|
|
@Test void configuredDefinitionAdminRoutesAndStartupValidationCoverEveryQuickstepSetting() throws Exception {
|
|
java.nio.file.Files.writeString(data.resolve("config.yml"),
|
|
"quickstep-cooldown-seconds: 2\nquickstep-distance-blocks: 6\nquickstep-speed-blocks-per-tick: 1.5\n");
|
|
try (var f = new FantasyStickLifecycleNativeTest.Fixture(data)) {
|
|
f.plugin.setEnabled(true);
|
|
var registry = (games.dmg.fantasystick.ability.AbilityRegistry)
|
|
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "abilityRegistry");
|
|
var definition = registry.find("quickstep").orElseThrow();
|
|
assertEquals(java.time.Duration.ofSeconds(2), definition.cooldown());
|
|
assertEquals(0, new java.math.BigDecimal("6").compareTo(definition.parameters().get("distance")));
|
|
assertEquals(0, new java.math.BigDecimal("1.5").compareTo(definition.parameters().get("speed")));
|
|
var sender = new EquipmentAdminCommandNativeTest.Sender();
|
|
assertEquals(List.of("adventure-site", "config", "experience", "give", "inspect", "party-registrar", "quest-giver", "recover", "shop", "training-dummy"),
|
|
f.command.tabComplete(sender.port, "fantasystick", new String[]{""}));
|
|
assertEquals(List.of("config"), f.command.tabComplete(sender.port, "fantasystick", new String[]{"co"}));
|
|
assertTrue(f.command.tabComplete(sender.port, "fantasystick", new String[]{"unsupported"}).isEmpty());
|
|
sender.allowed = false;
|
|
assertTrue(f.command.tabComplete(sender.port, "fantasystick", new String[]{"config", "quickstep"}).isEmpty());
|
|
f.command.execute(sender.port, "fantasystick", new String[]{"config", "quickstep-distance-blocks", "7"});
|
|
assertTrue(sender.messages.getLast().contains("Permission denied")); sender.allowed = true;
|
|
assertEquals(List.of("quickstep-cooldown-seconds", "quickstep-distance-blocks", "quickstep-speed-blocks-per-tick"),
|
|
f.command.tabComplete(sender.port, "fantasystick", new String[]{"config", "quickstep-"}));
|
|
assertEquals(List.of("4", "6"), f.command.tabComplete(sender.port, "fantasystick",
|
|
new String[]{"config", "quickstep-distance-blocks", ""}));
|
|
f.command.execute(sender.port, "fantasystick", new String[]{"config", "quickstep-distance-blocks"});
|
|
assertTrue(sender.messages.getLast().contains("=6"));
|
|
f.command.execute(sender.port, "fantasystick", new String[]{"config", "quickstep-distance-blocks", "7"});
|
|
var response = f.tasks.poll(5, java.util.concurrent.TimeUnit.SECONDS); assertNotNull(response); response.run();
|
|
assertEquals(0, new java.math.BigDecimal("7").compareTo(registry.find("quickstep").orElseThrow()
|
|
.parameters().get("distance")));
|
|
f.command.execute(sender.port, "fantasystick", new String[]{"config", "quickstep-distance-blocks", "0"});
|
|
assertTrue(sender.messages.getLast().contains("invalid"));
|
|
f.command.execute(sender.port, "fantasystick",
|
|
new String[]{"config", "quickstep-distance-blocks", "7", "extra"});
|
|
assertTrue(sender.messages.getLast().contains("Usage"));
|
|
f.plugin.setEnabled(false); f.plugin.setEnabled(true);
|
|
var reloaded = (games.dmg.fantasystick.ability.AbilityRegistry)
|
|
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin, "abilityRegistry");
|
|
assertEquals(0, new java.math.BigDecimal("7").compareTo(reloaded.find("quickstep").orElseThrow()
|
|
.parameters().get("distance")));
|
|
}
|
|
for (String invalid : List.of("quickstep-cooldown-seconds: -1\n", "quickstep-distance-blocks: 0\n",
|
|
"quickstep-speed-blocks-per-tick: 0\n")) {
|
|
var directory = data.resolve(Integer.toHexString(invalid.hashCode()));
|
|
java.nio.file.Files.createDirectories(directory); java.nio.file.Files.writeString(directory.resolve("config.yml"), invalid);
|
|
try (var f = new FantasyStickLifecycleNativeTest.Fixture(directory)) {
|
|
assertThrows(IllegalArgumentException.class, () -> f.plugin.setEnabled(true));
|
|
assertTrue(f.order.isEmpty()); assertTrue(f.recipes.isEmpty());
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void learnAndAssign(FantasyStickLifecycleNativeTest.Fixture fixture,
|
|
FantasyStickLifecycleNativeTest.Actor actor) throws Exception {
|
|
actor.core.inventory.getInventory().setSelectedSlot(0); PartyBookNativeTest.open(fixture, actor);
|
|
PartyBookNativeTest.click(fixture, actor, abilityButton(actor, "[Learn]", "quickstep"));
|
|
PartyBookNativeTest.drain(fixture); PartyBookNativeTest.drain(fixture);
|
|
}
|
|
|
|
private static AtomicReference<Vector> prepareMovement(FantasyStickLifecycleNativeTest.Actor actor, Vector initial) {
|
|
var velocity = new AtomicReference<>(initial.clone());
|
|
doReturn(true).when(actor.player).isOnGround(); doReturn(false).when(actor.player).isFlying();
|
|
doReturn(false).when(actor.player).isGliding(); doReturn(false).when(actor.player).isInsideVehicle();
|
|
doAnswer(ignored -> velocity.get().clone()).when(actor.player).getVelocity();
|
|
doAnswer(call -> { velocity.set(call.getArgument(0, Vector.class).clone()); return null; }).when(actor.player).setVelocity(any(Vector.class));
|
|
var eye = new Location(actor.world, 0, 65.6, 0); eye.setDirection(new Vector(1, 0, 0));
|
|
doReturn(eye).when(actor.player).getEyeLocation();
|
|
return velocity;
|
|
}
|
|
|
|
private static void use(FantasyStickLifecycleNativeTest.Fixture fixture,
|
|
FantasyStickLifecycleNativeTest.Actor actor) {
|
|
actor.core.inventory.getInventory().setSelectedSlot(2);
|
|
fixture.dispatcher.accept(new PlayerInteractEvent(actor.player, org.bukkit.event.block.Action.RIGHT_CLICK_AIR,
|
|
actor.core.inventory.getItem(2), null, org.bukkit.block.BlockFace.SELF, EquipmentSlot.HAND));
|
|
}
|
|
|
|
private static ClickEvent.Custom abilityButton(FantasyStickLifecycleNativeTest.Actor actor, String label, String ability) {
|
|
var book = org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(actor.books.getLast())
|
|
.get(net.minecraft.core.component.DataComponents.WRITTEN_BOOK_CONTENT);
|
|
assertNotNull(book);
|
|
for (var page : book.pages()) {
|
|
var found = abilityButton(page.raw(), label, ability);
|
|
if (found != null) return found;
|
|
}
|
|
throw new AssertionError("Missing " + label + " action for " + ability);
|
|
}
|
|
|
|
private static ClickEvent.Custom abilityButton(net.minecraft.network.chat.Component component,
|
|
String label, String ability) {
|
|
if (component.getString().equals(label)
|
|
&& component.getStyle().getClickEvent() instanceof ClickEvent.Custom custom
|
|
&& custom.payload().orElse(null) instanceof net.minecraft.nbt.StringTag payload
|
|
&& payload.value().contains(":" + ability)) return custom;
|
|
for (var child : component.getSiblings()) {
|
|
var found = abilityButton(child, label, ability);
|
|
if (found != null) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static void withStats(Path data, FantasyStickLifecycleNativeTest.Actor actor,
|
|
int dexterity, boolean learned) throws Exception {
|
|
java.nio.file.Files.createDirectories(data);
|
|
var repository = new games.dmg.fantasystick.persistence.YamlCharacterRepository(data);
|
|
var old = repository.load(actor.core.record.id()).orElseThrow();
|
|
var stats = new EnumMap<>(old.stats()); stats.put(Stat.DEX, BigInteger.valueOf(dexterity));
|
|
repository.replace(new CharacterRecord(old.id(), old.name(), old.revision().add(BigInteger.ONE), stats,
|
|
old.level(), old.experience(), BigInteger.valueOf(learned ? 39 : 40), old.health(),
|
|
learned ? java.util.Set.of("quickstep") : old.learned(),
|
|
learned ? List.of("quickstep", "", "", "", "", "", "") : old.loadout(),
|
|
old.cooldowns(), old.restrictedKills(), false, null, null), old.revision());
|
|
}
|
|
}
|