From c9f77bed05fc77fd5fb3ad722b5a5dfc24c934fe Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Jan 2026 08:29:03 +0000 Subject: [PATCH] Implement Phase 2: Trader Enhancements with stock and currency system - Add EnumStockReset.java for stock reset timing types - NONE, MCDAILY, MCWEEKLY, MCCUSTOM (MC time) - RLDAILY, RLWEEKLY, RLCUSTOM (real time) - Helper methods for interval calculation and display formatting - Add TraderStock.java for stock management - Per-slot stock limits with -1 for unlimited - Per-player vs per-server stock tracking modes - Automatic reset timer with configurable intervals - PlayerTraderStock inner class for per-player tracking - NBT serialization for persistence - Modify RoleTrader.java to support stock and currency - Add TraderStock instance for stock management - Add useCurrency flag and slotPrices array for virtual currency - Auto-check for stock reset on interact() - Helper methods: hasStock(), getAvailableStock(), consumeStock() - Helper methods: getSlotPrice(), setSlotPrice(), hasSlotPrice() - getTimeUntilResetFormatted() for GUI display - Modify ContainerNPCTrader.java for transactions - Check stock availability before purchase - Handle virtual currency purchases via CurrencyController - Fall back to item-based currency when useCurrency disabled - Consume stock after successful purchase --- .../noppes/npcs/constants/EnumStockReset.java | 64 ++++ .../npcs/containers/ContainerNPCTrader.java | 37 ++- .../npcs/controllers/data/TraderStock.java | 313 ++++++++++++++++++ .../java/noppes/npcs/roles/RoleTrader.java | 126 +++++++ 4 files changed, 535 insertions(+), 5 deletions(-) create mode 100644 src/main/java/noppes/npcs/constants/EnumStockReset.java create mode 100644 src/main/java/noppes/npcs/controllers/data/TraderStock.java diff --git a/src/main/java/noppes/npcs/constants/EnumStockReset.java b/src/main/java/noppes/npcs/constants/EnumStockReset.java new file mode 100644 index 000000000..cdc56cb95 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumStockReset.java @@ -0,0 +1,64 @@ +package noppes.npcs.constants; + +/** + * Stock reset types for Trader NPCs. + * Mirrors EnumQuestRepeat pattern for consistency. + */ +public enum EnumStockReset { + NONE, // Never resets + MCDAILY, // Every 24000 MC ticks (1 MC day) + MCWEEKLY, // Every 168000 MC ticks (7 MC days) + MCCUSTOM, // Custom MC tick interval + RLDAILY, // Every 24 real hours + RLWEEKLY, // Every 7 real days + RLCUSTOM; // Custom real-time interval + + /** + * Get reset interval in milliseconds for real-time types + * or ticks for MC-time types + */ + public long getDefaultInterval() { + switch (this) { + case MCDAILY: + return 24000; // MC ticks + case MCWEEKLY: + return 168000; // MC ticks (7 days) + case RLDAILY: + return 86400000L; // 24 hours in ms + case RLWEEKLY: + return 604800000L; // 7 days in ms + default: + return 0; + } + } + + /** + * Check if this reset type uses real-world time + */ + public boolean isRealTime() { + return this == RLDAILY || this == RLWEEKLY || this == RLCUSTOM; + } + + /** + * Check if this reset type uses Minecraft time + */ + public boolean isMinecraftTime() { + return this == MCDAILY || this == MCWEEKLY || this == MCCUSTOM; + } + + /** + * Get display name for GUI + */ + public String getDisplayName() { + switch (this) { + case NONE: return "Never"; + case MCDAILY: return "MC Daily"; + case MCWEEKLY: return "MC Weekly"; + case MCCUSTOM: return "MC Custom"; + case RLDAILY: return "Real Daily"; + case RLWEEKLY: return "Real Weekly"; + case RLCUSTOM: return "Real Custom"; + default: return "Unknown"; + } + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java index 71dcd6fa6..936868152 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java @@ -4,6 +4,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.controllers.CurrencyController; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.roles.RoleTrader; @@ -61,17 +62,43 @@ public class ContainerNPCTrader extends ContainerNpcInterface { ItemStack item = slot.getStack(); if (!canGivePlayer(item, entityplayer)) return null; - if (!canBuy(i, entityplayer)) - return null; if (!isSlotEnabled(i, entityplayer)) return null; - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); - NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + + // Check stock availability + String playerName = entityplayer.getCommandSenderName(); + if (!role.hasStock(i, playerName, 1)) { + return null; // Out of stock + } + + // Handle currency-based purchase + if (role.hasSlotPrice(i)) { + long price = role.getSlotPrice(i); + if (CurrencyController.Instance == null) { + return null; // Currency system not available + } + if (!CurrencyController.Instance.canAfford(entityplayer, price)) { + return null; // Can't afford + } + // Withdraw currency + if (!CurrencyController.Instance.withdraw(entityplayer, price)) { + return null; // Withdrawal failed + } + } else { + // Item-based currency - existing logic + if (!canBuy(i, entityplayer)) + return null; + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT); + NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT); + } + + // Consume stock + role.consumeStock(i, playerName, 1); + ItemStack soldItem = item.copy(); givePlayer(soldItem, entityplayer); role.addPurchase(i, entityplayer.getDisplayName()); return soldItem; - } public boolean isSlotEnabled(int slot, EntityPlayer player) { diff --git a/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/src/main/java/noppes/npcs/controllers/data/TraderStock.java new file mode 100644 index 000000000..808b06f36 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -0,0 +1,313 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.constants.EnumStockReset; + +import java.util.HashMap; +import java.util.Map; + +/** + * Manages trader stock system including: + * - Per-slot stock limits + * - Per-player vs per-server stock tracking + * - Automatic stock reset timers + */ +public class TraderStock { + // Stock configuration + public boolean enableStock = false; + public boolean perPlayer = false; // false = per-server, true = per-player + + // Reset configuration (mirrors Quest cooldown system) + public EnumStockReset resetType = EnumStockReset.NONE; + public long customResetTime = 0; // For MCCUSTOM/RLCUSTOM types (in ticks or ms) + + // Stock quantities per slot (max stock, -1 = unlimited) + public int[] maxStock = new int[18]; + + // Current stock (per-server mode) + public int[] currentStock = new int[18]; + public long lastResetTime = 0; + + // Per-player stock tracking + public HashMap playerStock = new HashMap<>(); + + public TraderStock() { + for (int i = 0; i < 18; i++) { + maxStock[i] = -1; // -1 = unlimited + currentStock[i] = -1; + } + } + + /** + * Get available stock for a slot + * @param slot The slot index + * @param playerName The player name (for per-player mode) + * @return Available stock count, or Integer.MAX_VALUE if unlimited + */ + public int getAvailableStock(int slot, String playerName) { + if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { + return Integer.MAX_VALUE; // Unlimited + } + + if (perPlayer) { + PlayerTraderStock pStock = playerStock.get(playerName); + if (pStock == null) { + return maxStock[slot]; // Not purchased yet + } + return pStock.getStock(slot, maxStock[slot]); + } + + return Math.max(0, currentStock[slot]); + } + + /** + * Check if stock is available for purchase + */ + public boolean hasStock(int slot, String playerName, int amount) { + return getAvailableStock(slot, playerName) >= amount; + } + + /** + * Consume stock when purchase is made + * @return true if successful, false if insufficient stock + */ + public boolean consumeStock(int slot, String playerName, int amount) { + if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) { + return true; // Unlimited, always succeeds + } + + if (perPlayer) { + PlayerTraderStock pStock = playerStock.computeIfAbsent( + playerName, k -> new PlayerTraderStock()); + return pStock.consumeStock(slot, maxStock[slot], amount); + } + + if (currentStock[slot] >= amount) { + currentStock[slot] -= amount; + return true; + } + return false; + } + + /** + * Check if stock should reset based on current time + * @param currentTime Current MC world time (ticks) or real time (ms) depending on reset type + */ + public boolean shouldReset(long currentTime) { + if (resetType == EnumStockReset.NONE) { + return false; + } + + long elapsed = currentTime - lastResetTime; + long resetInterval = getResetInterval(); + + return elapsed >= resetInterval; + } + + /** + * Reset all stock to max values + * @param currentTime Current time to record as reset time + */ + public void resetStock(long currentTime) { + lastResetTime = currentTime; + + // Reset per-server stock + for (int i = 0; i < 18; i++) { + currentStock[i] = maxStock[i]; + } + + // Clear per-player stock + playerStock.clear(); + } + + /** + * Get reset interval based on reset type + * @return Interval in ticks (MC time) or milliseconds (real time) + */ + public long getResetInterval() { + switch (resetType) { + case MCDAILY: + return 24000; // 1 MC day in ticks + case MCWEEKLY: + return 168000; // 7 MC days in ticks + case MCCUSTOM: + return customResetTime; // Custom ticks + case RLDAILY: + return 86400000L; // 24 hours in ms + case RLWEEKLY: + return 604800000L; // 7 days in ms + case RLCUSTOM: + return customResetTime; // Custom ms + default: + return Long.MAX_VALUE; + } + } + + /** + * Get time until next reset + * @param currentTime Current time + * @return Time remaining until reset, or -1 if no reset scheduled + */ + public long getTimeUntilReset(long currentTime) { + if (resetType == EnumStockReset.NONE) { + return -1; + } + long elapsed = currentTime - lastResetTime; + long interval = getResetInterval(); + return Math.max(0, interval - elapsed); + } + + /** + * Format time until reset for display + * @param currentTime Current time + * @return Formatted string like "2 hours" or "1 day, 5 hours" + */ + public String getTimeUntilResetFormatted(long currentTime) { + long remaining = getTimeUntilReset(currentTime); + if (remaining < 0) { + return ""; + } + + if (resetType.isRealTime()) { + // Convert ms to readable format + long hours = remaining / 3600000L; + long days = hours / 24; + hours = hours % 24; + + if (days > 0) { + return days + " day" + (days != 1 ? "s" : "") + + (hours > 0 ? ", " + hours + " hour" + (hours != 1 ? "s" : "") : ""); + } else if (hours > 0) { + return hours + " hour" + (hours != 1 ? "s" : ""); + } else { + long minutes = remaining / 60000L; + return minutes + " minute" + (minutes != 1 ? "s" : ""); + } + } else { + // Convert ticks to MC time format + long mcHours = remaining / 1000; // 1000 ticks = 1 MC hour + long mcDays = mcHours / 24; + mcHours = mcHours % 24; + + if (mcDays > 0) { + return mcDays + " MC day" + (mcDays != 1 ? "s" : "") + + (mcHours > 0 ? ", " + mcHours + " MC hour" + (mcHours != 1 ? "s" : "") : ""); + } else { + return mcHours + " MC hour" + (mcHours != 1 ? "s" : ""); + } + } + } + + /** + * Set stock for a specific slot + */ + public void setMaxStock(int slot, int amount) { + if (slot >= 0 && slot < 18) { + maxStock[slot] = amount; + if (currentStock[slot] < 0 || currentStock[slot] > amount) { + currentStock[slot] = amount; + } + } + } + + // ==================== NBT Serialization ==================== + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("EnableStock", enableStock); + compound.setBoolean("PerPlayer", perPlayer); + compound.setInteger("ResetType", resetType.ordinal()); + compound.setLong("CustomResetTime", customResetTime); + compound.setIntArray("MaxStock", maxStock); + compound.setIntArray("CurrentStock", currentStock); + compound.setLong("LastResetTime", lastResetTime); + + // Save per-player stock + NBTTagList playerList = new NBTTagList(); + for (Map.Entry entry : playerStock.entrySet()) { + NBTTagCompound playerTag = new NBTTagCompound(); + playerTag.setString("Player", entry.getKey()); + entry.getValue().writeToNBT(playerTag); + playerList.appendTag(playerTag); + } + compound.setTag("PlayerStock", playerList); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + enableStock = compound.getBoolean("EnableStock"); + perPlayer = compound.getBoolean("PerPlayer"); + + int resetOrdinal = compound.getInteger("ResetType"); + if (resetOrdinal >= 0 && resetOrdinal < EnumStockReset.values().length) { + resetType = EnumStockReset.values()[resetOrdinal]; + } + + customResetTime = compound.getLong("CustomResetTime"); + + int[] loadedMax = compound.getIntArray("MaxStock"); + int[] loadedCurrent = compound.getIntArray("CurrentStock"); + + if (loadedMax != null && loadedMax.length == 18) { + maxStock = loadedMax; + } + if (loadedCurrent != null && loadedCurrent.length == 18) { + currentStock = loadedCurrent; + } + + lastResetTime = compound.getLong("LastResetTime"); + + // Load per-player stock + playerStock.clear(); + NBTTagList playerList = compound.getTagList("PlayerStock", 10); + for (int i = 0; i < playerList.tagCount(); i++) { + NBTTagCompound playerTag = playerList.getCompoundTagAt(i); + String playerName = playerTag.getString("Player"); + PlayerTraderStock pStock = new PlayerTraderStock(); + pStock.readFromNBT(playerTag); + playerStock.put(playerName, pStock); + } + } + + // ==================== Inner Class for Per-Player Stock ==================== + + public static class PlayerTraderStock { + public int[] purchasedAmounts = new int[18]; + public long lastPurchaseTime = 0; + + public PlayerTraderStock() { + for (int i = 0; i < 18; i++) { + purchasedAmounts[i] = 0; + } + } + + public int getStock(int slot, int maxStock) { + if (slot < 0 || slot >= 18) return 0; + return Math.max(0, maxStock - purchasedAmounts[slot]); + } + + public boolean consumeStock(int slot, int maxStock, int amount) { + if (slot < 0 || slot >= 18) return false; + if (getStock(slot, maxStock) >= amount) { + purchasedAmounts[slot] += amount; + lastPurchaseTime = System.currentTimeMillis(); + return true; + } + return false; + } + + public void writeToNBT(NBTTagCompound compound) { + compound.setIntArray("Purchased", purchasedAmounts); + compound.setLong("LastPurchase", lastPurchaseTime); + } + + public void readFromNBT(NBTTagCompound compound) { + int[] loaded = compound.getIntArray("Purchased"); + if (loaded != null && loaded.length == 18) { + purchasedAmounts = loaded; + } + lastPurchaseTime = compound.getLong("LastPurchase"); + } + } +} diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 5275c26c0..6215eef2f 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -4,11 +4,13 @@ import foxz.utils.Market; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; import noppes.npcs.NBTTags; import noppes.npcs.NoppesUtilPlayer; import noppes.npcs.NoppesUtilServer; import noppes.npcs.NpcMiscInventory; import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.data.TraderStock; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashMap; @@ -30,6 +32,13 @@ public class RoleTrader extends RoleInterface { public HashMap playerPurchases; public HashMap playerDisableSlot; + // Stock system + public TraderStock stock = new TraderStock(); + + // Currency system + public boolean useCurrency = false; // Use virtual currency instead of items + public long[] slotPrices = new long[18]; // Currency prices per slot + public RoleTrader(EntityNPCInterface npc) { super(npc); inventoryCurrency = new NpcMiscInventory(36); @@ -59,6 +68,20 @@ public class RoleTrader extends RoleInterface { nbttagcompound.setIntArray("Purchases", purchases); nbttagcompound.setTag("PlayerPurchases", NBTTags.nbtStringIntegerArrayMap(playerPurchases)); } + + // Stock system + nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound())); + + // Currency system + nbttagcompound.setBoolean("UseCurrency", useCurrency); + NBTTagList priceList = new NBTTagList(); + for (long price : slotPrices) { + NBTTagCompound priceTag = new NBTTagCompound(); + priceTag.setLong("Price", price); + priceList.appendTag(priceTag); + } + nbttagcompound.setTag("SlotPrices", priceList); + return nbttagcompound; } @@ -94,10 +117,39 @@ public class RoleTrader extends RoleInterface { disableSlot = new int[18]; for (int i = 0; i < disableSlot.length; ++i) disableSlot[i] = 0; } + + // Stock system + if (nbttagcompound.hasKey("Stock")) { + stock.readFromNBT(nbttagcompound.getCompoundTag("Stock")); + } + + // Currency system + useCurrency = nbttagcompound.getBoolean("UseCurrency"); + if (nbttagcompound.hasKey("SlotPrices")) { + NBTTagList priceList = nbttagcompound.getTagList("SlotPrices", 10); + for (int i = 0; i < priceList.tagCount() && i < 18; i++) { + slotPrices[i] = priceList.getCompoundTagAt(i).getLong("Price"); + } + } } @Override public void interact(EntityPlayer player) { + // Check for stock reset before opening GUI + if (stock.enableStock) { + long currentTime = stock.resetType.isRealTime() + ? System.currentTimeMillis() + : player.worldObj.getTotalWorldTime(); + + if (stock.shouldReset(currentTime)) { + stock.resetStock(currentTime); + // Save to market if using shared market + if (!marketName.isEmpty()) { + Market.save(this, marketName); + } + } + } + npc.say(player, npc.advanced.getInteractLine()); try { Market.getMarket(this, marketName); @@ -138,4 +190,78 @@ public class RoleTrader extends RoleInterface { return map.get(name); } + // ==================== Stock System Methods ==================== + + /** + * Check if there is stock available for a slot + */ + public boolean hasStock(int slot, String playerName, int amount) { + if (!stock.enableStock) { + return true; // Stock system disabled, always available + } + return stock.hasStock(slot, playerName, amount); + } + + /** + * Get available stock for a slot + */ + public int getAvailableStock(int slot, String playerName) { + if (!stock.enableStock) { + return Integer.MAX_VALUE; // Unlimited + } + return stock.getAvailableStock(slot, playerName); + } + + /** + * Consume stock after purchase + */ + public boolean consumeStock(int slot, String playerName, int amount) { + if (!stock.enableStock) { + return true; // Stock system disabled + } + boolean consumed = stock.consumeStock(slot, playerName, amount); + if (consumed && !marketName.isEmpty()) { + Market.save(this, marketName); + } + return consumed; + } + + /** + * Get time until stock reset formatted for display + */ + public String getTimeUntilResetFormatted() { + if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) { + return ""; + } + long currentTime = stock.resetType.isRealTime() + ? System.currentTimeMillis() + : (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0); + return stock.getTimeUntilResetFormatted(currentTime); + } + + // ==================== Currency System Methods ==================== + + /** + * Get the currency price for a slot + */ + public long getSlotPrice(int slot) { + if (slot < 0 || slot >= 18) return 0; + return slotPrices[slot]; + } + + /** + * Set the currency price for a slot + */ + public void setSlotPrice(int slot, long price) { + if (slot >= 0 && slot < 18) { + slotPrices[slot] = Math.max(0, price); + } + } + + /** + * Check if a slot has a currency price set + */ + public boolean hasSlotPrice(int slot) { + return useCurrency && slot >= 0 && slot < 18 && slotPrices[slot] > 0; + } }