diff --git a/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java b/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java index 4b45d2b..2d99a3a 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java @@ -357,7 +357,8 @@ ItemStack createStatus(Node n) { lore.add(" "); lore.add("§4WARNING!"); lore.add("§cDeactivating resets the current cycle!"); - lore.add("§cThe cost of the current cycle will be refunded."); + lore.add("§cThe inputs of the current cycle will be refunded."); + lore.add("§7Upkeep is charged at each new day while active."); m.setLore(lore); i.setItemMeta(m); } diff --git a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java index 0b9e595..7fae36c 100644 --- a/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java +++ b/src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java @@ -43,6 +43,7 @@ import net.tfminecraft.dowsing.utils.ItemDropper; import net.tfminecraft.dowsing.utils.NodeEngine; import net.tfminecraft.dowsing.utils.NodeReloader; +import net.tfminecraft.dowsing.utils.NodeToggleLog; import net.tfminecraft.dowsing.enums.ConfirmType; import net.tfminecraft.simplefactions.guild.Guild; import net.tfminecraft.simplefactions.managers.FactionManager; @@ -101,10 +102,9 @@ public static double getTotalUpkeep(Guild g) { if (g == null) return 0; double total = 0; for (Node n : nodes) { - if (!n.hasGuild()) continue; + if (!n.getIsActive() || !n.hasGuild()) continue; if (!n.getGuild().getId().equalsIgnoreCase(g.getId())) continue; - Double upkeep = n.getUpkeep(); - if (upkeep != null) total += upkeep; + total += n.getDailyUpkeep(); } return total; } @@ -233,7 +233,9 @@ public void validate() { public void confirmClick(Player p, Node n, ConfirmType t) { if(t != ConfirmType.DEACTIVATE && blockPendingRefund(p, n)) return; if(t.equals(ConfirmType.DEACTIVATE)) { + boolean wasActive = n.getIsActive(); n.deActivate(); + if(wasActive) NodeToggleLog.deactivated(p, n); InventoryManager inv = new InventoryManager(); inv.nodeView(p, n); currentNode.put(p, n); @@ -452,6 +454,7 @@ public void invenClick(InventoryClickEvent e) { } else { n.activate(); if(n.getIsActive()) { + NodeToggleLog.activated(p, n); p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BIT, 1f, 1f); } else { p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f); diff --git a/src/main/java/net/tfminecraft/dowsing/objects/Node.java b/src/main/java/net/tfminecraft/dowsing/objects/Node.java index d1a86ee..dd05a41 100644 --- a/src/main/java/net/tfminecraft/dowsing/objects/Node.java +++ b/src/main/java/net/tfminecraft/dowsing/objects/Node.java @@ -1,6 +1,8 @@ package net.tfminecraft.dowsing.objects; import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -67,6 +69,26 @@ public Double getUpkeep() { public void setUpkeep(Double upkeep) { this.upkeep = upkeep; } + /** + * Charged by the SimpleFactions daily settlement for each day the node is active. The + * over-capacity multiplier is worked out from the guild's current nodes and capacity, + * since members joining or capacity being bought does not refresh every node. + */ + public double getDailyUpkeep() { + double base = upkeep == null ? 0.0 : upkeep; + return BigDecimal.valueOf(base) + .multiply(BigDecimal.valueOf(currentCostIncrease())) + .setScale(2, RoundingMode.HALF_UP) + .doubleValue(); + } + double currentCostIncrease() { + if(resolveGuild() == null) return costIncrease == null ? 1.0 : costIncrease; + return costIncreaseFor(NodeManager.getNodeAmount(this.guild), getCapacity()); + } + static double costIncreaseFor(int nodeAmount, int capacity) { + int multiplier = Math.max(1, 1+nodeAmount-capacity); + return 1.0+((multiplier-1.0)*0.5); + } public UUID getId() { return id; } @@ -432,7 +454,7 @@ public void activate() { this.errors.add("§7No bank"); } if(this.guild != null && this.guild.getBank() != null) { - if(this.guild.isBankrupt() || this.guild.getBank().getWealth() < this.upkeep) { + if(this.guild.isBankrupt() || this.guild.getBank().getWealth() < getDailyUpkeep()) { failed = true; this.errors.add("§7Lacking upkeep"); } @@ -453,7 +475,6 @@ public void activate() { if(failed) { return; } - this.guild.getBank().withdraw(this.upkeep); this.isActive = true; this.timeLeft = this.modifiedTime; this.cycleTime = 0; @@ -565,13 +586,11 @@ public void update() { updateEfficiencyTime(); if(timeLeft > modifiedTime) timeLeft = modifiedTime; setCompleteDrops(); - int newMultipler = 1+NodeManager.getNodeAmount(this.guild)-getCapacity(); - if(newMultipler < 1) { - newMultipler = 1; - } - this.multiplier = newMultipler; + int nodeAmount = NodeManager.getNodeAmount(this.guild); + int capacity = getCapacity(); + this.multiplier = Math.max(1, 1+nodeAmount-capacity); Formatter format = new Formatter(); - this.costIncrease = format.formatDouble(1.0+((multiplier-1.0)*0.5)); + this.costIncrease = format.formatDouble(costIncreaseFor(nodeAmount, capacity)); this.upkeep = format.formatDouble(this.upkeep); } Double getAddedPrestige(String e) { diff --git a/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java b/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java index bd02700..030d949 100644 --- a/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java +++ b/src/main/java/net/tfminecraft/dowsing/utils/ItemCreator.java @@ -137,7 +137,7 @@ public ItemStack createTypeItemNode(Node n, NodeType t, boolean gui) { } lore.add("§eYield: §a"+n.getYield()); Formatter format = new Formatter(); - lore.add("§eTotal Upkeep: §f"+format.formatDouble(n.getUpkeep()*n.getCostIncrease())+"d"); + lore.add("§eTotal Upkeep: §f"+format.formatDouble(n.getDailyUpkeep())+"d/day"); lore.add("§7Possible Drops:"); Double maxWeight = 0.0; for(String s : n.getCompleteDrop().keySet()) { diff --git a/src/main/java/net/tfminecraft/dowsing/utils/NodeEngine.java b/src/main/java/net/tfminecraft/dowsing/utils/NodeEngine.java index 9aa3697..5c30ff8 100644 --- a/src/main/java/net/tfminecraft/dowsing/utils/NodeEngine.java +++ b/src/main/java/net/tfminecraft/dowsing/utils/NodeEngine.java @@ -92,9 +92,6 @@ public void refund(Node n) { inputs.put(key, amount); } } - if(n.getGuild() != null && n.getGuild().getBank() != null) { - n.getGuild().getBank().deposit(n.getUpkeep()); - } n.setInputCounter(n.getInputCounter()-1); for(String key : inputs.keySet()) { addItem(key, inputs.get(key), i); diff --git a/src/main/java/net/tfminecraft/dowsing/utils/NodeToggleLog.java b/src/main/java/net/tfminecraft/dowsing/utils/NodeToggleLog.java new file mode 100644 index 0000000..30fde03 --- /dev/null +++ b/src/main/java/net/tfminecraft/dowsing/utils/NodeToggleLog.java @@ -0,0 +1,82 @@ +package net.tfminecraft.dowsing.utils; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import net.tfminecraft.dowsing.DowsingMain; +import net.tfminecraft.dowsing.objects.Node; +import net.tfminecraft.simplefactions.managers.FactionManager; + +/** + * Upkeep is charged for the nodes active at the new day, so switching a node off just + * before it and on again after skips a day's upkeep. Every manual toggle is recorded with + * the time left until the new day, and one close to it is logged as a warning. + */ +public final class NodeToggleLog { + public static final int WARN_SECONDS_BEFORE_NEW_DAY = 60 * 60; + private static final String FILE_NAME = "node-toggles.log"; + private static final DateTimeFormatter TIMESTAMP = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private NodeToggleLog() {} + + public static void activated(Player player, Node node) { + record(player, node, "activated"); + } + + public static void deactivated(Player player, Node node) { + record(player, node, "deactivated"); + } + + private static void record(Player player, Node node, String action) { + if (player == null || node == null) return; + int secondsUntilNewDay = FactionManager.getSecondsUntilNewDay(); + String line = format(player.getName(), action, node, secondsUntilNewDay); + Logger logger = DowsingMain.plugin.getLogger(); + if (secondsUntilNewDay <= WARN_SECONDS_BEFORE_NEW_DAY) { + logger.warning(line); + } else { + logger.info(line); + } + append(LocalDateTime.now().format(TIMESTAMP) + " " + line); + } + + static String format(String player, String action, Node node, int secondsUntilNewDay) { + String guild = node.getGuild() == null ? "none" : node.getGuild().getId(); + String resource = node.getBlock() == null ? "unknown" : node.getBlock().getResource(); + return String.format(Locale.ROOT, "[NodeToggle] %s %s %s node %s of guild %s at %s, upkeep %.2fd/day, %s until new day", + player, action, resource, node.getId(), guild, location(node.getLoc()), + node.getDailyUpkeep(), duration(secondsUntilNewDay)); + } + + static String duration(int seconds) { + int clamped = Math.max(0, seconds); + return String.format(Locale.ROOT, "%dh %02dm", clamped / 3600, (clamped % 3600) / 60); + } + + private static String location(Location loc) { + if (loc == null) return "unknown"; + String world = loc.getWorld() == null ? "?" : loc.getWorld().getName(); + return world + " " + loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ(); + } + + private static void append(String line) { + File file = new File(DowsingMain.plugin.getDataFolder(), FILE_NAME); + try { + Files.writeString(file.toPath(), line + System.lineSeparator(), StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } catch (IOException e) { + DowsingMain.plugin.getLogger().log(Level.WARNING, "Could not write " + FILE_NAME, e); + } + } +} diff --git a/src/test/java/net/tfminecraft/dowsing/managers/NodeUpkeepTest.java b/src/test/java/net/tfminecraft/dowsing/managers/NodeUpkeepTest.java new file mode 100644 index 0000000..c9861dd --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/managers/NodeUpkeepTest.java @@ -0,0 +1,81 @@ +package net.tfminecraft.dowsing.managers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import net.tfminecraft.dowsing.objects.Node; +import net.tfminecraft.simplefactions.guild.Guild; + +class NodeUpkeepTest { + private List previousNodes; + private Guild guild; + + @BeforeEach + void setUp() { + previousNodes = NodeManager.nodes; + NodeManager.nodes = new ArrayList<>(); + guild = mock(Guild.class); + when(guild.getId()).thenReturn("The_Chisels"); + } + + @AfterEach + void tearDown() { + NodeManager.nodes = previousNodes; + } + + @Test + void onlyActiveNodesOweUpkeep() { + NodeManager.nodes.add(node(guild, true, 40.0, 1.0)); + NodeManager.nodes.add(node(guild, false, 25.0, 1.0)); + + assertEquals(40.0, NodeManager.getTotalUpkeep(guild)); + } + + @Test + void overCapacityMultiplierIsCharged() { + NodeManager.nodes.add(node(guild, true, 40.0, 1.5)); + + assertEquals(60.0, NodeManager.getTotalUpkeep(guild)); + } + + @Test + void upkeepRoundsHalfUpToTheCent() { + NodeManager.nodes.add(node(guild, true, 0.29, 1.5)); + + assertEquals(0.44, NodeManager.getTotalUpkeep(guild)); + } + + @Test + void otherGuildsNodesAreIgnored() { + Guild other = mock(Guild.class); + when(other.getId()).thenReturn("Other"); + NodeManager.nodes.add(node(other, true, 40.0, 1.0)); + + assertEquals(0.0, NodeManager.getTotalUpkeep(guild)); + } + + private static Node node(Guild owner, boolean active, double upkeep, double costIncrease) { + Node node = new Node(java.util.UUID.randomUUID(), null, null, null, active, 1, 0, null, 10, 0, 50) { + @Override + public Boolean hasGuild() { + return true; + } + + @Override + public Guild getGuild() { + return owner; + } + }; + node.setUpkeep(upkeep); + node.setCostIncrease(costIncrease); + return node; + } +} diff --git a/src/test/java/net/tfminecraft/dowsing/objects/NodeCostIncreaseTest.java b/src/test/java/net/tfminecraft/dowsing/objects/NodeCostIncreaseTest.java new file mode 100644 index 0000000..1c945f7 --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/objects/NodeCostIncreaseTest.java @@ -0,0 +1,25 @@ +package net.tfminecraft.dowsing.objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class NodeCostIncreaseTest { + + @Test + void withinCapacityCostsTheBaseAmount() { + assertEquals(1.0, Node.costIncreaseFor(1, 1)); + assertEquals(1.0, Node.costIncreaseFor(0, 3)); + } + + @Test + void eachNodeOverCapacityAddsHalf() { + assertEquals(1.5, Node.costIncreaseFor(2, 1)); + assertEquals(2.0, Node.costIncreaseFor(3, 1)); + } + + @Test + void buyingCapacityLowersTheMultiplier() { + assertEquals(1.5, Node.costIncreaseFor(3, 2)); + } +} diff --git a/src/test/java/net/tfminecraft/dowsing/utils/NodeToggleLogTest.java b/src/test/java/net/tfminecraft/dowsing/utils/NodeToggleLogTest.java new file mode 100644 index 0000000..06368e8 --- /dev/null +++ b/src/test/java/net/tfminecraft/dowsing/utils/NodeToggleLogTest.java @@ -0,0 +1,16 @@ +package net.tfminecraft.dowsing.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class NodeToggleLogTest { + + @Test + void durationShowsHoursAndMinutes() { + assertEquals("0h 00m", NodeToggleLog.duration(0)); + assertEquals("0h 59m", NodeToggleLog.duration(3599)); + assertEquals("23h 30m", NodeToggleLog.duration(23 * 3600 + 30 * 60)); + assertEquals("0h 00m", NodeToggleLog.duration(-5)); + } +}