feat: add stature potions and tiny-player launchers
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import java.util.function.IntUnaryOperator;
|
||||
|
||||
public final class HeightMath {
|
||||
private HeightMath() {
|
||||
}
|
||||
|
||||
public static double randomScale(HeightSettings settings, IntUnaryOperator randomIndex) {
|
||||
int outcomes = settings.randomStepCount() + 1;
|
||||
int index = randomIndex.applyAsInt(outcomes);
|
||||
if (index < 0 || index >= outcomes) {
|
||||
throw new IllegalArgumentException("random index is outside the requested bound");
|
||||
}
|
||||
return normalize(settings.minimum() + index * settings.adjustmentStep());
|
||||
}
|
||||
|
||||
public static double grow(double current, HeightSettings settings) {
|
||||
return clamp(current + settings.adjustmentStep(), settings);
|
||||
}
|
||||
|
||||
public static double shrink(double current, HeightSettings settings) {
|
||||
return clamp(current - settings.adjustmentStep(), settings);
|
||||
}
|
||||
|
||||
public static double safeStoredScale(Double stored, HeightSettings settings) {
|
||||
if (stored == null || !Double.isFinite(stored)) {
|
||||
return clamp(1.0, settings);
|
||||
}
|
||||
return clamp(stored, settings);
|
||||
}
|
||||
|
||||
public static double clamp(double value, HeightSettings settings) {
|
||||
return normalize(Math.max(settings.minimum(), Math.min(settings.maximum(), value)));
|
||||
}
|
||||
|
||||
private static double normalize(double value) {
|
||||
return Math.rint(value * 1_000_000.0) / 1_000_000.0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
public record HeightSettings(
|
||||
double minimum,
|
||||
double maximum,
|
||||
double adjustmentStep,
|
||||
double launcherThreshold,
|
||||
double launcherSpeed,
|
||||
int launcherCooldownTicks) {
|
||||
|
||||
public HeightSettings {
|
||||
requirePositiveFinite("height.minimum", minimum);
|
||||
requirePositiveFinite("height.maximum", maximum);
|
||||
requirePositiveFinite("height.adjustment-step", adjustmentStep);
|
||||
requirePositiveFinite("launcher.maximum-player-scale-exclusive", launcherThreshold);
|
||||
requirePositiveFinite("launcher.speed", launcherSpeed);
|
||||
if (minimum > maximum) {
|
||||
throw new IllegalArgumentException("height.minimum must not exceed height.maximum");
|
||||
}
|
||||
if (minimum < 0.0625 || maximum > 16.0) {
|
||||
throw new IllegalArgumentException("height range must stay within Minecraft's 0.0625 to 16.0 scale range");
|
||||
}
|
||||
if (launcherThreshold < minimum || launcherThreshold > maximum) {
|
||||
throw new IllegalArgumentException("launcher threshold must be within the height range");
|
||||
}
|
||||
if (launcherCooldownTicks < 0) {
|
||||
throw new IllegalArgumentException("launcher.cooldown-ticks must not be negative");
|
||||
}
|
||||
}
|
||||
|
||||
public int randomStepCount() {
|
||||
return (int) Math.floor((maximum - minimum) / adjustmentStep + 1.0e-9);
|
||||
}
|
||||
|
||||
private static void requirePositiveFinite(String key, double value) {
|
||||
if (!Double.isFinite(value) || value <= 0.0) {
|
||||
throw new IllegalArgumentException(key + " must be a finite positive number");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
public final class HeightStore {
|
||||
private final Path statePath;
|
||||
private final YamlConfiguration state;
|
||||
|
||||
public HeightStore(File dataFolder) {
|
||||
statePath = dataFolder.toPath().resolve("state.yml");
|
||||
state = YamlConfiguration.loadConfiguration(statePath.toFile());
|
||||
}
|
||||
|
||||
public synchronized Double find(UUID playerId) {
|
||||
String path = path(playerId);
|
||||
return state.contains(path) ? state.getDouble(path) : null;
|
||||
}
|
||||
|
||||
public synchronized void save(UUID playerId, double scale) throws IOException {
|
||||
state.set(path(playerId), scale);
|
||||
Files.createDirectories(statePath.getParent());
|
||||
Path temporary = statePath.resolveSibling("state.yml.tmp");
|
||||
Files.writeString(temporary, state.saveToString(), StandardCharsets.UTF_8);
|
||||
try {
|
||||
Files.move(temporary, statePath, StandardCopyOption.ATOMIC_MOVE,
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (AtomicMoveNotSupportedException exception) {
|
||||
Files.move(temporary, statePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
private static String path(UUID playerId) {
|
||||
return "players." + playerId + ".scale";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
public record LaunchVector(double x, double y, double z) {
|
||||
public static LaunchVector fromDirection(int x, int y, int z, double speed) {
|
||||
double length = Math.sqrt((double) x * x + (double) y * y + (double) z * z);
|
||||
if (length == 0.0) {
|
||||
throw new IllegalArgumentException("launch direction must not be stationary");
|
||||
}
|
||||
return new LaunchVector(x / length * speed, y / length * speed, z / length * speed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
public final class LauncherPolicy {
|
||||
private LauncherPolicy() {
|
||||
}
|
||||
|
||||
public static boolean isSmallEnough(double scale, double exclusiveThreshold) {
|
||||
return scale < exclusiveThreshold;
|
||||
}
|
||||
|
||||
public static boolean cooldownExpired(Long previousTick, long currentTick, int cooldownTicks) {
|
||||
return previousTick == null || currentTick - previousTick >= cooldownTicks;
|
||||
}
|
||||
|
||||
public static boolean isSafeExit(boolean empty) {
|
||||
return empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.RecipeChoice;
|
||||
import org.bukkit.inventory.ShapedRecipe;
|
||||
import org.bukkit.inventory.meta.PotionMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.potion.PotionType;
|
||||
|
||||
public final class PotionRecipes {
|
||||
static final String[] SHIFTING_SHAPE = {"ACA", "AWA", "ACA"};
|
||||
static final String[] GROWTH_SHAPE = {"GAG", "ASA", "GRG"};
|
||||
static final String[] DIMINUTION_SHAPE = {"GAG", "ASA", "GFG"};
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final NamespacedKey potionKindKey;
|
||||
|
||||
public PotionRecipes(JavaPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
potionKindKey = new NamespacedKey(plugin, "stature_potion");
|
||||
}
|
||||
|
||||
public void register() {
|
||||
registerShifting();
|
||||
registerGrowth();
|
||||
registerDiminution();
|
||||
}
|
||||
|
||||
public ItemStack create(StaturePotion kind) {
|
||||
ItemStack item = new ItemStack(Material.POTION);
|
||||
PotionMeta meta = (PotionMeta) item.getItemMeta();
|
||||
meta.displayName(Component.text(kind.displayName(), NamedTextColor.LIGHT_PURPLE));
|
||||
meta.getPersistentDataContainer().set(potionKindKey, PersistentDataType.STRING, kind.name());
|
||||
meta.setColor(color(kind));
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
public StaturePotion identify(ItemStack item) {
|
||||
if (item == null || item.getType() != Material.POTION || !(item.getItemMeta() instanceof PotionMeta meta)) {
|
||||
return null;
|
||||
}
|
||||
String value = meta.getPersistentDataContainer().get(potionKindKey, PersistentDataType.STRING);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return StaturePotion.valueOf(value);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAwkwardPotion(ItemStack item) {
|
||||
return item != null && item.getItemMeta() instanceof PotionMeta meta
|
||||
&& meta.getBasePotionType() == PotionType.AWKWARD;
|
||||
}
|
||||
|
||||
public NamespacedKey shiftingKey() {
|
||||
return new NamespacedKey(plugin, "shifting_stature");
|
||||
}
|
||||
|
||||
private void registerShifting() {
|
||||
ShapedRecipe recipe = new ShapedRecipe(shiftingKey(), create(StaturePotion.SHIFTING));
|
||||
recipe.shape(SHIFTING_SHAPE);
|
||||
recipe.setIngredient('A', Material.AMETHYST_SHARD);
|
||||
recipe.setIngredient('C', Material.CHORUS_FRUIT);
|
||||
recipe.setIngredient('W', Material.POTION);
|
||||
plugin.getServer().addRecipe(recipe);
|
||||
}
|
||||
|
||||
private void registerGrowth() {
|
||||
ShapedRecipe recipe = new ShapedRecipe(new NamespacedKey(plugin, "growth"), create(StaturePotion.GROWTH));
|
||||
recipe.shape(GROWTH_SHAPE);
|
||||
recipe.setIngredient('G', Material.GOLD_INGOT);
|
||||
recipe.setIngredient('A', Material.AMETHYST_SHARD);
|
||||
recipe.setIngredient('S', new RecipeChoice.ExactChoice(create(StaturePotion.SHIFTING)));
|
||||
recipe.setIngredient('R', Material.RABBIT_FOOT);
|
||||
plugin.getServer().addRecipe(recipe);
|
||||
}
|
||||
|
||||
private void registerDiminution() {
|
||||
ShapedRecipe recipe = new ShapedRecipe(new NamespacedKey(plugin, "diminution"),
|
||||
create(StaturePotion.DIMINUTION));
|
||||
recipe.shape(DIMINUTION_SHAPE);
|
||||
recipe.setIngredient('G', Material.GOLD_INGOT);
|
||||
recipe.setIngredient('A', Material.AMETHYST_SHARD);
|
||||
recipe.setIngredient('S', new RecipeChoice.ExactChoice(create(StaturePotion.SHIFTING)));
|
||||
recipe.setIngredient('F', Material.FERMENTED_SPIDER_EYE);
|
||||
plugin.getServer().addRecipe(recipe);
|
||||
}
|
||||
|
||||
private static Color color(StaturePotion kind) {
|
||||
return switch (kind) {
|
||||
case SHIFTING -> Color.PURPLE;
|
||||
case GROWTH -> Color.LIME;
|
||||
case DIMINUTION -> Color.FUCHSIA;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class SpigotHeightsPlugin extends JavaPlugin {
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
HeightSettings settings;
|
||||
try {
|
||||
settings = loadSettings(getConfig());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
getLogger().severe("Invalid configuration: " + exception.getMessage());
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
return;
|
||||
}
|
||||
|
||||
HeightStore store = new HeightStore(getDataFolder());
|
||||
PotionRecipes potions = new PotionRecipes(this);
|
||||
potions.register();
|
||||
getServer().getPluginManager().registerEvents(
|
||||
new StatureListener(this, settings, store, potions), this);
|
||||
getServer().getPluginManager().registerEvents(new TinyPlayerLauncher(settings), this);
|
||||
getLogger().info("Spigot Heights enabled.");
|
||||
}
|
||||
|
||||
static HeightSettings loadSettings(FileConfiguration config) {
|
||||
return new HeightSettings(
|
||||
config.getDouble("height.minimum"),
|
||||
config.getDouble("height.maximum"),
|
||||
config.getDouble("height.adjustment-step"),
|
||||
config.getDouble("launcher.maximum-player-scale-exclusive"),
|
||||
config.getDouble("launcher.speed"),
|
||||
config.getInt("launcher.cooldown-ticks"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.attribute.AttributeInstance;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.inventory.PrepareItemCraftEvent;
|
||||
import org.bukkit.event.player.PlayerItemConsumeEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.Recipe;
|
||||
import org.bukkit.Keyed;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
public final class StatureListener implements Listener {
|
||||
private final JavaPlugin plugin;
|
||||
private final HeightSettings settings;
|
||||
private final HeightStore store;
|
||||
private final PotionRecipes potions;
|
||||
|
||||
public StatureListener(JavaPlugin plugin, HeightSettings settings, HeightStore store, PotionRecipes potions) {
|
||||
this.plugin = plugin;
|
||||
this.settings = settings;
|
||||
this.store = store;
|
||||
this.potions = potions;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onCraftPrepare(PrepareItemCraftEvent event) {
|
||||
Recipe recipe = event.getRecipe();
|
||||
if (!(recipe instanceof Keyed keyed) || !keyed.getKey().equals(potions.shiftingKey())) {
|
||||
return;
|
||||
}
|
||||
ItemStack[] matrix = event.getInventory().getMatrix();
|
||||
if (matrix.length < 5 || !potions.isAwkwardPotion(matrix[4])) {
|
||||
event.getInventory().setResult(null);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onConsume(PlayerItemConsumeEvent event) {
|
||||
StaturePotion kind = potions.identify(event.getItem());
|
||||
if (kind == null) {
|
||||
return;
|
||||
}
|
||||
Player player = event.getPlayer();
|
||||
double current = currentScale(player);
|
||||
double scale = switch (kind) {
|
||||
case SHIFTING -> HeightMath.randomScale(settings,
|
||||
bound -> ThreadLocalRandom.current().nextInt(bound));
|
||||
case GROWTH -> HeightMath.grow(current, settings);
|
||||
case DIMINUTION -> HeightMath.shrink(current, settings);
|
||||
};
|
||||
applyAndSave(player, scale);
|
||||
player.sendMessage("Your scale is now " + scale + ".");
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
apply(event.getPlayer(), HeightMath.safeStoredScale(store.find(event.getPlayer().getUniqueId()), settings));
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onRespawn(PlayerRespawnEvent event) {
|
||||
plugin.getServer().getScheduler().runTask(plugin, () -> {
|
||||
Player player = event.getPlayer();
|
||||
apply(player, HeightMath.safeStoredScale(store.find(player.getUniqueId()), settings));
|
||||
});
|
||||
}
|
||||
|
||||
private void applyAndSave(Player player, double scale) {
|
||||
apply(player, scale);
|
||||
try {
|
||||
store.save(player.getUniqueId(), scale);
|
||||
} catch (IOException exception) {
|
||||
plugin.getLogger().severe("Could not save scale for " + player.getUniqueId() + ": "
|
||||
+ exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void apply(Player player, double scale) {
|
||||
AttributeInstance attribute = player.getAttribute(Attribute.SCALE);
|
||||
if (attribute != null) {
|
||||
attribute.setBaseValue(scale);
|
||||
}
|
||||
}
|
||||
|
||||
private static double currentScale(Player player) {
|
||||
AttributeInstance attribute = player.getAttribute(Attribute.SCALE);
|
||||
return attribute == null ? 1.0 : attribute.getBaseValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
public enum StaturePotion {
|
||||
SHIFTING("Potion of Shifting Stature"),
|
||||
GROWTH("Potion of Growth"),
|
||||
DIMINUTION("Potion of Diminution");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
StaturePotion(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String displayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.attribute.AttributeInstance;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.Directional;
|
||||
import org.bukkit.block.data.type.Hopper;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
public final class TinyPlayerLauncher implements Listener {
|
||||
private final HeightSettings settings;
|
||||
private final Map<UUID, Long> lastLaunchTicks = new HashMap<>();
|
||||
|
||||
public TinyPlayerLauncher(HeightSettings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onMove(PlayerMoveEvent event) {
|
||||
Location destination = event.getTo();
|
||||
if (destination == null || sameBlock(event.getFrom(), destination)) {
|
||||
return;
|
||||
}
|
||||
Player player = event.getPlayer();
|
||||
if (!LauncherPolicy.isSmallEnough(scale(player), settings.launcherThreshold())) {
|
||||
return;
|
||||
}
|
||||
long tick = Bukkit.getCurrentTick();
|
||||
Long lastTick = lastLaunchTicks.get(player.getUniqueId());
|
||||
if (!LauncherPolicy.cooldownExpired(lastTick, tick, settings.launcherCooldownTicks())) {
|
||||
return;
|
||||
}
|
||||
Block hopperBlock = destination.clone().subtract(0.0, 0.1, 0.0).getBlock();
|
||||
if (!(hopperBlock.getBlockData() instanceof Hopper hopper)) {
|
||||
return;
|
||||
}
|
||||
Block dispenser = hopperBlock.getRelative(hopper.getFacing());
|
||||
if (dispenser.getType() != Material.DISPENSER
|
||||
|| !(dispenser.getBlockData() instanceof Directional directional)) {
|
||||
return;
|
||||
}
|
||||
BlockFace facing = directional.getFacing();
|
||||
Block exit = dispenser.getRelative(facing);
|
||||
if (!LauncherPolicy.isSafeExit(exit.isEmpty())) {
|
||||
return;
|
||||
}
|
||||
Location exitLocation = exit.getLocation().add(0.5, 0.1, 0.5);
|
||||
exitLocation.setYaw(player.getLocation().getYaw());
|
||||
exitLocation.setPitch(player.getLocation().getPitch());
|
||||
if (!player.teleport(exitLocation)) {
|
||||
return;
|
||||
}
|
||||
Vector direction = facing.getDirection();
|
||||
LaunchVector launch = LaunchVector.fromDirection(
|
||||
direction.getBlockX(), direction.getBlockY(), direction.getBlockZ(), settings.launcherSpeed());
|
||||
player.setVelocity(new Vector(launch.x(), launch.y(), launch.z()));
|
||||
lastLaunchTicks.put(player.getUniqueId(), tick);
|
||||
}
|
||||
|
||||
private static boolean sameBlock(Location first, Location second) {
|
||||
return first.getWorld().equals(second.getWorld())
|
||||
&& first.getBlockX() == second.getBlockX()
|
||||
&& first.getBlockY() == second.getBlockY()
|
||||
&& first.getBlockZ() == second.getBlockZ();
|
||||
}
|
||||
|
||||
private static double scale(Player player) {
|
||||
AttributeInstance attribute = player.getAttribute(Attribute.SCALE);
|
||||
return attribute == null ? 1.0 : attribute.getValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
height:
|
||||
minimum: 0.4
|
||||
maximum: 2.0
|
||||
adjustment-step: 0.1
|
||||
|
||||
launcher:
|
||||
maximum-player-scale-exclusive: 0.5
|
||||
speed: 1.5
|
||||
cooldown-ticks: 20
|
||||
@@ -0,0 +1,6 @@
|
||||
name: SpigotHeights
|
||||
version: ${version}
|
||||
main: games.dmg.spigotheights.SpigotHeightsPlugin
|
||||
api-version: "1.21"
|
||||
description: Craftable player stature potions and tiny-player dispenser launchers.
|
||||
author: dmg.games
|
||||
@@ -0,0 +1,29 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HeightMathTest {
|
||||
private static final HeightSettings SETTINGS = new HeightSettings(0.4, 2.0, 0.1, 0.5, 1.5, 20);
|
||||
|
||||
@Test
|
||||
void randomSelectionCanReachBothInclusiveEndpoints() {
|
||||
assertEquals(0.4, HeightMath.randomScale(SETTINGS, bound -> 0), 0.000001);
|
||||
assertEquals(2.0, HeightMath.randomScale(SETTINGS, bound -> bound - 1), 0.000001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void adjustmentsClampAtLimitsWithoutFloatingPointDrift() {
|
||||
assertEquals(2.0, HeightMath.grow(1.95, SETTINGS), 0.000001);
|
||||
assertEquals(0.4, HeightMath.shrink(0.42, SETTINGS), 0.000001);
|
||||
assertEquals(1.1, HeightMath.grow(1.0, SETTINGS), 0.000001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingAndStoredValuesAreSafelyClamped() {
|
||||
assertEquals(1.0, HeightMath.safeStoredScale(null, SETTINGS), 0.000001);
|
||||
assertEquals(0.4, HeightMath.safeStoredScale(-2.0, SETTINGS), 0.000001);
|
||||
assertEquals(2.0, HeightMath.safeStoredScale(4.0, SETTINGS), 0.000001);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class HeightSettingsTest {
|
||||
@Test
|
||||
void acceptsDocumentedDefaults() {
|
||||
HeightSettings settings = new HeightSettings(0.4, 2.0, 0.1, 0.5, 1.5, 20);
|
||||
assertEquals(0.4, settings.minimum());
|
||||
assertEquals(16, settings.randomStepCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidRangesAndNumbers() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new HeightSettings(2.0, 0.4, 0.1, 0.5, 1.5, 20));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new HeightSettings(0.4, 2.0, 0.0, 0.5, 1.5, 20));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new HeightSettings(0.4, 2.0, 0.1, 2.1, 1.5, 20));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new HeightSettings(0.4, Double.NaN, 0.1, 0.5, 1.5, 20));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class HeightStoreTest {
|
||||
@TempDir
|
||||
Path temporaryDirectory;
|
||||
|
||||
@Test
|
||||
void storesByUuidAndPreservesUnknownYamlFields() throws Exception {
|
||||
UUID playerId = UUID.randomUUID();
|
||||
Path stateFile = temporaryDirectory.resolve("state.yml");
|
||||
Files.writeString(stateFile, "future-setting: retained\n");
|
||||
HeightStore store = new HeightStore(temporaryDirectory.toFile());
|
||||
|
||||
store.save(playerId, 0.7);
|
||||
|
||||
HeightStore reloaded = new HeightStore(temporaryDirectory.toFile());
|
||||
assertEquals(0.7, reloaded.find(playerId));
|
||||
assertEquals(true, Files.readString(stateFile).contains("future-setting: retained"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class LaunchVectorTest {
|
||||
@Test
|
||||
void supportsEveryDispenserAxisAtConfiguredSpeed() {
|
||||
assertVector(1.5, 0.0, 0.0, LaunchVector.fromDirection(1, 0, 0, 1.5));
|
||||
assertVector(-1.5, 0.0, 0.0, LaunchVector.fromDirection(-1, 0, 0, 1.5));
|
||||
assertVector(0.0, 1.5, 0.0, LaunchVector.fromDirection(0, 1, 0, 1.5));
|
||||
assertVector(0.0, -1.5, 0.0, LaunchVector.fromDirection(0, -1, 0, 1.5));
|
||||
assertVector(0.0, 0.0, 1.5, LaunchVector.fromDirection(0, 0, 1, 1.5));
|
||||
assertVector(0.0, 0.0, -1.5, LaunchVector.fromDirection(0, 0, -1, 1.5));
|
||||
}
|
||||
|
||||
private static void assertVector(double x, double y, double z, LaunchVector actual) {
|
||||
assertEquals(x, actual.x(), 0.000001);
|
||||
assertEquals(y, actual.y(), 0.000001);
|
||||
assertEquals(z, actual.z(), 0.000001);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class LauncherPolicyTest {
|
||||
@Test
|
||||
void thresholdIsStrictlyExclusive() {
|
||||
assertTrue(LauncherPolicy.isSmallEnough(0.4999, 0.5));
|
||||
assertFalse(LauncherPolicy.isSmallEnough(0.5, 0.5));
|
||||
assertFalse(LauncherPolicy.isSmallEnough(1.0, 0.5));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cooldownExpiresAtConfiguredTick() {
|
||||
assertFalse(LauncherPolicy.cooldownExpired(100L, 119, 20));
|
||||
assertTrue(LauncherPolicy.cooldownExpired(100L, 120, 20));
|
||||
assertTrue(LauncherPolicy.cooldownExpired(null, 1, 20));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onlyEmptyDispenserExitsAreSafe() {
|
||||
assertTrue(LauncherPolicy.isSafeExit(true));
|
||||
assertFalse(LauncherPolicy.isSafeExit(false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package games.dmg.spigotheights;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PotionRecipesTest {
|
||||
@Test
|
||||
void recipesUseApprovedShapes() {
|
||||
assertArrayEquals(new String[] {"ACA", "AWA", "ACA"}, PotionRecipes.SHIFTING_SHAPE);
|
||||
assertArrayEquals(new String[] {"GAG", "ASA", "GRG"}, PotionRecipes.GROWTH_SHAPE);
|
||||
assertArrayEquals(new String[] {"GAG", "ASA", "GFG"}, PotionRecipes.DIMINUTION_SHAPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyPotionHasASeparatePersistentIdentity() {
|
||||
assertNotEquals(StaturePotion.SHIFTING.name(), StaturePotion.GROWTH.name());
|
||||
assertNotEquals(StaturePotion.GROWTH.name(), StaturePotion.DIMINUTION.name());
|
||||
assertNotEquals(StaturePotion.DIMINUTION.name(), StaturePotion.SHIFTING.name());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user