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
This commit is contained in:
Claude committed 2026-01-04 08:29:03 +00:00
1 parent 60b985d33d
commit c9f77bed05
4 files changed
+535 -5

No files matched your search

@@ -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";
}
}
}
@@ -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) {
@@ -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<String, PlayerTraderStock> 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<String, PlayerTraderStock> 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");
}
}
}
@@ -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<String, int[]> playerPurchases;
public HashMap<String, int[]> 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;
}
}