diff --git a/build.gradle b/build.gradle index dc3a7dd3b..b76ad9d0c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id 'idea' } -version = "1.10.2" +version = "1.10.3" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" @@ -31,12 +31,14 @@ tasks.create('buildNoMixin', Exec) { commandLine gradleWrapper, 'build', '-Pnomixin' } -sourceSets { - named("main").configure { - java { - srcDirs += ['src/api/java'] - } +sourceSets.main.java { + def apiDir = file('src/api/java') + + if (!apiDir.exists()) { + throw new GradleException("Missing API sources: Run './gradlew updateAPI' or enable checkout submodules in CI.") } + + srcDirs += apiDir } // Modify the existing 'build' task to depend on 'updateAPI' diff --git a/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md new file mode 100644 index 000000000..fb1c33158 --- /dev/null +++ b/docs/CNPC_1.11_MARKET_UPDATE_PLAN.md @@ -0,0 +1,1871 @@ +# CustomNPC+ 1.11 Market Update - Implementation Plan + +## Overview + +This document outlines the implementation plan for the CustomNPC+ 1.11 update, focusing on three major features: + +1. **Auction House** - A new NPC role allowing players to access a global auction system +2. **Currency System** - Built-in currency integration with VaultAPI for Bukkit hybrid servers +3. **Trader Enhancements** - Stock/Inventory system with per-player/per-server options and reset timers + +--- + +## Table of Contents + +1. [Architecture Overview](#architecture-overview) +2. [Feature 1: Currency System](#feature-1-currency-system) +3. [Feature 2: Trader Enhancements](#feature-2-trader-enhancements) +4. [Feature 3: Auction House](#feature-3-auction-house) +5. [Implementation Order](#implementation-order) +6. [File Summary](#file-summary) +7. [Testing Strategy](#testing-strategy) + +--- + +## Architecture Overview + +### Current Codebase Patterns + +The implementation will follow existing patterns found in the codebase: + +| Pattern | Used By | Will Be Applied To | +|---------|---------|-------------------| +| Singleton Controller | `BankController`, `QuestController` | `AuctionController`, `CurrencyController` | +| Role Interface | `RoleTrader`, `RoleBank` | `RoleAuctioneer` | +| Player Data Extension | `PlayerBankData`, `PlayerQuestData` | `PlayerAuctionData`, `PlayerCurrencyData` | +| Cooldown System | `Quest.repeat`, `JobItemGiver` | Trader Stock Reset | +| Optional Addon | `PixelmonHelper`, `DBCAddon` | `VaultHelper` | +| NBT Serialization | All data classes | All new data classes | +| Cache System | `Market.java`, `CacheHashMap` | Auction listings cache | + +### Package Structure for New Features + +``` +noppes/npcs/ +├── controllers/ +│ ├── AuctionController.java # Manages global auction house +│ ├── CurrencyController.java # Manages currency settings +│ └── data/ +│ ├── AuctionListing.java # Single auction listing +│ ├── AuctionData.java # World auction data +│ ├── PlayerAuctionData.java # Per-player auction history +│ ├── PlayerCurrencyData.java # Per-player currency balance +│ └── TraderStock.java # Trader stock configuration +├── roles/ +│ └── RoleAuctioneer.java # Auctioneer NPC role +├── containers/ +│ ├── ContainerAuction*.java # Auction GUI containers +│ └── ContainerNPCTraderStock.java # Stock management container +├── client/gui/ +│ ├── player/ +│ │ └── GuiAuction*.java # Player-facing auction GUIs +│ └── roles/ +│ └── GuiNpcAuctioneerSetup.java # Auctioneer configuration GUI + +kamkeel/npcs/ +├── controllers/ +│ └── CurrencyController.java # Currency management +├── network/packets/ +│ ├── data/ +│ │ ├── AuctionSyncPacket.java # Sync auction data +│ │ └── CurrencySyncPacket.java # Sync currency data +│ └── player/ +│ ├── AuctionActionPacket.java # Auction actions (bid, buy, sell) +│ └── CurrencyActionPacket.java # Currency transactions +└── compat/ + └── VaultHelper.java # VaultAPI integration +``` + +--- + +## Feature 1: Currency System + +### 1.1 Overview + +The currency system provides a unified economy layer that can: +- Use VaultAPI when available on Bukkit hybrid servers (Cauldron, Thermos, Mohist) +- Fall back to an internal item-based currency when VaultAPI is unavailable +- Be configured per-world or globally + +### 1.2 VaultAPI Integration + +#### 1.2.1 Dependency Setup + +**File: `dependencies.gradle`** +```gradle +// VaultAPI - compile-only for optional integration +compileOnly("net.milkbowl.vault:VaultAPI:1.4") { + transitive = false +} +``` + +**File: `repositories.gradle`** +```gradle +maven { + name = "vault-repo" + url = "https://nexus.hc.to/content/repositories/pub_releases/" +} +``` + +> **Note:** VaultAPI 1.4.1 works with 1.7.10 Bukkit hybrid servers. The dependency is compile-only so the mod works without Vault present. + +#### 1.2.2 VaultHelper Class + +**File: `src/main/java/kamkeel/npcs/compat/VaultHelper.java`** + +```java +package kamkeel.npcs.compat; + +import cpw.mods.fml.common.Loader; +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.LogWriter; +import org.bukkit.Bukkit; +import org.bukkit.plugin.RegisteredServiceProvider; + +public class VaultHelper { + public static boolean Enabled = false; + public static boolean Available = false; + + // Cached via reflection + private static Object economy = null; + private static Class economyClass = null; + private static java.lang.reflect.Method getBalance; + private static java.lang.reflect.Method withdrawPlayer; + private static java.lang.reflect.Method depositPlayer; + private static java.lang.reflect.Method hasMethod; + private static java.lang.reflect.Method format; + + public static void load() { + // Check if we're on a Bukkit hybrid server + try { + Class.forName("org.bukkit.Bukkit"); + Available = true; + } catch (ClassNotFoundException e) { + Available = false; + return; + } + + // Try to hook into Vault + try { + Class vaultClass = Class.forName("net.milkbowl.vault.economy.Economy"); + economyClass = vaultClass; + + RegisteredServiceProvider rsp = Bukkit.getServicesManager() + .getRegistration(vaultClass); + + if (rsp != null) { + economy = rsp.getProvider(); + + // Cache reflection methods + getBalance = economyClass.getMethod("getBalance", String.class); + withdrawPlayer = economyClass.getMethod("withdrawPlayer", + String.class, double.class); + depositPlayer = economyClass.getMethod("depositPlayer", + String.class, double.class); + hasMethod = economyClass.getMethod("has", String.class, double.class); + format = economyClass.getMethod("format", double.class); + + Enabled = true; + LogWriter.info("VaultAPI Economy hooked successfully!"); + } + } catch (Exception e) { + LogWriter.info("VaultAPI not available: " + e.getMessage()); + Enabled = false; + } + } + + // Economy Methods + public static double getBalance(EntityPlayer player) { + if (!Enabled) return 0.0; + try { + return (Double) getBalance.invoke(economy, player.getCommandSenderName()); + } catch (Exception e) { + LogWriter.except(e); + return 0.0; + } + } + + public static boolean withdraw(EntityPlayer player, double amount) { + if (!Enabled) return false; + try { + Object response = withdrawPlayer.invoke(economy, + player.getCommandSenderName(), amount); + return (Boolean) response.getClass() + .getMethod("transactionSuccess").invoke(response); + } catch (Exception e) { + LogWriter.except(e); + return false; + } + } + + public static boolean deposit(EntityPlayer player, double amount) { + if (!Enabled) return false; + try { + Object response = depositPlayer.invoke(economy, + player.getCommandSenderName(), amount); + return (Boolean) response.getClass() + .getMethod("transactionSuccess").invoke(response); + } catch (Exception e) { + LogWriter.except(e); + return false; + } + } + + public static boolean has(EntityPlayer player, double amount) { + if (!Enabled) return false; + try { + return (Boolean) hasMethod.invoke(economy, + player.getCommandSenderName(), amount); + } catch (Exception e) { + LogWriter.except(e); + return false; + } + } + + public static String formatCurrency(double amount) { + if (!Enabled) return String.format("%.2f", amount); + try { + return (String) format.invoke(economy, amount); + } catch (Exception e) { + return String.format("%.2f", amount); + } + } +} +``` + +#### 1.2.3 Initialization Hook + +**File: `src/main/java/noppes/npcs/CustomNpcs.java`** + +Add to `load()` method after AddonManager: +```java +// Initialize VaultAPI integration (for Bukkit hybrid servers) +VaultHelper.load(); +``` + +### 1.3 Currency Controller + +**File: `src/main/java/kamkeel/npcs/controllers/CurrencyController.java`** + +```java +package kamkeel.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import kamkeel.npcs.compat.VaultHelper; +import noppes.npcs.NoppesUtilPlayer; + +public class CurrencyController { + private static CurrencyController instance; + + // Configuration + public boolean useVaultIfAvailable = true; + public ItemStack fallbackCurrency = null; // Item to use when Vault unavailable + public double itemToVaultRatio = 1.0; // 1 item = X vault currency + + public static CurrencyController getInstance() { + if (instance == null) { + instance = new CurrencyController(); + } + return instance; + } + + /** + * Check if Vault economy is being used + */ + public boolean isUsingVault() { + return useVaultIfAvailable && VaultHelper.Enabled; + } + + /** + * Get player's balance (Vault or item count) + */ + public double getBalance(EntityPlayer player) { + if (isUsingVault()) { + return VaultHelper.getBalance(player); + } + // Fall back to counting items + if (fallbackCurrency != null) { + return NoppesUtilPlayer.countItems(player, fallbackCurrency, false, false); + } + return 0.0; + } + + /** + * Check if player can afford amount + */ + public boolean canAfford(EntityPlayer player, double amount) { + if (isUsingVault()) { + return VaultHelper.has(player, amount); + } + return getBalance(player) >= amount; + } + + /** + * Withdraw currency from player + */ + public boolean withdraw(EntityPlayer player, double amount) { + if (isUsingVault()) { + return VaultHelper.withdraw(player, amount); + } + // Remove items + if (fallbackCurrency != null) { + ItemStack toRemove = fallbackCurrency.copy(); + toRemove.stackSize = (int) Math.ceil(amount / itemToVaultRatio); + return NoppesUtilPlayer.consumeItem(player, toRemove, false, false); + } + return false; + } + + /** + * Deposit currency to player + */ + public boolean deposit(EntityPlayer player, double amount) { + if (isUsingVault()) { + return VaultHelper.deposit(player, amount); + } + // Give items + if (fallbackCurrency != null) { + ItemStack toGive = fallbackCurrency.copy(); + toGive.stackSize = (int) Math.floor(amount / itemToVaultRatio); + return player.inventory.addItemStackToInventory(toGive); + } + return false; + } + + /** + * Format currency for display + */ + public String format(double amount) { + if (isUsingVault()) { + return VaultHelper.formatCurrency(amount); + } + return String.format("%.0f", amount); + } + + // NBT Persistence + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("UseVault", useVaultIfAvailable); + compound.setDouble("ItemRatio", itemToVaultRatio); + if (fallbackCurrency != null) { + compound.setTag("FallbackItem", fallbackCurrency.writeToNBT(new NBTTagCompound())); + } + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + useVaultIfAvailable = compound.getBoolean("UseVault"); + itemToVaultRatio = compound.getDouble("ItemRatio"); + if (compound.hasKey("FallbackItem")) { + fallbackCurrency = ItemStack.loadItemStackFromNBT( + compound.getCompoundTag("FallbackItem")); + } + } +} +``` + +### 1.4 Player Currency Data + +**File: `src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java`** + +```java +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import java.util.HashMap; + +public class PlayerCurrencyData { + private final PlayerData parent; + + // Transaction history (for auditing/display) + public HashMap transactionHistory = new HashMap<>(); + public double totalEarned = 0.0; + public double totalSpent = 0.0; + + public PlayerCurrencyData(PlayerData parent) { + this.parent = parent; + } + + public void recordTransaction(double amount, boolean isEarning) { + long timestamp = System.currentTimeMillis(); + transactionHistory.put(timestamp, isEarning ? amount : -amount); + if (isEarning) { + totalEarned += amount; + } else { + totalSpent += amount; + } + // Limit history size + if (transactionHistory.size() > 100) { + // Remove oldest entries + Long oldest = transactionHistory.keySet().stream() + .min(Long::compare).orElse(null); + if (oldest != null) { + transactionHistory.remove(oldest); + } + } + } + + public void loadNBTData(NBTTagCompound compound) { + if (compound.hasKey("CurrencyData")) { + NBTTagCompound data = compound.getCompoundTag("CurrencyData"); + totalEarned = data.getDouble("TotalEarned"); + totalSpent = data.getDouble("TotalSpent"); + } + } + + public void saveNBTData(NBTTagCompound compound) { + NBTTagCompound data = new NBTTagCompound(); + data.setDouble("TotalEarned", totalEarned); + data.setDouble("TotalSpent", totalSpent); + compound.setTag("CurrencyData", data); + } +} +``` + +### 1.5 Configuration UI + +Add currency configuration to the global settings GUI: + +**File: `src/main/java/noppes/npcs/client/gui/global/GuiNPCManageCurrency.java`** + +This GUI will allow server operators to: +- Enable/disable Vault integration +- Set fallback currency item +- Configure item-to-Vault ratio +- View server economy statistics + +--- + +## Feature 2: Trader Enhancements + +### 2.1 Overview + +Enhance the existing `RoleTrader` with: +- **Stock System**: Limited inventory that depletes when players buy +- **Per-Player vs Per-Server Stock**: Configure whether stock is shared or individual +- **Stock Reset Timer**: Automatic restocking based on configurable cooldown (like quest cooldowns) +- **Currency Integration**: Use Vault currency alongside or instead of item-based trading + +### 2.2 Trader Stock Configuration + +#### 2.2.1 Stock Data Class + +**File: `src/main/java/noppes/npcs/controllers/data/TraderStock.java`** + +```java +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import java.util.HashMap; + +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 + + // Stock quantities per slot (max stock) + 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 + */ + public int getAvailableStock(int slot, String playerName) { + if (!enableStock || 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 currentStock[slot]; + } + + /** + * Consume stock when purchase is made + */ + public boolean consumeStock(int slot, String playerName, int amount) { + if (!enableStock || maxStock[slot] < 0) { + return true; // Unlimited + } + + 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 + */ + 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 + */ + public void resetStock(long currentTime) { + lastResetTime = currentTime; + + for (int i = 0; i < 18; i++) { + currentStock[i] = maxStock[i]; + } + + // Clear per-player stock + playerStock.clear(); + } + + /** + * Get reset interval in milliseconds + */ + public long getResetInterval() { + switch (resetType) { + case MCDAILY: + return 24000 * 50; // 24000 ticks * 50ms + case MCWEEKLY: + return 168000 * 50; + case MCCUSTOM: + return customResetTime * 50; + case RLDAILY: + return 86400000L; // 24 hours in ms + case RLWEEKLY: + return 604800000L; // 7 days in ms + case RLCUSTOM: + return customResetTime; + default: + return Long.MAX_VALUE; + } + } + + /** + * Get time until next reset (for display) + */ + public long getTimeUntilReset(long currentTime) { + if (resetType == EnumStockReset.NONE) { + return -1; + } + long elapsed = currentTime - lastResetTime; + long interval = getResetInterval(); + return Math.max(0, interval - elapsed); + } + + // 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"); + resetType = EnumStockReset.values()[compound.getInteger("ResetType")]; + customResetTime = compound.getLong("CustomResetTime"); + maxStock = compound.getIntArray("MaxStock"); + currentStock = compound.getIntArray("CurrentStock"); + lastResetTime = compound.getLong("LastResetTime"); + + // Load per-player stock + 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 tracking +class PlayerTraderStock { + public int[] purchasedAmounts = new int[18]; + public long lastPurchaseTime = 0; + + public int getStock(int slot, int maxStock) { + return Math.max(0, maxStock - purchasedAmounts[slot]); + } + + public boolean consumeStock(int slot, int maxStock, int amount) { + 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) { + purchasedAmounts = compound.getIntArray("Purchased"); + lastPurchaseTime = compound.getLong("LastPurchase"); + } +} +``` + +#### 2.2.2 Stock Reset Enum + +**File: `src/main/java/noppes/npcs/constants/EnumStockReset.java`** + +```java +package noppes.npcs.constants; + +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 +} +``` + +### 2.3 RoleTrader Modifications + +**File: `src/main/java/noppes/npcs/roles/RoleTrader.java`** + +Add the following to `RoleTrader`: + +```java +// Add new fields +public TraderStock stock = new TraderStock(); +public boolean useCurrency = false; // Use Vault/Currency system +public double[] slotPrices = new double[18]; // Currency prices per slot + +// Modify interact() to check stock reset +@Override +public void interact(EntityPlayer player) { + // Check for stock reset before opening GUI + if (stock.enableStock) { + long currentTime = stock.resetType.name().startsWith("RL") + ? 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()); + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerTrader, npc); +} + +// Modify writeToNBT +@Override +public NBTTagCompound writeToNBT(NBTTagCompound nbttagcompound) { + // ... existing code ... + + // Add stock data + nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound())); + nbttagcompound.setBoolean("UseCurrency", useCurrency); + + // Save currency prices + NBTTagList priceList = new NBTTagList(); + for (double price : slotPrices) { + NBTTagCompound priceTag = new NBTTagCompound(); + priceTag.setDouble("Price", price); + priceList.appendTag(priceTag); + } + nbttagcompound.setTag("SlotPrices", priceList); + + return nbttagcompound; +} + +// Modify readFromNBT +@Override +public void readFromNBT(NBTTagCompound nbttagcompound) { + // ... existing code ... + + // Load stock data + if (nbttagcompound.hasKey("Stock")) { + stock.readFromNBT(nbttagcompound.getCompoundTag("Stock")); + } + useCurrency = nbttagcompound.getBoolean("UseCurrency"); + + // Load currency prices + 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).getDouble("Price"); + } + } +} +``` + +### 2.4 ContainerNPCTrader Modifications + +**File: `src/main/java/noppes/npcs/containers/ContainerNPCTrader.java`** + +Modify `slotClick()` to handle stock and currency: + +```java +public ItemStack slotClick(int slotIndex, EntityPlayer player) { + RoleTrader role = (RoleTrader) npc.roleInterface; + String playerName = player.getCommandSenderName(); + + // ... existing validation ... + + // Check stock availability + if (role.stock.enableStock) { + int available = role.stock.getAvailableStock(slotIndex, playerName); + if (available <= 0) { + // Out of stock - send message to player + return null; + } + } + + // Handle currency-based purchase + if (role.useCurrency && role.slotPrices[slotIndex] > 0) { + double price = role.slotPrices[slotIndex]; + CurrencyController currency = CurrencyController.getInstance(); + + if (!currency.canAfford(player, price)) { + // Can't afford - send message + return null; + } + + // Withdraw currency + if (!currency.withdraw(player, price)) { + return null; + } + } else { + // ... existing item-based currency logic ... + } + + // Consume stock + if (role.stock.enableStock) { + role.stock.consumeStock(slotIndex, playerName, 1); + } + + // ... existing item delivery logic ... + + // Save if using market + if (!role.marketName.isEmpty()) { + Market.save(role, role.marketName); + } + + return soldItem; +} +``` + +### 2.5 Trader Setup GUI Additions + +**File: `src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java`** + +Add new configuration options: + +```java +// Add buttons/fields for: +// - "Enable Stock" toggle +// - "Per Player" / "Per Server" radio buttons +// - "Reset Type" dropdown (None, MC Daily, MC Weekly, etc.) +// - "Custom Reset Time" field +// - "Use Currency" toggle +// - Per-slot stock quantity inputs +// - Per-slot currency price inputs + +private void initStockGui() { + addButton(new GuiNpcButton(20, x, y, 80, 20, + "Enable Stock: " + (role.stock.enableStock ? "Yes" : "No"))); + + addButton(new GuiNpcButton(21, x, y + 22, 80, 20, + role.stock.perPlayer ? "Per Player" : "Per Server")); + + String[] resetTypes = {"None", "MC Daily", "MC Weekly", "MC Custom", + "RL Daily", "RL Weekly", "RL Custom"}; + addButton(new GuiNpcButton(22, x, y + 44, 80, 20, resetTypes, + role.stock.resetType.ordinal())); + + addButton(new GuiNpcButton(23, x, y + 66, 80, 20, + "Use Currency: " + (role.useCurrency ? "Yes" : "No"))); +} +``` + +--- + +## Feature 3: Auction House + +### 3.1 Overview + +The Auction House is a global trading system where players can: +- **List items for sale** at fixed prices or as auctions with bidding +- **Browse listings** by category, search, and filter +- **Bid on auctions** with automatic outbid notifications +- **Buy instantly** for buy-it-now prices +- **Manage their listings** (cancel, modify) +- **Collect proceeds and expired items** via mailbox or direct claim + +### 3.2 Auction Data Structures + +#### 3.2.1 AuctionListing + +**File: `src/main/java/noppes/npcs/controllers/data/AuctionListing.java`** + +```java +package noppes.npcs.controllers.data; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import java.util.UUID; + +public class AuctionListing { + public int id; + public UUID sellerUUID; + public String sellerName; + public ItemStack item; + + // Pricing + public double startingPrice; + public double buyoutPrice; // 0 = no buyout + public double currentBid; + public UUID highBidderUUID; + public String highBidderName; + + // Timing + public long listingTime; + public long expirationTime; + public EnumAuctionDuration duration; + + // Status + public EnumAuctionStatus status = EnumAuctionStatus.ACTIVE; + + // Category + public EnumAuctionCategory category = EnumAuctionCategory.MISC; + + public AuctionListing() {} + + public AuctionListing(UUID seller, String sellerName, ItemStack item, + double startPrice, double buyout, EnumAuctionDuration duration) { + this.sellerUUID = seller; + this.sellerName = sellerName; + this.item = item.copy(); + this.startingPrice = startPrice; + this.buyoutPrice = buyout; + this.currentBid = startPrice; + this.duration = duration; + this.listingTime = System.currentTimeMillis(); + this.expirationTime = listingTime + duration.getMillis(); + this.category = categorizeItem(item); + } + + /** + * Check if auction has expired + */ + public boolean isExpired() { + return System.currentTimeMillis() >= expirationTime; + } + + /** + * Get time remaining in milliseconds + */ + public long getTimeRemaining() { + return Math.max(0, expirationTime - System.currentTimeMillis()); + } + + /** + * Place a bid + */ + public boolean placeBid(UUID bidder, String bidderName, double amount) { + if (status != EnumAuctionStatus.ACTIVE) return false; + if (amount <= currentBid) return false; + if (bidder.equals(sellerUUID)) return false; // Can't bid on own item + + // Minimum bid increment (5% or 1, whichever is greater) + double minIncrement = Math.max(1, currentBid * 0.05); + if (amount < currentBid + minIncrement) return false; + + highBidderUUID = bidder; + highBidderName = bidderName; + currentBid = amount; + + return true; + } + + /** + * Execute buyout + */ + public boolean buyout(UUID buyer, String buyerName) { + if (status != EnumAuctionStatus.ACTIVE) return false; + if (buyoutPrice <= 0) return false; // No buyout available + if (buyer.equals(sellerUUID)) return false; + + highBidderUUID = buyer; + highBidderName = buyerName; + currentBid = buyoutPrice; + status = EnumAuctionStatus.SOLD; + + return true; + } + + /** + * Cancel listing (seller only, before any bids) + */ + public boolean cancel(UUID requester) { + if (!requester.equals(sellerUUID)) return false; + if (highBidderUUID != null) return false; // Has bids, can't cancel + + status = EnumAuctionStatus.CANCELLED; + return true; + } + + /** + * Auto-categorize item based on type + */ + private EnumAuctionCategory categorizeItem(ItemStack item) { + // Implement item categorization logic + // Check for weapons, armor, tools, food, blocks, etc. + return EnumAuctionCategory.MISC; + } + + // NBT Serialization + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setInteger("Id", id); + compound.setString("SellerUUID", sellerUUID.toString()); + compound.setString("SellerName", sellerName); + compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); + compound.setDouble("StartPrice", startingPrice); + compound.setDouble("BuyoutPrice", buyoutPrice); + compound.setDouble("CurrentBid", currentBid); + if (highBidderUUID != null) { + compound.setString("BidderUUID", highBidderUUID.toString()); + compound.setString("BidderName", highBidderName); + } + compound.setLong("ListingTime", listingTime); + compound.setLong("ExpirationTime", expirationTime); + compound.setInteger("Duration", duration.ordinal()); + compound.setInteger("Status", status.ordinal()); + compound.setInteger("Category", category.ordinal()); + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + id = compound.getInteger("Id"); + sellerUUID = UUID.fromString(compound.getString("SellerUUID")); + sellerName = compound.getString("SellerName"); + item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + startingPrice = compound.getDouble("StartPrice"); + buyoutPrice = compound.getDouble("BuyoutPrice"); + currentBid = compound.getDouble("CurrentBid"); + if (compound.hasKey("BidderUUID")) { + highBidderUUID = UUID.fromString(compound.getString("BidderUUID")); + highBidderName = compound.getString("BidderName"); + } + listingTime = compound.getLong("ListingTime"); + expirationTime = compound.getLong("ExpirationTime"); + duration = EnumAuctionDuration.values()[compound.getInteger("Duration")]; + status = EnumAuctionStatus.values()[compound.getInteger("Status")]; + category = EnumAuctionCategory.values()[compound.getInteger("Category")]; + } +} +``` + +#### 3.2.2 Supporting Enums + +**File: `src/main/java/noppes/npcs/constants/EnumAuctionDuration.java`** + +```java +package noppes.npcs.constants; + +public enum EnumAuctionDuration { + SHORT(2 * 60 * 60 * 1000L), // 2 hours + MEDIUM(12 * 60 * 60 * 1000L), // 12 hours + LONG(24 * 60 * 60 * 1000L), // 24 hours + VERY_LONG(48 * 60 * 60 * 1000L); // 48 hours + + private final long millis; + + EnumAuctionDuration(long millis) { + this.millis = millis; + } + + public long getMillis() { + return millis; + } + + public String getDisplayName() { + switch (this) { + case SHORT: return "2 Hours"; + case MEDIUM: return "12 Hours"; + case LONG: return "24 Hours"; + case VERY_LONG: return "48 Hours"; + default: return "Unknown"; + } + } +} +``` + +**File: `src/main/java/noppes/npcs/constants/EnumAuctionStatus.java`** + +```java +package noppes.npcs.constants; + +public enum EnumAuctionStatus { + ACTIVE, // Currently listed + SOLD, // Successfully sold (buyout or auction end with bids) + EXPIRED, // Ended without bids + CANCELLED, // Cancelled by seller + CLAIMED // Item/money has been claimed +} +``` + +**File: `src/main/java/noppes/npcs/constants/EnumAuctionCategory.java`** + +```java +package noppes.npcs.constants; + +public enum EnumAuctionCategory { + WEAPONS, + ARMOR, + TOOLS, + FOOD, + POTIONS, + BLOCKS, + MATERIALS, + ENCHANTED, + MISC +} +``` + +### 3.3 Auction Controller + +**File: `src/main/java/noppes/npcs/controllers/AuctionController.java`** + +```java +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.constants.*; + +import java.io.File; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class AuctionController { + private static AuctionController instance; + + // All listings (thread-safe for async operations) + public ConcurrentHashMap listings = new ConcurrentHashMap<>(); + + // Index by seller for quick lookup + private Map> sellerIndex = new ConcurrentHashMap<>(); + + // Pending claims (seller -> items/money to claim) + public Map> pendingClaims = new ConcurrentHashMap<>(); + + private int nextId = 1; + private long lastSaveTime = 0; + private static final long SAVE_INTERVAL = 60000; // Save every minute + + // Configuration + public double listingFee = 0.0; // Fee to list item + public double saleTax = 0.05; // 5% tax on sales + public int maxListingsPerPlayer = 20; + public boolean requireAuctioneer = true; // Require Auctioneer NPC to access + + public static AuctionController getInstance() { + if (instance == null) { + instance = new AuctionController(); + instance.load(); + } + return instance; + } + + /** + * Create a new auction listing + */ + public AuctionListing createListing(EntityPlayer seller, ItemStack item, + double startPrice, double buyout, + EnumAuctionDuration duration) { + UUID sellerUUID = seller.getUniqueID(); + + // Check max listings + Set sellerListings = sellerIndex.get(sellerUUID); + if (sellerListings != null && sellerListings.size() >= maxListingsPerPlayer) { + return null; + } + + // Charge listing fee + if (listingFee > 0) { + CurrencyController currency = CurrencyController.getInstance(); + if (!currency.withdraw(seller, listingFee)) { + return null; + } + } + + // Create listing + AuctionListing listing = new AuctionListing( + sellerUUID, seller.getCommandSenderName(), + item, startPrice, buyout, duration + ); + listing.id = nextId++; + + // Add to indexes + listings.put(listing.id, listing); + sellerIndex.computeIfAbsent(sellerUUID, k -> new HashSet<>()).add(listing.id); + + // Remove item from seller inventory + item.stackSize = 0; + + scheduleSave(); + return listing; + } + + /** + * Place a bid on an auction + */ + public boolean placeBid(EntityPlayer bidder, int listingId, double amount) { + AuctionListing listing = listings.get(listingId); + if (listing == null || listing.isExpired()) return false; + + CurrencyController currency = CurrencyController.getInstance(); + + // Check if bidder can afford + if (!currency.canAfford(bidder, amount)) return false; + + // Refund previous bidder + if (listing.highBidderUUID != null) { + addClaim(listing.highBidderUUID, listing.highBidderName, + null, listing.currentBid, "Outbid refund for: " + + listing.item.getDisplayName()); + } + + // Place bid + if (!listing.placeBid(bidder.getUniqueID(), + bidder.getCommandSenderName(), amount)) { + return false; + } + + // Hold currency from bidder + currency.withdraw(bidder, amount); + + scheduleSave(); + return true; + } + + /** + * Buyout an auction + */ + public boolean buyout(EntityPlayer buyer, int listingId) { + AuctionListing listing = listings.get(listingId); + if (listing == null || listing.buyoutPrice <= 0) return false; + + CurrencyController currency = CurrencyController.getInstance(); + + // Check if buyer can afford + if (!currency.canAfford(buyer, listing.buyoutPrice)) return false; + + // Refund previous bidder if any + if (listing.highBidderUUID != null) { + addClaim(listing.highBidderUUID, listing.highBidderName, + null, listing.currentBid, "Buyout refund for: " + + listing.item.getDisplayName()); + } + + // Execute buyout + if (!listing.buyout(buyer.getUniqueID(), buyer.getCommandSenderName())) { + return false; + } + + // Withdraw from buyer + currency.withdraw(buyer, listing.buyoutPrice); + + // Calculate seller proceeds (minus tax) + double proceeds = listing.buyoutPrice * (1 - saleTax); + + // Add claims for both parties + addClaim(listing.sellerUUID, listing.sellerName, null, proceeds, + "Sale of: " + listing.item.getDisplayName()); + addClaim(buyer.getUniqueID(), buyer.getCommandSenderName(), + listing.item, 0, "Purchased: " + listing.item.getDisplayName()); + + scheduleSave(); + return true; + } + + /** + * Cancel a listing (seller only) + */ + public boolean cancelListing(EntityPlayer player, int listingId) { + AuctionListing listing = listings.get(listingId); + if (listing == null) return false; + + if (!listing.cancel(player.getUniqueID())) return false; + + // Return item to seller + addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, + "Cancelled listing: " + listing.item.getDisplayName()); + + scheduleSave(); + return true; + } + + /** + * Process expired auctions (called periodically) + */ + public void processExpiredAuctions() { + long now = System.currentTimeMillis(); + + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; + if (!listing.isExpired()) continue; + + if (listing.highBidderUUID != null) { + // Auction successful - has bids + listing.status = EnumAuctionStatus.SOLD; + + double proceeds = listing.currentBid * (1 - saleTax); + addClaim(listing.sellerUUID, listing.sellerName, null, proceeds, + "Auction sold: " + listing.item.getDisplayName()); + addClaim(listing.highBidderUUID, listing.highBidderName, + listing.item, 0, "Won auction: " + listing.item.getDisplayName()); + } else { + // No bids - return to seller + listing.status = EnumAuctionStatus.EXPIRED; + addClaim(listing.sellerUUID, listing.sellerName, listing.item, 0, + "Expired listing: " + listing.item.getDisplayName()); + } + } + + scheduleSave(); + } + + /** + * Get active listings with optional filters + */ + public List getActiveListings(EnumAuctionCategory category, + String search, + int page, int pageSize) { + return listings.values().stream() + .filter(l -> l.status == EnumAuctionStatus.ACTIVE) + .filter(l -> !l.isExpired()) + .filter(l -> category == null || l.category == category) + .filter(l -> search == null || search.isEmpty() || + l.item.getDisplayName().toLowerCase() + .contains(search.toLowerCase())) + .sorted(Comparator.comparingLong(l -> l.expirationTime)) + .skip((long) page * pageSize) + .limit(pageSize) + .collect(Collectors.toList()); + } + + /** + * Get player's own listings + */ + public List getPlayerListings(UUID playerUUID) { + Set ids = sellerIndex.get(playerUUID); + if (ids == null) return Collections.emptyList(); + + return ids.stream() + .map(listings::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * Get pending claims for a player + */ + public List getPendingClaims(UUID playerUUID) { + return pendingClaims.getOrDefault(playerUUID, Collections.emptyList()); + } + + /** + * Claim pending items/money + */ + public boolean claim(EntityPlayer player, int claimIndex) { + List claims = pendingClaims.get(player.getUniqueID()); + if (claims == null || claimIndex >= claims.size()) return false; + + AuctionClaim claim = claims.get(claimIndex); + + // Give item or money + if (claim.item != null) { + if (!player.inventory.addItemStackToInventory(claim.item.copy())) { + return false; // Inventory full + } + } + if (claim.money > 0) { + CurrencyController.getInstance().deposit(player, claim.money); + } + + claims.remove(claimIndex); + scheduleSave(); + return true; + } + + private void addClaim(UUID playerUUID, String playerName, + ItemStack item, double money, String description) { + AuctionClaim claim = new AuctionClaim(item, money, description); + pendingClaims.computeIfAbsent(playerUUID, k -> new ArrayList<>()).add(claim); + } + + // Persistence + private void scheduleSave() { + long now = System.currentTimeMillis(); + if (now - lastSaveTime > SAVE_INTERVAL) { + save(); + lastSaveTime = now; + } + } + + public void save() { + try { + NBTTagCompound compound = new NBTTagCompound(); + compound.setInteger("NextId", nextId); + compound.setDouble("ListingFee", listingFee); + compound.setDouble("SaleTax", saleTax); + compound.setInteger("MaxListings", maxListingsPerPlayer); + + // Save listings + NBTTagList listingList = new NBTTagList(); + for (AuctionListing listing : listings.values()) { + listingList.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + compound.setTag("Listings", listingList); + + // Save claims + NBTTagList claimList = new NBTTagList(); + for (Map.Entry> entry : pendingClaims.entrySet()) { + NBTTagCompound playerClaims = new NBTTagCompound(); + playerClaims.setString("UUID", entry.getKey().toString()); + NBTTagList claims = new NBTTagList(); + for (AuctionClaim claim : entry.getValue()) { + claims.appendTag(claim.writeToNBT(new NBTTagCompound())); + } + playerClaims.setTag("Claims", claims); + claimList.appendTag(playerClaims); + } + compound.setTag("PendingClaims", claimList); + + File file = new File(CustomNpcs.getWorldSaveDirectory(), "auction.dat"); + CompressedStreamTools.writeCompressed(compound, + new java.io.FileOutputStream(file)); + + } catch (Exception e) { + LogWriter.except(e); + } + } + + public void load() { + try { + File file = new File(CustomNpcs.getWorldSaveDirectory(), "auction.dat"); + if (!file.exists()) return; + + NBTTagCompound compound = CompressedStreamTools.readCompressed( + new java.io.FileInputStream(file)); + + nextId = compound.getInteger("NextId"); + listingFee = compound.getDouble("ListingFee"); + saleTax = compound.getDouble("SaleTax"); + maxListingsPerPlayer = compound.getInteger("MaxListings"); + + // Load listings + NBTTagList listingList = compound.getTagList("Listings", 10); + for (int i = 0; i < listingList.tagCount(); i++) { + AuctionListing listing = new AuctionListing(); + listing.readFromNBT(listingList.getCompoundTagAt(i)); + listings.put(listing.id, listing); + sellerIndex.computeIfAbsent(listing.sellerUUID, k -> new HashSet<>()) + .add(listing.id); + } + + // Load claims + NBTTagList claimList = compound.getTagList("PendingClaims", 10); + for (int i = 0; i < claimList.tagCount(); i++) { + NBTTagCompound playerClaims = claimList.getCompoundTagAt(i); + UUID playerUUID = UUID.fromString(playerClaims.getString("UUID")); + NBTTagList claims = playerClaims.getTagList("Claims", 10); + List claimsList = new ArrayList<>(); + for (int j = 0; j < claims.tagCount(); j++) { + AuctionClaim claim = new AuctionClaim(); + claim.readFromNBT(claims.getCompoundTagAt(j)); + claimsList.add(claim); + } + pendingClaims.put(playerUUID, claimsList); + } + + } catch (Exception e) { + LogWriter.except(e); + } + } +} + +// Claim data structure +class AuctionClaim { + public ItemStack item; + public double money; + public String description; + public long timestamp; + + public AuctionClaim() {} + + public AuctionClaim(ItemStack item, double money, String description) { + this.item = item != null ? item.copy() : null; + this.money = money; + this.description = description; + this.timestamp = System.currentTimeMillis(); + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + if (item != null) { + compound.setTag("Item", item.writeToNBT(new NBTTagCompound())); + } + compound.setDouble("Money", money); + compound.setString("Description", description); + compound.setLong("Timestamp", timestamp); + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + if (compound.hasKey("Item")) { + item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + } + money = compound.getDouble("Money"); + description = compound.getString("Description"); + timestamp = compound.getLong("Timestamp"); + } +} +``` + +### 3.4 Auctioneer Role + +**File: `src/main/java/noppes/npcs/roles/RoleAuctioneer.java`** + +```java +package noppes.npcs.roles; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.entity.EntityNPCInterface; + +public class RoleAuctioneer extends RoleInterface { + + // Configuration + public String auctionHouseName = "Global"; // For future multi-auction support + public boolean allowSelling = true; + public boolean allowBuying = true; + public boolean showExpiredListings = true; + + // Custom messages + public String welcomeMessage = ""; + public String sellDisabledMessage = "This auctioneer does not accept listings."; + public String buyDisabledMessage = "This auctioneer does not sell items."; + + public RoleAuctioneer(EntityNPCInterface npc) { + super(npc); + } + + @Override + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("AuctionHouse", auctionHouseName); + compound.setBoolean("AllowSelling", allowSelling); + compound.setBoolean("AllowBuying", allowBuying); + compound.setBoolean("ShowExpired", showExpiredListings); + compound.setString("WelcomeMsg", welcomeMessage); + compound.setString("SellDisabledMsg", sellDisabledMessage); + compound.setString("BuyDisabledMsg", buyDisabledMessage); + return compound; + } + + @Override + public void readFromNBT(NBTTagCompound compound) { + auctionHouseName = compound.getString("AuctionHouse"); + allowSelling = compound.getBoolean("AllowSelling"); + allowBuying = compound.getBoolean("AllowBuying"); + showExpiredListings = compound.getBoolean("ShowExpired"); + welcomeMessage = compound.getString("WelcomeMsg"); + sellDisabledMessage = compound.getString("SellDisabledMsg"); + buyDisabledMessage = compound.getString("BuyDisabledMsg"); + } + + @Override + public void interact(EntityPlayer player) { + // Show welcome message if set + if (!welcomeMessage.isEmpty()) { + npc.say(player, npc.advanced.getInteractLine()); + } + + // Open auction house GUI + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); + } +} +``` + +### 3.5 Update EnumRoleType + +**File: `src/main/java/noppes/npcs/constants/EnumRoleType.java`** + +```java +public enum EnumRoleType { + None, + Trader, + Follower, + Bank, + Transporter, + Postman, + Companion, + Mount, + Auctioneer // Add this +} +``` + +### 3.6 Update DataAdvanced + +**File: `src/main/java/noppes/npcs/DataAdvanced.java`** + +Add to `setRole()` method: + +```java +else if (role == EnumRoleType.Auctioneer && !(npc.roleInterface instanceof RoleAuctioneer)) + npc.roleInterface = new RoleAuctioneer(npc); +``` + +### 3.7 Update EnumGuiType + +**File: `src/main/java/noppes/npcs/constants/EnumGuiType.java`** + +Add new GUI types: + +```java +// Auction House GUIs +PlayerAuction, // Main auction browse/buy interface +PlayerAuctionSell, // Create new listing +PlayerAuctionMyListings, // View own listings +PlayerAuctionClaims, // Claim items/money +AuctioneerSetup, // NPC configuration +ManageAuctions // Admin management +``` + +### 3.8 Auction GUI System + +The auction GUI system will consist of multiple interconnected screens: + +#### 3.8.1 Main Auction Browser + +**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionBrowse.java`** + +Features: +- Category filter tabs (Weapons, Armor, Tools, etc.) +- Search bar for item names +- Sort options (Time remaining, Price, Recently listed) +- Paginated listing display +- Quick buy/bid buttons + +#### 3.8.2 Create Listing GUI + +**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java`** + +Features: +- Item slot for the item to sell +- Starting price input +- Buyout price input (optional) +- Duration selector +- Listing fee display +- Confirm button + +#### 3.8.3 My Listings GUI + +**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionMyListings.java`** + +Features: +- List of player's active listings +- Current bid status +- Time remaining +- Cancel button (if no bids) + +#### 3.8.4 Claims GUI + +**File: `src/main/java/noppes/npcs/client/gui/player/GuiAuctionClaims.java`** + +Features: +- List of pending claims (items won, money from sales, refunds) +- Claim button for each item +- Claim all button + +### 3.9 Network Packets + +#### 3.9.1 Auction Action Packet + +**File: `src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java`** + +```java +package kamkeel.npcs.network.packets.player; + +public class AuctionActionPacket extends AbstractPacket { + + public enum Action { + CREATE_LISTING, + PLACE_BID, + BUYOUT, + CANCEL, + CLAIM, + CLAIM_ALL, + BROWSE, + MY_LISTINGS + } + + private Action action; + private int listingId; + private double amount; + private NBTTagCompound itemData; + private int duration; + private double buyoutPrice; + private int category; + private String search; + private int page; + + // Implement sendData/receiveData... +} +``` + +#### 3.9.2 Auction Sync Packet + +**File: `src/main/java/kamkeel/npcs/network/packets/data/AuctionSyncPacket.java`** + +Used to send auction listing data to clients for GUI display. + +### 3.10 Periodic Tasks + +Add to server tick handler to process expired auctions: + +**File: `src/main/java/noppes/npcs/ServerTickHandler.java`** + +```java +private int auctionCheckCounter = 0; + +@SubscribeEvent +public void onServerTick(TickEvent.ServerTickEvent event) { + // ... existing code ... + + // Check expired auctions every 5 minutes (6000 ticks) + if (++auctionCheckCounter >= 6000) { + auctionCheckCounter = 0; + AuctionController.getInstance().processExpiredAuctions(); + } +} +``` + +--- + +## Implementation Order + +The features should be implemented in this order due to dependencies: + +### Phase 1: Currency System (Foundation) +1. Add VaultAPI dependency to build files +2. Create `VaultHelper.java` with reflection-based integration +3. Create `CurrencyController.java` +4. Add initialization hook in `CustomNpcs.java` +5. Create `PlayerCurrencyData.java` and integrate with `PlayerData` +6. Create currency configuration GUI +7. Test with and without Vault present + +### Phase 2: Trader Enhancements +1. Create `EnumStockReset.java` +2. Create `TraderStock.java` +3. Modify `RoleTrader.java` to add stock and currency fields +4. Modify `ContainerNPCTrader.java` to handle stock/currency +5. Update `GuiNpcTraderSetup.java` with new options +6. Update network packets for stock synchronization +7. Test stock depletion and reset timers + +### Phase 3: Auction House +1. Create auction enums (`EnumAuctionDuration`, `EnumAuctionStatus`, `EnumAuctionCategory`) +2. Create `AuctionListing.java` and `AuctionClaim` classes +3. Create `AuctionController.java` +4. Update `EnumRoleType.java` to add Auctioneer +5. Create `RoleAuctioneer.java` +6. Update `DataAdvanced.java` for role instantiation +7. Update `EnumGuiType.java` with auction GUIs +8. Create auction containers +9. Create auction GUI classes +10. Create network packets +11. Add periodic task for expired auctions +12. Create auctioneer setup GUI +13. Comprehensive testing + +--- + +## File Summary + +### New Files to Create + +| File | Description | +|------|-------------| +| `kamkeel/npcs/compat/VaultHelper.java` | VaultAPI integration | +| `kamkeel/npcs/controllers/CurrencyController.java` | Currency management | +| `noppes/npcs/controllers/data/PlayerCurrencyData.java` | Per-player currency data | +| `noppes/npcs/controllers/data/TraderStock.java` | Trader stock configuration | +| `noppes/npcs/constants/EnumStockReset.java` | Stock reset types | +| `noppes/npcs/controllers/AuctionController.java` | Auction house management | +| `noppes/npcs/controllers/data/AuctionListing.java` | Auction listing data | +| `noppes/npcs/constants/EnumAuctionDuration.java` | Auction duration options | +| `noppes/npcs/constants/EnumAuctionStatus.java` | Auction status states | +| `noppes/npcs/constants/EnumAuctionCategory.java` | Item categories | +| `noppes/npcs/roles/RoleAuctioneer.java` | Auctioneer NPC role | +| `noppes/npcs/containers/ContainerAuction*.java` | Auction containers | +| `noppes/npcs/client/gui/player/GuiAuction*.java` | Auction GUIs | +| `noppes/npcs/client/gui/roles/GuiNpcAuctioneerSetup.java` | Auctioneer config | +| `kamkeel/npcs/network/packets/player/AuctionActionPacket.java` | Auction actions | +| `kamkeel/npcs/network/packets/data/AuctionSyncPacket.java` | Auction data sync | + +### Files to Modify + +| File | Changes | +|------|---------| +| `dependencies.gradle` | Add VaultAPI dependency | +| `repositories.gradle` | Add Vault repository | +| `CustomNpcs.java` | Initialize VaultHelper, AuctionController | +| `EnumRoleType.java` | Add Auctioneer | +| `EnumGuiType.java` | Add auction GUI types | +| `DataAdvanced.java` | Handle Auctioneer role | +| `RoleTrader.java` | Add stock and currency support | +| `ContainerNPCTrader.java` | Handle stock/currency transactions | +| `GuiNpcTraderSetup.java` | Add stock/currency UI | +| `PlayerData.java` | Add PlayerCurrencyData | +| `CommonProxy.java` | Register auction containers | +| `ServerTickHandler.java` | Add auction expiration check | +| `PacketHandler.java` | Register auction packets | + +--- + +## Testing Strategy + +### Unit Testing +- Currency operations (deposit, withdraw, balance) +- Stock depletion and reset calculations +- Auction bid validation +- Auction expiration handling + +### Integration Testing +- VaultAPI integration on hybrid servers +- Fallback to item currency when Vault unavailable +- Stock persistence across server restarts +- Auction persistence and recovery + +### User Acceptance Testing +- Complete trading flow with stock limits +- Complete auction flow (list → bid → win → claim) +- Currency display formatting +- GUI usability + +### Edge Cases +- Negative currency amounts +- Concurrent bids on same auction +- Server crash during transaction +- Player offline when auction ends +- Maximum listings per player +- Inventory full when claiming + +--- + +## API/Scripting Extensions + +Consider adding these scripting hooks: + +```java +// ICurrency interface for scripts +double getBalance(IPlayer player); +boolean withdraw(IPlayer player, double amount); +boolean deposit(IPlayer player, double amount); +String format(double amount); + +// IAuction interface for scripts +IAuctionListing createListing(IPlayer seller, IItemStack item, + double startPrice, double buyout, int durationHours); +boolean cancelListing(int listingId); +List getPlayerListings(IPlayer player); + +// Events +onAuctionCreated(IAuctionListing listing); +onAuctionBid(IAuctionListing listing, IPlayer bidder, double amount); +onAuctionSold(IAuctionListing listing, IPlayer buyer, double price); +onAuctionExpired(IAuctionListing listing); +onTraderPurchase(ITrader trader, IPlayer buyer, int slot, int stockRemaining); +onTraderStockReset(ITrader trader); +``` + +--- + +## Conclusion + +This implementation plan provides a comprehensive roadmap for the CustomNPC+ 1.11 Market Update. The three features are designed to work together seamlessly: + +1. **Currency System** provides the foundation for monetary transactions +2. **Trader Enhancements** modernize the existing trader with stock and currency support +3. **Auction House** creates a global marketplace leveraging the currency system + +The implementation follows existing codebase patterns and can be developed incrementally, with each phase building on the previous one. diff --git a/gradle.properties b/gradle.properties index 39f2d510a..48416e9a4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -199,7 +199,7 @@ disableCheckstyle = true # ideaCheckSpotlessOnBuild = true # Non-GTNH properties -modVersion = 1.10 +modVersion = 1.10.3 # Increase Gradle's JVM memory usage to avoid daemon expiration due to # insufficient heap space on CI builds. diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 85d5c3f43..aa61689c4 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -60,6 +60,7 @@ import kamkeel.npcs.network.packets.player.MailActionPacket; import kamkeel.npcs.network.packets.player.SaveBookPacket; import kamkeel.npcs.network.packets.player.SaveSignPacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; +import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import kamkeel.npcs.network.packets.player.TransportPacket; import kamkeel.npcs.network.packets.player.customgui.CustomGuiButtonPacket; import kamkeel.npcs.network.packets.player.customgui.CustomGuiClosePacket; @@ -568,6 +569,7 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new InputDevicePacket()); PLAYER_PACKET.registerPacket(new ScreenSizePacket()); + PLAYER_PACKET.registerPacket(new SpecialKeyStatePacket()); PLAYER_PACKET.registerPacket(new SyncRevisionInfoPacket()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java index 04d3d3396..0c1db3cd9 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java @@ -14,6 +14,7 @@ public enum EnumPlayerPacket { CompanionAction, GetRole, InputDevice, + SpecialKeyState, ScreenSize, MagicCycles, diff --git a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java new file mode 100644 index 000000000..3b9f56690 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java @@ -0,0 +1,60 @@ +package kamkeel.npcs.network.packets.player; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; + +import java.io.IOException; + +public final class SpecialKeyStatePacket extends AbstractPacket { + private boolean pressed; + + public SpecialKeyStatePacket() { + } + + private SpecialKeyStatePacket(boolean pressed) { + this.pressed = pressed; + } + + public static void send(boolean pressed) { + PacketClient.sendClient(new SpecialKeyStatePacket(pressed)); + } + + @Override + public Enum getType() { + return EnumPlayerPacket.SpecialKeyState; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.PLAYER_PACKET; + } + + @Override + @SideOnly(Side.CLIENT) + public void sendData(ByteBuf out) throws IOException { + out.writeBoolean(pressed); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + boolean keyDown = in.readBoolean(); + if (!(player instanceof EntityPlayerMP)) { + return; + } + + PlayerData data = PlayerDataController.Instance.getPlayerData((EntityPlayerMP) player); + if (data != null) { + data.setSpecialKeyDown(keyDown); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java index c62a36553..faa14f4d1 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java @@ -92,7 +92,7 @@ public final class LinkedItemScriptPacket extends AbstractPacket { } else { data.saveScript(in); if (ConfigDebug.PlayerLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { - LogWriter.script(String.format("[%s] (Player) %s SAVED EFFECT %s [%s]", "LINKED ITEM SCRIPTS", player.getCommandSenderName(), linkedItem.getName())); + LogWriter.script(String.format("[%s] (Player) %s SAVED LINKED ITEM [%s]", "LINKED ITEM SCRIPTS", player.getCommandSenderName(), linkedItem.getName())); } } } diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index aecfcc17c..c5148bd5b 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -101,7 +101,7 @@ import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; -@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.2") +@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.3") public class CustomNpcs { @SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy") diff --git a/src/main/java/noppes/npcs/NoppesUtilPlayer.java b/src/main/java/noppes/npcs/NoppesUtilPlayer.java index 2c9c3b647..c78ffdf46 100644 --- a/src/main/java/noppes/npcs/NoppesUtilPlayer.java +++ b/src/main/java/noppes/npcs/NoppesUtilPlayer.java @@ -123,8 +123,12 @@ public class NoppesUtilPlayer { } public static void teleportPlayer(EntityPlayerMP player, double posX, double posY, double posZ, int dimension) { + // Dismount from NPC mount before teleportation to prevent mount desync + if (player.ridingEntity instanceof EntityNPCInterface) { + player.mountEntity(null); + } + if (player.dimension != dimension) { - int dim = player.dimension; MinecraftServer server = MinecraftServer.getServer(); WorldServer wor = server.worldServerForDimension(dimension); if (wor == null) { @@ -144,8 +148,12 @@ public class NoppesUtilPlayer { } public static void teleportPlayer(EntityPlayerMP player, double posX, double posY, double posZ, float yaw, float pitch, int dimension) { + // Dismount from NPC mount before teleportation to prevent mount desync + if (player.ridingEntity instanceof EntityNPCInterface) { + player.mountEntity(null); + } + if (player.dimension != dimension) { - int dim = player.dimension; MinecraftServer server = MinecraftServer.getServer(); WorldServer wor = server.worldServerForDimension(dimension); if (wor == null) { diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index f5a2f4d42..e9531296a 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -44,6 +44,7 @@ import net.minecraftforge.event.world.BlockEvent; import noppes.npcs.api.IWorld; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.item.IItemCustomizable; +import noppes.npcs.api.item.IItemStack; import noppes.npcs.config.ConfigMain; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumQuestType; @@ -60,6 +61,8 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.items.ItemNpcTool; import noppes.npcs.quests.QuestItem; import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.scripted.item.ScriptCustomizableItem; +import noppes.npcs.scripted.item.ScriptItemStack; import static noppes.npcs.config.ConfigMain.TrackedQuestUpdateFrequency; @@ -545,6 +548,18 @@ public class ScriptPlayerEventHandler { } if (event.source.getEntity() instanceof EntityPlayerMP) { + EntityPlayerMP playerMP = (EntityPlayerMP) event.source.getEntity(); + if(playerMP.getHeldItem() != null) { + IItemStack itemStack = NpcAPI.Instance().getIItemStack(playerMP.getHeldItem()); + if (itemStack instanceof ScriptCustomizableItem) { + ScriptCustomizableItem sci = (ScriptCustomizableItem) itemStack; + int attackSpeed = sci.getAttackSpeed(); + if (event.entityLiving.hurtResistantTime <= event.entityLiving.maxHurtResistantTime - attackSpeed) { + event.entityLiving.hurtResistantTime = 0; + } + } + } + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.source.getEntity()); float attackAmount = event.ammount; if (ConfigMain.AttributesEnabled && !DBCAddon.IsAvailable()) diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index a00274beb..47a1148df 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -173,19 +173,6 @@ public class ServerEventsHandler { @SubscribeEvent public void invoke(LivingHurtEvent event) { - - - - if(event.source.getEntity() instanceof EntityPlayerMP && event.entity instanceof EntityLivingBase ){ - EntityPlayerMP ep = (EntityPlayerMP) event.source.getEntity(); - if(ep.getHeldItem() != null && ep.getHeldItem().getItem() instanceof ItemLinked){ - int time = ep.getHeldItem().stackTagCompound.getCompoundTag("ItemData").getCompoundTag("LinkedData").getInteger("AttackSpeed"); - event.entity.hurtResistantTime = time; - ((EntityLivingBase) event.entity).hurtTime = time; - } - } - - if (!(event.entityLiving instanceof EntityPlayer)) return; @@ -433,6 +420,8 @@ public class ServerEventsHandler { return; String name = EntityList.getEntityString(entity); + if (entity instanceof EntityPlayer) + name = "Player"; QuestKill quest = (QuestKill) data.quest.questInterface; if (data.quest.partyOptions.objectiveRequirement == EnumPartyObjectives.Leader && !party.getLeaderUUID().equals(player.getUniqueID())) return; diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 9cbc331e2..eb5018871 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -10,6 +10,7 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.world.WorldServer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; public class ServerTickHandler { @@ -65,6 +66,11 @@ public class ServerTickHandler { @SubscribeEvent public void playerLogout(PlayerEvent.PlayerLoggedOutEvent event) { + // Dismount player from NPC mount before logout to prevent orphaned mount state + if (event.player.ridingEntity instanceof EntityNPCInterface) { + event.player.mountEntity(null); + } + PlayerData playerData = PlayerData.get(event.player); if (playerData != null) { playerData.onLogout(); diff --git a/src/main/java/noppes/npcs/ai/EntityAILeapAtTargetNpc.java b/src/main/java/noppes/npcs/ai/EntityAILeapAtTargetNpc.java new file mode 100644 index 000000000..437dbd6d3 --- /dev/null +++ b/src/main/java/noppes/npcs/ai/EntityAILeapAtTargetNpc.java @@ -0,0 +1,63 @@ +package noppes.npcs.ai; + +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.ai.EntityAIBase; +import net.minecraft.util.MathHelper; +import noppes.npcs.entity.EntityNPCInterface; + +public class EntityAILeapAtTargetNpc extends EntityAIBase { + private final EntityNPCInterface npc; + private EntityLivingBase leapTarget; + private final float leapMotionY; + + public EntityAILeapAtTargetNpc(EntityNPCInterface npc, float leapMotionY) { + this.npc = npc; + this.leapMotionY = leapMotionY; + this.setMutexBits(5); + } + + @Override + public boolean shouldExecute() { + this.leapTarget = this.npc.getAttackTarget(); + if (this.leapTarget == null) { + return false; + } + double distanceSq = this.npc.getDistanceSqToEntity(this.leapTarget); + if (distanceSq < 4.0D || distanceSq > 16.0D) { + return false; + } + if (!this.npc.onGround) { + return false; + } + return this.npc.getRNG().nextInt(5) == 0; + } + + @Override + public boolean continueExecuting() { + boolean airborne = !this.npc.onGround; + if (!airborne) { + this.npc.setNpcJumpingState(false); + } + return airborne; + } + + @Override + public void startExecuting() { + double d0 = this.leapTarget.posX - this.npc.posX; + double d1 = this.leapTarget.posZ - this.npc.posZ; + float f = MathHelper.sqrt_double(d0 * d0 + d1 * d1); + + if ((double) f >= 1.0E-4D) { + this.npc.motionX += d0 / (double) f * 0.5D * 0.8D + this.npc.motionX * 0.2D; + this.npc.motionZ += d1 / (double) f * 0.5D * 0.8D + this.npc.motionZ * 0.2D; + } + + this.npc.motionY = this.leapMotionY; + this.npc.setNpcJumpingState(true); + } + + @Override + public void resetTask() { + this.npc.setNpcJumpingState(false); + } +} diff --git a/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java b/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java index 5dbe5f233..4f87575bf 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java +++ b/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java @@ -30,7 +30,11 @@ public class EntityAIPounceTarget extends EntityAIBase { @Override public boolean continueExecuting() { - return !this.npc.onGround; + boolean airborne = !this.npc.onGround; + if (!airborne) { + this.npc.setNpcJumpingState(false); + } + return airborne; } @Override @@ -47,6 +51,12 @@ public class EntityAIPounceTarget extends EntityAIBase { this.npc.motionX *= this.leapSpeed; this.npc.motionZ *= this.leapSpeed; this.npc.motionY *= this.leapSpeed; + this.npc.setNpcJumpingState(true); + } + + @Override + public void resetTask() { + this.npc.setNpcJumpingState(false); } public float getAngleForXYZ(double varX, double varY, double varZ, double horiDist) { diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index da455f5a0..4545be96e 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -223,6 +223,7 @@ import java.util.Random; public class ClientProxy extends CommonProxy { public static KeyBinding NPCButton; + public static KeyBinding SpecialKey; public static final Random RAND = new Random(); public static FontContainer Font; @@ -289,8 +290,10 @@ public class ClientProxy extends CommonProxy { Minecraft mc = Minecraft.getMinecraft(); NPCButton = new KeyBinding("NPC Inventory", Keyboard.KEY_N, "key.categories.customnpc"); + SpecialKey = new KeyBinding("key.customnpcs.special", Keyboard.KEY_C, "key.categories.customnpc"); ClientRegistry.registerKeyBinding(NPCButton); + ClientRegistry.registerKeyBinding(SpecialKey); new PresetController(CustomNpcs.Dir); diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 46af7df1a..af44405dc 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -8,6 +8,7 @@ import cpw.mods.fml.common.gameevent.TickEvent.Phase; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.InputDevicePacket; +import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; import net.minecraft.client.Minecraft; import net.minecraft.entity.Entity; @@ -23,9 +24,12 @@ import noppes.npcs.client.gui.hud.EnumHudComponent; import noppes.npcs.client.gui.hud.HudComponent; import noppes.npcs.client.gui.player.inventory.GuiCNPCInventory; import noppes.npcs.client.renderer.RenderNPCInterface; +import noppes.npcs.constants.EnumRoleType; import noppes.npcs.constants.MarkType; import noppes.npcs.controllers.data.MarkData; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.roles.RoleMount; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import tconstruct.client.tabs.InventoryTabCustomNpc; @@ -42,6 +46,7 @@ public class ClientTickHandler { private int buttonPressed = -1; private long buttonTime = 0L; private final int[] ignoreKeys = new int[]{157, 29, 54, 42, 184, 56, 220, 219}; + private boolean lastSpecialKeyDown = false; @SubscribeEvent(priority = EventPriority.LOWEST) public void onClientTick(TickEvent.ClientTickEvent event) { @@ -52,6 +57,30 @@ public class ClientTickHandler { } this.prevWorld = mc.theWorld; } + if (event.phase == Phase.START) { + EntityPlayer player = mc.thePlayer; + if (player != null) { + boolean specialKeyDown = ClientProxy.SpecialKey != null && Keyboard.isKeyDown(ClientProxy.SpecialKey.getKeyCode()); + if (specialKeyDown != lastSpecialKeyDown) { + PlayerData data = CustomNpcs.proxy.getPlayerData(player); + if (data != null) { + data.setSpecialKeyDown(specialKeyDown); + } + SpecialKeyStatePacket.send(specialKeyDown); + lastSpecialKeyDown = specialKeyDown; + } + + if (player.ridingEntity instanceof EntityNPCInterface) { + EntityNPCInterface mount = (EntityNPCInterface) player.ridingEntity; + if (mount.advanced.role == EnumRoleType.Mount && mount.roleInterface instanceof RoleMount) { + RoleMount role = (RoleMount) mount.roleInterface; + if (!role.isSprintAllowed() && player.isSprinting()) { + player.setSprinting(false); + } + } + } + } + } if (event.phase == Phase.END) { if (mc.thePlayer != null && mc.theWorld != null && !mc.isGamePaused() && ClientEventHandler.hasOverlays(mc.thePlayer)) { renderCNPCPlayer.itemRenderer.updateEquippedItem(); diff --git a/src/main/java/noppes/npcs/client/controllers/MusicController.java b/src/main/java/noppes/npcs/client/controllers/MusicController.java index fc2d9369b..98c30f2ac 100644 --- a/src/main/java/noppes/npcs/client/controllers/MusicController.java +++ b/src/main/java/noppes/npcs/client/controllers/MusicController.java @@ -66,6 +66,8 @@ public class MusicController { this.playingSound.stopSound(); this.sounds.remove(this.playingSound.sound); this.playingSound = null; + this.entity = null; + this.playDelay = 20; // Prevent immediate restart when music is stopped } else if (soundHandler != null) { try { soundHandler.stopSounds(); @@ -193,4 +195,26 @@ public class MusicController { } this.sounds.clear(); } + + /** + * Stops a specific sound by its resource key. + * This is used to stop dialog sounds without affecting Bard music. + * @param soundKey The sound resource key to stop + */ + public void stopSound(String soundKey) { + if (soundKey == null || soundKey.isEmpty()) { + return; + } + ScriptClientSound sound = this.sounds.remove(soundKey); + if (sound != null) { + SoundHandler soundHandler = Minecraft.getMinecraft().getSoundHandler(); + if (soundHandler != null) { + try { + soundHandler.stopSound(sound); + } catch (Exception ignored) { + } + } + sound.stopSound(); + } + } } diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java b/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java index 0d39c0cd8..9025023b1 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java @@ -706,10 +706,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo if (!questCatSearch.equals(newText)) { questCatSearch = newText; if (questCatScroll != null) { - if (selectedQuestCategory.isEmpty()) - questCatScroll.setList(new ArrayList<>()); - else - questCatScroll.setList(filterList(questCatData, questCatSearch)); + questCatScroll.setList(filterList(questCatData, questCatSearch)); + questCatScroll.resetScroll(); } } } @@ -718,12 +716,14 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!questFinishedSearch.equals(newText)) { questFinishedSearch = newText; - if (questFinishedScroll != null) + if (questFinishedScroll != null) { questFinishedScroll.setList( selectedQuestCategory.isEmpty() ? new ArrayList<>() : filterAndTrimListByCategory(questFinishedData, selectedQuestCategory, questFinishedSearch) ); + questFinishedScroll.resetScroll(); + } } } tf = getTextField(activeFieldId); @@ -731,12 +731,14 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!questActiveSearch.equals(newText)) { questActiveSearch = newText; - if (questActiveScroll != null) + if (questActiveScroll != null) { questActiveScroll.setList( selectedQuestCategory.isEmpty() ? new ArrayList<>() : filterAndTrimListByCategory(questActiveData, selectedQuestCategory, questActiveSearch) ); + questActiveScroll.resetScroll(); + } } } } @@ -747,8 +749,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!questFinishedSearch.equals(newText)) { questFinishedSearch = newText; - if (questFinishedScroll != null) + if (questFinishedScroll != null) { questFinishedScroll.setList(filterList(questFinishedData, questFinishedSearch)); + questFinishedScroll.resetScroll(); + } } } tf = getTextField(activeFieldId); @@ -756,8 +760,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!questActiveSearch.equals(newText)) { questActiveSearch = newText; - if (questActiveScroll != null) + if (questActiveScroll != null) { questActiveScroll.setList(filterList(questActiveData, questActiveSearch)); + questActiveScroll.resetScroll(); + } } } } @@ -768,12 +774,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!dialogCatSearch.equals(newText)) { dialogCatSearch = newText; - if (dialogCatScroll != null) - dialogCatScroll.setList( - selectedDialogCategory.isEmpty() ? - new ArrayList<>() : - filterList(dialogCatData, dialogCatSearch) - ); + if (dialogCatScroll != null) { + dialogCatScroll.setList(filterList(dialogCatData, dialogCatSearch)); + dialogCatScroll.resetScroll(); + } } } tf = getTextField(readFieldId); @@ -781,12 +785,14 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!dialogReadSearch.equals(newText)) { dialogReadSearch = newText; - if (dialogReadScroll != null) + if (dialogReadScroll != null) { dialogReadScroll.setList( selectedDialogCategory.isEmpty() ? new ArrayList<>() : filterAndTrimListByCategory(dialogReadData, selectedDialogCategory, dialogReadSearch) ); + dialogReadScroll.resetScroll(); + } } } } @@ -797,8 +803,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!dialogCompactSearch.equals(newText)) { dialogCompactSearch = newText; - if (dialogCompactScroll != null) + if (dialogCompactScroll != null) { dialogCompactScroll.setList(filterList(dialogReadData, dialogCompactSearch)); + dialogCompactScroll.resetScroll(); + } } } } @@ -809,12 +817,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!transCatSearch.equals(newText)) { transCatSearch = newText; - if (transCatScroll != null) - transCatScroll.setList( - selectedTransCategory.isEmpty() ? - new ArrayList<>() : - filterList(transCatData, transCatSearch) - ); + if (transCatScroll != null) { + transCatScroll.setList(filterList(transCatData, transCatSearch)); + transCatScroll.resetScroll(); + } } } tf = getTextField(locFieldId); @@ -822,12 +828,14 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!transLocSearch.equals(newText)) { transLocSearch = newText; - if (transLocScroll != null) + if (transLocScroll != null) { transLocScroll.setList( selectedTransCategory.isEmpty() ? new ArrayList<>() : filterAndTrimListByCategory(transLocData, selectedTransCategory, transLocSearch) ); + transLocScroll.resetScroll(); + } } } } @@ -838,8 +846,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!transCompactSearch.equals(newText)) { transCompactSearch = newText; - if (transCompactScroll != null) + if (transCompactScroll != null) { transCompactScroll.setList(filterList(transLocData, transCompactSearch)); + transCompactScroll.resetScroll(); + } } } } @@ -850,8 +860,10 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo String newText = tf.getText().toLowerCase(); if (!singleSearch.equals(newText)) { singleSearch = newText; - if (singleScroll != null) + if (singleScroll != null) { singleScroll.setList(filterList(currentTab == 13 ? bankData : factionData, singleSearch)); + singleScroll.resetScroll(); + } } } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java index a5ebd6e1f..41e105a38 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java @@ -262,6 +262,7 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I public void setSelected(String selected) { } + @Override public void closeSubGui(SubGuiInterface gui) { super.closeSubGui(gui); if (gui instanceof GuiNpcMobSpawnerSelector && this.spawnEntryScroll.hasSelected()) { @@ -270,6 +271,9 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I this.spawn.spawnCompounds.put(selected, selector.getCompound()); initGui(); } + if (gui instanceof SubGuiNpcBiomes || gui instanceof SubGuiNpcDimensions || gui instanceof SubGuiSpawningOptions) { + save(); + } } @Override diff --git a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java index f4296a882..227f7cecc 100644 --- a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java @@ -125,12 +125,6 @@ public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListe y += spacing; - // Attack Speed Field (integers only) - //addLabel(new GuiNpcLabel(13,"Attack Speed",x,y + 5, CustomNpcResourceListener.DefaultTextColor)); - - - //y += spacing; - // TEXTURE field addLabel(new GuiNpcLabel(3, "display.texture", x, y + 5, CustomNpcResourceListener.DefaultTextColor)); addTextField(new GuiNpcTextField(2, this, x + 60, y, 200, 20, linkedItem.display.texture)); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiDialogInteract.java b/src/main/java/noppes/npcs/client/gui/player/GuiDialogInteract.java index a49db2873..728649f6d 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiDialogInteract.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiDialogInteract.java @@ -75,6 +75,12 @@ public class GuiDialogInteract extends GuiNPCInterface implements IGuiClose { */ private boolean sentClosePacket = false; + /** + * Tracks the dialog sound that was played, so we can stop only this sound + * on close without affecting Bard music or other sounds. + */ + private String dialogSound = null; + public GuiDialogInteract(EntityNPCInterface npc, Dialog dialog) { super(npc); this.dialog = dialog; @@ -671,8 +677,13 @@ public class GuiDialogInteract extends GuiNPCInterface implements IGuiClose { this.options = new ArrayList(); if (dialog.sound != null && !dialog.sound.isEmpty()) { + // Stop any previous dialog sound before playing new one + if (this.dialogSound != null) { + MusicController.Instance.stopSound(this.dialogSound); + } MusicController.Instance.stopMusic(); MusicController.Instance.playSound(dialog.sound, (float) npc.posX, (float) npc.posY, (float) npc.posZ); + this.dialogSound = dialog.sound; } if (!dialog.showPreviousBlocks) { @@ -707,7 +718,11 @@ public class GuiDialogInteract extends GuiNPCInterface implements IGuiClose { super.close(); if (parent != null) NoppesUtil.openGUI(player, parent); - MusicController.Instance.stopAllSounds(); + // Only stop the dialog sound, not Bard music or other sounds + if (this.dialogSound != null) { + MusicController.Instance.stopSound(this.dialogSound); + this.dialogSound = null; + } } @Override diff --git a/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernDialogInteract.java b/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernDialogInteract.java index fc4a221c7..3f382d98c 100644 --- a/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernDialogInteract.java +++ b/src/main/java/noppes/npcs/client/gui/player/modern/GuiModernDialogInteract.java @@ -63,6 +63,12 @@ public class GuiModernDialogInteract extends GuiNPCInterface implements IGuiClos */ private boolean sentClosePacket = false; + /** + * Tracks the dialog sound that was played, so we can stop only this sound + * on close without affecting Bard music or other sounds. + */ + private String dialogSound = null; + public GuiModernDialogInteract(EntityNPCInterface npc, Dialog dialog) { super(npc); this.dialog = dialog; @@ -380,8 +386,13 @@ public class GuiModernDialogInteract extends GuiNPCInterface implements IGuiClos this.options = new ArrayList(); if (dialog.sound != null && !dialog.sound.isEmpty()) { + // Stop any previous dialog sound before playing new one + if (this.dialogSound != null) { + MusicController.Instance.stopSound(this.dialogSound); + } MusicController.Instance.stopMusic(); MusicController.Instance.playSound(dialog.sound, (float) npc.posX, (float) npc.posY, (float) npc.posZ); + this.dialogSound = dialog.sound; } if (!dialog.showPreviousBlocks) { @@ -440,6 +451,11 @@ public class GuiModernDialogInteract extends GuiNPCInterface implements IGuiClos } closed(); } + // Only stop the dialog sound, not Bard music or other sounds + if (this.dialogSound != null) { + MusicController.Instance.stopSound(this.dialogSound); + this.dialogSound = null; + } super.onGuiClosed(); } } diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java index 5f4d228e2..0639113af 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcMount.java @@ -4,7 +4,6 @@ import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.role.RoleSavePacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.util.GuiNPCInterface2; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -13,8 +12,6 @@ import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.roles.RoleMount; -import java.util.Locale; - public class GuiNpcMount extends GuiNPCInterface2 implements ITextfieldListener { private final RoleMount role; @@ -32,55 +29,82 @@ public class GuiNpcMount extends GuiNPCInterface2 implements ITextfieldListener return; } - int y = guiTop + 6; - addOffsetRow(0, "role.mount.offsetx", role.getOffsetX(), y); - y += 24; - addOffsetRow(1, "role.mount.offsety", role.getOffsetY(), y); - y += 24; - addOffsetRow(2, "role.mount.offsetz", role.getOffsetZ(), y); - y += 24; - addJumpRow(3, "role.mount.jumpheight", role.getJumpStrength(), y); - y += 24; + int leftLabelX = guiLeft + 4; + int leftFieldX = guiLeft + 116; + int leftY = guiTop + 6; - addLabel(new GuiNpcLabel(5, "role.mount.sprint", guiLeft + 4, y + 5)); - addButton(new GuiNpcButton(5, guiLeft + 140, y, 60, 20, new String[]{"gui.no", "gui.yes"}, role.isSprintAllowed() ? 1 : 0)); + leftY = addOffsetRow(0, 10, "role.mount.offsetx", role.getOffsetX(), leftLabelX, leftFieldX, leftY); + leftY = addOffsetRow(1, 11, "role.mount.offsety", role.getOffsetY(), leftLabelX, leftFieldX, leftY); + leftY = addOffsetRow(2, 12, "role.mount.offsetz", role.getOffsetZ(), leftLabelX, leftFieldX, leftY); + leftY = addJumpRow(3, 13, "role.mount.jumpheight", role.getJumpStrength(), leftLabelX, leftFieldX, leftY); - addButton(new GuiNpcButton(4, guiLeft + 4, y + 30, 80, 20, "role.mount.reset")); + addLabel(new GuiNpcLabel(4, "role.mount.sprint", leftLabelX, leftY + 5)); + addButton(new GuiNpcButton(101, leftFieldX, leftY, 60, 20, new String[]{"gui.no", "gui.yes"}, role.isSprintAllowed() ? 1 : 0)); + leftY += 24; - GuiNpcLabel speedLabel = new GuiNpcLabel(6, "", guiLeft + 4, y + 56); - speedLabel.label = StatCollector.translateToLocalFormatted("role.mount.walkspeed", npc.ais.getWalkingSpeed()); - addLabel(speedLabel); + int rightLabelX = guiLeft + 214; + int rightFieldX = guiLeft + 320; + int rightY = guiTop + 6; + + addLabel(new GuiNpcLabel(20, "role.mount.fly", rightLabelX, rightY + 5)); + addButton(new GuiNpcButton(102, rightFieldX, rightY, 60, 20, new String[]{"gui.no", "gui.yes"}, role.isFlyingMountEnabled() ? 1 : 0)); + rightY += 24; + + if (role.isFlyingMountEnabled()) { + addLabel(new GuiNpcLabel(21, "role.mount.hover", rightLabelX, rightY + 5)); + addButton(new GuiNpcButton(103, rightFieldX, rightY, 60, 20, new String[]{"gui.no", "gui.yes"}, role.isHoverModeEnabled() ? 1 : 0)); + rightY += 24; + + rightY = addSpeedRow(22, 14, "role.mount.ascendspeed", role.getFlyingAscendSpeed(), rightLabelX, rightFieldX, rightY, 0.1F, 3.0F, 0.60F); + rightY = addSpeedRow(23, 15, "role.mount.descendspeed", role.getFlyingDescendSpeed(), rightLabelX, rightFieldX, rightY, 0.05F, 3.0F, 0.35F); + } } - private void addOffsetRow(int id, String translationKey, float value, int y) { - addLabel(new GuiNpcLabel(id, translationKey, guiLeft + 4, y + 5)); - GuiNpcTextField field = new GuiNpcTextField(id, this, guiLeft + 140, y, 80, 20, format(value)); + private int addOffsetRow(int labelId, int fieldId, String translationKey, float value, int labelX, int fieldX, int y) { + addLabel(new GuiNpcLabel(labelId, translationKey, labelX, y + 5)); + GuiNpcTextField field = new GuiNpcTextField(fieldId, this, fieldX, y, 80, 20, format(value)); field.floatsOnly = true; field.setMinMaxDefaultFloat(-5.0F, 5.0F, 0.0F); addTextField(field); + return y + 24; } - private void addJumpRow(int id, String translationKey, float value, int y) { - addLabel(new GuiNpcLabel(id, translationKey, guiLeft + 4, y + 5)); - GuiNpcTextField field = new GuiNpcTextField(id, this, guiLeft + 140, y, 80, 20, format(value)); + private int addJumpRow(int labelId, int fieldId, String translationKey, float value, int labelX, int fieldX, int y) { + addLabel(new GuiNpcLabel(labelId, translationKey, labelX, y + 5)); + GuiNpcTextField field = new GuiNpcTextField(fieldId, this, fieldX, y, 80, 20, format(value)); field.floatsOnly = true; field.setMinMaxDefaultFloat(0.1F, 15.0F, 1.0F); addTextField(field); + return y + 24; + } + + private int addSpeedRow(int labelId, int fieldId, String translationKey, float value, int labelX, int fieldX, int y, float min, float max, float defaultValue) { + addLabel(new GuiNpcLabel(labelId, translationKey, labelX, y + 5)); + GuiNpcTextField field = new GuiNpcTextField(fieldId, this, fieldX, y, 80, 20, format(value)); + field.floatsOnly = true; + field.setMinMaxDefaultFloat(min, max, defaultValue); + addTextField(field); + return y + 24; } private String format(float value) { - return String.format(Locale.ROOT, "%.2f", value); + return String.format(java.util.Locale.ROOT, "%.2f", value); } @Override public void buttonEvent(GuiButton guibutton) { if (role == null) return; - if (guibutton.id == 4) { - role.resetOffsets(); - initGui(); - } else if (guibutton.id == 5 && guibutton instanceof GuiNpcButton) { - role.setSprintAllowed(((GuiNpcButton) guibutton).getValue() == 1); + if (guibutton instanceof GuiNpcButton) { + GuiNpcButton button = (GuiNpcButton) guibutton; + if (guibutton.id == 101) { + role.setSprintAllowed(button.getValue() == 1); + } else if (guibutton.id == 102) { + role.setFlyingMountEnabled(button.getValue() == 1); + initGui(); + } else if (guibutton.id == 103) { + role.setHoverModeEnabled(button.getValue() == 1); + } } } @@ -90,32 +114,42 @@ public class GuiNpcMount extends GuiNPCInterface2 implements ITextfieldListener return; float value = textfield.isFloat() ? textfield.getFloat() : 0.0F; switch (textfield.id) { - case 0: + case 10: role.setOffsetX(value); break; - case 1: + case 11: role.setOffsetY(value); break; - case 2: + case 12: role.setOffsetZ(value); break; - case 3: + case 13: role.setJumpStrength(value); break; + case 14: + role.setFlyingAscendSpeed(value); + break; + case 15: + role.setFlyingDescendSpeed(value); + break; } textfield.setText(format(valueForField(textfield.id))); } private float valueForField(int id) { switch (id) { - case 0: + case 10: return role.getOffsetX(); - case 1: + case 11: return role.getOffsetY(); - case 2: + case 12: return role.getOffsetZ(); - case 3: + case 13: return role.getJumpStrength(); + case 14: + return role.getFlyingAscendSpeed(); + case 15: + return role.getFlyingDescendSpeed(); default: return 0.0F; } diff --git a/src/main/java/noppes/npcs/controllers/PlayerQuestController.java b/src/main/java/noppes/npcs/controllers/PlayerQuestController.java index e08203136..7b99f40d8 100644 --- a/src/main/java/noppes/npcs/controllers/PlayerQuestController.java +++ b/src/main/java/noppes/npcs/controllers/PlayerQuestController.java @@ -136,10 +136,14 @@ public class PlayerQuestController { return player.worldObj.getTotalWorldTime() - questTime >= 24000; } else if (quest.repeat == EnumQuestRepeat.MCWEEKLY) { return player.worldObj.getTotalWorldTime() - questTime >= 168000; + } else if (quest.repeat == EnumQuestRepeat.MCCUSTOM) { + return player.worldObj.getTotalWorldTime() - questTime >= quest.customCooldown; } else if (quest.repeat == EnumQuestRepeat.RLDAILY) { return System.currentTimeMillis() - questTime >= 86400000; } else if (quest.repeat == EnumQuestRepeat.RLWEEKLY) { return System.currentTimeMillis() - questTime >= 604800000; + } else if (quest.repeat == EnumQuestRepeat.RLCUSTOM) { + return System.currentTimeMillis() - questTime >= quest.customCooldown; } return false; } diff --git a/src/main/java/noppes/npcs/controllers/data/Animation.java b/src/main/java/noppes/npcs/controllers/data/Animation.java index b60f9ad4d..c7d006198 100644 --- a/src/main/java/noppes/npcs/controllers/data/Animation.java +++ b/src/main/java/noppes/npcs/controllers/data/Animation.java @@ -12,7 +12,10 @@ import noppes.npcs.constants.EnumAnimationPart; import noppes.npcs.controllers.AnimationController; import java.util.ArrayList; - +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; public class Animation implements IAnimation { public AnimationData parent; //Client-sided only @@ -34,6 +37,12 @@ public class Animation implements IAnimation { //Client-sided public boolean paused; + protected final Map dataStore = new HashMap<>(); + + public Consumer onAnimationStart; + public BiConsumer onAnimationFrame; + public Consumer onAnimationEnd; + public Animation() { } @@ -175,6 +184,18 @@ public class Animation implements IAnimation { return time; } + @Override + public boolean hasData(String key) {return dataStore.containsKey(key);} + + @Override + public Object getData(String key) {return dataStore.get(key);} + + @Override + public IAnimation setData(String key, Object v) {dataStore.put(key, v);return this;} + + @Override + public IAnimation removeData(String key) {dataStore.remove(key);return this;} + public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("ID")) { id = compound.getInteger("ID"); @@ -254,8 +275,10 @@ public class Animation implements IAnimation { if (this.currentFrame() != null) { EventHooks.onAnimationFrameEntered(this, this.currentFrame()); + fireFrameTask(this.currentFrame); } else { EventHooks.onAnimationEnded(this); + fireEndTask(); } if (nextFrame != null) { @@ -294,4 +317,61 @@ public class Animation implements IAnimation { } } } + + @Override + public IAnimation onStart(Consumer task) { + this.onAnimationStart = task; + return this; + } + + @Override + public IAnimation onFrame(BiConsumer task) { + this.onAnimationFrame = task; + return this; + } + + @Override + public IAnimation onEnd(Consumer task) { + this.onAnimationEnd = task; + return this; + } + + protected void fireStartTask() { + if (this.onAnimationStart != null) { + this.onAnimationStart.accept(this); + } + } + + protected void fireFrameTask(int frame) { + if (this.onAnimationFrame != null) { + this.onAnimationFrame.accept(frame, this); + } + } + + protected void fireEndTask() { + if (this.onAnimationEnd != null) { + this.onAnimationEnd.accept(this); + } + } + + /** + * Only called on setAnimation, to transfer tasks from + * this global instance stored in AnimationController + * to the new local instance on entity's AnimationData, + * and clear them from the global instance. + */ + public void moveFromGlobalToLocal(Animation animation) { + this.onAnimationStart = animation.onAnimationStart; + this.onAnimationFrame = animation.onAnimationFrame; + this.onAnimationEnd = animation.onAnimationEnd; + + animation.onAnimationEnd = null; + animation.onAnimationFrame = null; + animation.onAnimationStart = null; + + this.dataStore.putAll(animation.dataStore); + animation.dataStore.clear(); + } + + } diff --git a/src/main/java/noppes/npcs/controllers/data/AnimationData.java b/src/main/java/noppes/npcs/controllers/data/AnimationData.java index c537d94e1..f5e6d80b4 100644 --- a/src/main/java/noppes/npcs/controllers/data/AnimationData.java +++ b/src/main/java/noppes/npcs/controllers/data/AnimationData.java @@ -98,15 +98,18 @@ public class AnimationData implements IAnimationData { this.isClientAnimating = this.allowAnimation && this.animation != null; if (prevIsClientAnimating && (!this.isClientAnimating || this.animation != this.currentClientAnimation)) { EventHooks.onAnimationEnded(this.currentClientAnimation); + this.currentClientAnimation.fireEndTask(); } if (this.isClientAnimating) { this.currentClientAnimation = this.animation; } if (this.animation != null && this.allowAnimation) { + this.animation.fireStartTask(); if (EventHooks.onAnimationStarted(this.animation)) return; EventHooks.onAnimationFrameEntered(this.animation, this.animation.currentFrame()); + this.animation.fireFrameTask(this.animation.currentFrame); } List entities = sendingEntity.worldObj.getEntitiesWithinAABB(EntityPlayer.class, AxisAlignedBB.getBoundingBox( @@ -269,6 +272,7 @@ public class AnimationData implements IAnimationData { newAnim.currentFrame = 0; newAnim.currentFrameTime = 0; newAnim.parent = this; + newAnim.moveFromGlobalToLocal((Animation) animation); } diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItem.java b/src/main/java/noppes/npcs/controllers/data/LinkedItem.java index 4fb5fc6e7..f1840a625 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItem.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItem.java @@ -29,7 +29,7 @@ public class LinkedItem implements ILinkedItem { public boolean isNormalItem = false; public boolean isTool = false; public int digSpeed = 1; - public int attackSpeed = 20; + public int attackSpeed = 10; public int armorType = -2; //-2: Fits in no armor slot, -1: Fits in all slots, 0 - 3: Fits in Head -> Boots slot respectively public int enchantability; @@ -59,7 +59,8 @@ public class LinkedItem implements ILinkedItem { compound.setBoolean("IsTool", this.isTool); compound.setBoolean("IsNormalItem", this.isNormalItem); compound.setInteger("DigSpeed", this.digSpeed); - compound.setInteger("AttackSpeed",this.attackSpeed); + compound.setInteger("AttackSpeed", this.attackSpeed); + compound.setInteger("ArmorType", this.armorType); compound.setInteger("Enchantability", this.enchantability); @@ -89,7 +90,7 @@ public class LinkedItem implements ILinkedItem { this.isTool = compound.getBoolean("IsTool"); this.isNormalItem = compound.getBoolean("IsNormalItem"); this.digSpeed = compound.getInteger("DigSpeed"); - this.attackSpeed = compound.getInteger("AttackSpeed"); + this.setAttackSpeed(compound.getInteger("AttackSpeed")); this.armorType = compound.getInteger("ArmorType"); this.enchantability = compound.getInteger("Enchantability"); @@ -241,6 +242,19 @@ public class LinkedItem implements ILinkedItem { this.enchantability = enchantability; } + @Override + public int getAttackSpeed() { + return attackSpeed; + } + + @Override + public void setAttackSpeed(int time) { + if(time <= 0) { + time = 10; + } + this.attackSpeed = time; + } + public LinkedItem clone() { NBTTagCompound nbt = this.writeToNBT(true); LinkedItem clone = new LinkedItem(this.name); @@ -254,10 +268,4 @@ public class LinkedItem implements ILinkedItem { setScriptHandler(data = new LinkedItemScript()); return data; } - - @Override - public int getAttackSpeed() {return attackSpeed;} - - @Override - public void setAttackSpeed(int time) {this.attackSpeed = time;} } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 5f6a5e00d..599f81ff1 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -84,6 +84,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { public ScreenSize screenSize = new ScreenSize(-1, -1); public int profileSlot = 0; + private boolean specialKeyDown = false; public void onLogin() { // Continue playing animation for self when re-logging @@ -267,6 +268,14 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { return this.isGUIOpen; } + public boolean isSpecialKeyDown() { + return specialKeyDown; + } + + public void setSpecialKeyDown(boolean specialKeyDown) { + this.specialKeyDown = specialKeyDown; + } + public ScreenSize getScreenSize() { return screenSize; } diff --git a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java index 78a7ee23a..18c730dd3 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java @@ -45,6 +45,7 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { if (handleMountedMovement(p_70612_1_, p_70612_2_)) return; if (!this.canFly() || this.hurtTime != 0 || !this.canBreathe()) { + setNpcFlyingState(false); super.moveEntityWithHeading(p_70612_1_, p_70612_2_); return; } @@ -66,11 +67,13 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { if (!this.getNavigator().noPath() && this.motionY > 0) { this.motionY = 0; } else { + setNpcFlyingState(false); super.moveEntityWithHeading(p_70612_1_, p_70612_2_); return; } } this.flyLimitAllow = true; + setNpcFlyingState(true); double d3 = this.motionY; super.moveEntityWithHeading(p_70612_1_, p_70612_2_); @@ -81,7 +84,6 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { } this.updateLimbSwing(); - this.velocityChanged = true; } public void updateLimbSwing() { @@ -97,12 +99,15 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { } @Override - protected void doMountedMovement(float strafe, float forward, float moveSpeed) { + public void performMountedMovement(float strafe, float forward, float moveSpeed) { if (!this.canFly() || this.hurtTime != 0 || !this.canBreathe()) { - super.doMountedMovement(strafe, forward, moveSpeed); + setNpcFlyingState(false); + super.performMountedMovement(strafe, forward, moveSpeed); return; } + boolean flightModeActive = isMountFlightModeActive(); + boolean aboveLimit = false; double heightOffGround = this.posY - this.worldObj.getTopSolidOrLiquidBlock((int) this.posX, (int) this.posZ); if (heightOffGround < 0) { @@ -120,14 +125,21 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { if (!this.getNavigator().noPath() && this.motionY > 0) { this.motionY = 0; } else { - super.doMountedMovement(strafe, forward, moveSpeed); + setNpcFlyingState(false); + super.performMountedMovement(strafe, forward, moveSpeed); return; } } this.flyLimitAllow = true; + setNpcFlyingState(true); + + if (flightModeActive) { + super.performMountedMovement(strafe, forward, moveSpeed); + return; + } double d3 = this.motionY; - super.doMountedMovement(strafe, forward, moveSpeed); + super.performMountedMovement(strafe, forward, moveSpeed); this.motionY = d3; if (this.getNavigator().noPath()) { @@ -135,6 +147,5 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { } this.updateLimbSwing(); - this.velocityChanged = true; } } diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 96310a01e..bf382e768 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -32,7 +32,6 @@ import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.ai.EntityAIAttackOnCollide; import net.minecraft.entity.ai.EntityAIBase; import net.minecraft.entity.ai.EntityAIHurtByTarget; -import net.minecraft.entity.ai.EntityAILeapAtTarget; import net.minecraft.entity.ai.EntityAIOpenDoor; import net.minecraft.entity.ai.EntityAIRestrictSun; import net.minecraft.entity.ai.EntityAISwimming; @@ -101,6 +100,7 @@ import noppes.npcs.ai.EntityAIMoveIndoors; import noppes.npcs.ai.EntityAIMovingPath; import noppes.npcs.ai.EntityAIOrbitTarget; import noppes.npcs.ai.EntityAIPanic; +import noppes.npcs.ai.EntityAILeapAtTargetNpc; import noppes.npcs.ai.EntityAIPounceTarget; import noppes.npcs.ai.EntityAIRangedAttack; import noppes.npcs.ai.EntityAIReturn; @@ -159,7 +159,9 @@ import noppes.npcs.roles.RoleInterface; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.entity.ScriptNpc; import noppes.npcs.scripted.event.NpcEvent; +import noppes.npcs.scripted.item.ScriptCustomizableItem; import noppes.npcs.util.GameProfileAlt; +import noppes.npcs.util.NPCMountUtil; import java.io.IOException; import java.util.ArrayList; @@ -211,7 +213,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti public ResourceLocation textureLocation = null; - private Entity lastRider; + private final NPCMountUtil.MountState mountState = new NPCMountUtil.MountState(); + private static final int NPC_STATE_FLAG_FLYING = 1; + private static final int NPC_STATE_FLAG_JUMPING = 1 << 1; public EnumAnimation currentAnimation = EnumAnimation.NONE; @@ -282,6 +286,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti this.dataWatcher.addObject(14, Integer.valueOf(0)); // Animation this.dataWatcher.addObject(15, Integer.valueOf(0)); // isWalking this.dataWatcher.addObject(16, ""); // Role + this.dataWatcher.addObject(17, Byte.valueOf((byte) 0)); // Mount state flags } protected boolean isAIEnabled() { @@ -634,6 +639,22 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti reset(); } + // Check for custom weapon attack speed - bypass immunity if enough time has passed + Entity sourceEntity = damagesource.getEntity(); + if (sourceEntity instanceof EntityPlayer) { + EntityPlayer player = (EntityPlayer) sourceEntity; + ItemStack heldItem = player.getHeldItem(); + if (heldItem != null) { + IItemStack itemStack = NpcAPI.Instance().getIItemStack(heldItem); + if (itemStack instanceof ScriptCustomizableItem) { + int attackSpeed = ((ScriptCustomizableItem) itemStack).getAttackSpeed(); + if (this.hurtResistantTime <= this.maxHurtResistantTime - attackSpeed) { + this.hurtResistantTime = 0; + } + } + } + } + if ((float) this.hurtResistantTime > (float) this.maxHurtResistantTime / 2.0F && i <= this.lastDamage) return false; @@ -967,7 +988,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti */ public void setLeapTask() { if (this.ais.leapType == 1) - this.tasks.addTask(this.taskCount++, aiLeap = new EntityAILeapAtTarget(this, 0.4F)); + this.tasks.addTask(this.taskCount++, aiLeap = new EntityAILeapAtTargetNpc(this, 0.4F)); if (this.ais.leapType == 2) this.tasks.addTask(this.taskCount++, aiLeap = new EntityAIPounceTarget(this)); } @@ -1486,13 +1507,14 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti delete(); } else { if (this.riddenByEntity != null) { - stabilizeDismountedRider(this.riddenByEntity); + NPCMountUtil.stabilizeDismountedRider(this.riddenByEntity); this.riddenByEntity.mountEntity(null); - haltMountedMotion(); + NPCMountUtil.haltMountedMotion(this, mountState); } if (this.ridingEntity != null) this.mountEntity(null); - lastRider = null; + mountState.lastRider = null; + NPCMountUtil.resetMountedFlightState(this, mountState); setHealth(-1); setSprinting(false); getNavigator().clearPathEntity(); @@ -1623,7 +1645,32 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti } public boolean isFlying() { - return isWalking() && !onGround; + return isNpcFlying(); + } + + public boolean isNpcFlying() { + return (dataWatcher.getWatchableObjectByte(17) & NPC_STATE_FLAG_FLYING) != 0; + } + + public boolean isNpcJumping() { + return (dataWatcher.getWatchableObjectByte(17) & NPC_STATE_FLAG_JUMPING) != 0; + } + + private void setNpcStateFlag(int mask, boolean value) { + byte current = dataWatcher.getWatchableObjectByte(17); + byte updated = value ? (byte) (current | mask) : (byte) (current & ~mask); + if (current != updated) { + dataWatcher.updateObject(17, updated); + } + } + + public void setNpcFlyingState(boolean flying) { + setNpcStateFlag(NPC_STATE_FLAG_FLYING, flying); + } + + public void setNpcJumpingState(boolean jumping) { + setNpcStateFlag(NPC_STATE_FLAG_JUMPING, jumping); + this.isJumping = jumping; } public void setBoolFlag(boolean bo, int id) { @@ -1872,9 +1919,12 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti } @Override - protected void fall(float par1) { + protected void fall(float distance) { + if (NPCMountUtil.isFlyingMountWithFlightEnabled(this)) { + return; + } if (!this.stats.noFallDamage) - super.fall(par1); + super.fall(distance); } @Override @@ -2089,64 +2139,47 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti } protected boolean handleMountedMovement(float strafe, float forward) { - if (advanced.role != EnumRoleType.Mount || !(roleInterface instanceof RoleMount)) - return false; - if (riddenByEntity != null && !(riddenByEntity instanceof EntityPlayer)) { - riddenByEntity.mountEntity(null); - return false; - } - if (!(riddenByEntity instanceof EntityPlayer)) - return false; - - RoleMount mount = (RoleMount) roleInterface; - EntityPlayer rider = (EntityPlayer) riddenByEntity; - this.prevRotationYaw = this.rotationYaw = rider.rotationYaw; - this.rotationPitch = rider.rotationPitch * 0.5F; - this.setRotation(this.rotationYaw, this.rotationPitch); - this.renderYawOffset = this.rotationYaw; - this.rotationYawHead = this.rotationYaw; - - float controlledStrafe = rider.moveStrafing * 0.5F; - float controlledForward = rider.moveForward; - if (controlledForward <= 0.0F) { - controlledForward *= 0.25F; - } - - float moveSpeed = Math.max(0.1F, getSpeed()); - boolean riderSprinting = rider.isSprinting() && mount.isSprintAllowed(); - if (riderSprinting) { - moveSpeed *= 1.2F; - } - - if (!mount.isSprintAllowed() && rider.isSprinting()) { - rider.setSprinting(false); - } - - this.setSprinting(riderSprinting); - this.stepHeight = 1.0F; - this.jumpMovementFactor = moveSpeed * 0.1F; - - this.getNavigator().clearPathEntity(); - this.setAttackTarget(null); - this.setRevengeTarget(null); - - this.fallDistance = 0.0F; - rider.fallDistance = 0.0F; - - applyMountedVerticalMotion(rider, moveSpeed, controlledForward); - doMountedMovement(controlledStrafe, controlledForward, moveSpeed); - - return true; + return NPCMountUtil.handleMountedMovement(this, mountState, strafe, forward); } - - protected void doMountedMovement(float strafe, float forward, float moveSpeed) { + + public void performMountedMovement(float strafe, float forward, float moveSpeed) { this.moveStrafing = strafe; this.moveForward = forward; + + boolean flightMode = isMountFlightModeActive(); + double prevMotionY = this.motionY; + + // Server-side only physics (like vanilla horse) if (!worldObj.isRemote) { + this.jumpMovementFactor = getMountedAirStrafeFactor(moveSpeed, flightMode); this.setAIMoveSpeed(moveSpeed); super.moveEntityWithHeading(strafe, forward); + + if (flightMode) { + this.motionY = prevMotionY; + this.isAirBorne = true; + } } + // Both sides do animation (like vanilla horse) + updateMountedLimbSwing(); + } + + protected boolean isMountFlightModeActive() { + return NPCMountUtil.isMountInFlightMode(mountState); + } + + private float getMountedAirStrafeFactor(float moveSpeed, boolean flightMode) { + if (moveSpeed <= 0.0F) { + return 0.0F; + } + if (!flightMode) { + return moveSpeed * 0.1F; + } + return moveSpeed * 0.5F; + } + + private void updateMountedLimbSwing() { this.prevLimbSwingAmount = this.limbSwingAmount; double deltaX = this.posX - this.prevPosX; double deltaZ = this.posZ - this.prevPosZ; @@ -2162,46 +2195,6 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti this.limbSwing += this.limbSwingAmount; } - protected void applyMountedVerticalMotion(EntityLivingBase rider, float moveSpeed, float forward) { - if (!(roleInterface instanceof RoleMount)) { - return; - } - RoleMount mount = (RoleMount) roleInterface; - double jumpFactor = MathHelper.clamp_double(mount.getJumpStrength(), 0.1D, 3.0D); - - if (canFly()) { - double verticalBase = Math.max(0.1D, moveSpeed * 0.6D); - double vertical = MathHelper.clamp_double(verticalBase * jumpFactor, 0.05D, 2.0D); - if (rider.isJumping) { - this.motionY = Math.min(this.motionY + vertical, vertical); - this.isAirBorne = true; - this.velocityChanged = true; - } else { - float pitch = rider.rotationPitch; - if (pitch < -10F) { - this.motionY = vertical; - } else if (pitch > 10F) { - this.motionY = -vertical; - } else if (!this.onGround) { - this.motionY *= 0.6D; - } - } - } else if (rider.isJumping && this.onGround) { - double baseVelocity = MathHelper.clamp_double(0.4D + moveSpeed * 1.5D, 0.2D, 1.2D); - double jumpVelocity = MathHelper.clamp_double(baseVelocity * jumpFactor, 0.2D, 2.0D); - this.motionY = jumpVelocity; - this.isAirBorne = true; - this.velocityChanged = true; - if (forward > 0.0F) { - float yawRadians = this.rotationYaw * (float) Math.PI / 180.0F; - double boost = 0.4D * forward; - this.motionX += -MathHelper.sin(yawRadians) * boost; - this.motionZ += MathHelper.cos(yawRadians) * boost; - } - ForgeHooks.onLivingJump(this); - } - } - @Override public boolean allowLeashing() { return false; @@ -2240,71 +2233,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti } private void handleMountRiderState() { - if (worldObj.isRemote) { - if (advanced.role != EnumRoleType.Mount || !(roleInterface instanceof RoleMount)) { - lastRider = null; - } - return; - } - - if (advanced.role != EnumRoleType.Mount || !(roleInterface instanceof RoleMount)) { - lastRider = null; - return; - } - - Entity currentRider = this.riddenByEntity; - if (currentRider != null && !(currentRider instanceof EntityPlayer)) { - Entity dismount = currentRider; - dismount.mountEntity(null); - stabilizeDismountedRider(dismount); - haltMountedMotion(); - currentRider = null; - } - if (currentRider != lastRider) { - if (lastRider != null) { - stabilizeDismountedRider(lastRider); - haltMountedMotion(); - } - lastRider = currentRider; - } - } - - private void stabilizeDismountedRider(Entity rider) { - boolean wasOnGround = rider.onGround; - double previousMotionX = rider.motionX; - double previousMotionY = rider.motionY; - double previousMotionZ = rider.motionZ; - float previousFall = rider.fallDistance; - - if (wasOnGround) { - rider.motionX = 0.0D; - rider.motionY = 0.0D; - rider.motionZ = 0.0D; - rider.fallDistance = 0.0F; - rider.onGround = true; - } else { - rider.motionX = previousMotionX; - rider.motionY = previousMotionY; - rider.motionZ = previousMotionZ; - rider.fallDistance = previousFall; - } - - rider.velocityChanged = true; - } - - private void haltMountedMotion() { - this.motionX = 0.0D; - this.motionY = 0.0D; - this.motionZ = 0.0D; - this.velocityChanged = true; - this.isAirBorne = false; - this.setSprinting(false); - this.moveForward = 0.0F; - this.moveStrafing = 0.0F; - this.setAIMoveSpeed(0.0F); - this.limbSwingAmount = 0.0F; - this.limbSwing = 0.0F; - this.getNavigator().clearPathEntity(); + NPCMountUtil.handleMountRiderState(this, mountState); } // Model Types: 0: Steve 64x32, 1: Steve 64x64, 2: Alex 64x64 diff --git a/src/main/java/noppes/npcs/entity/EntityNpcDragon.java b/src/main/java/noppes/npcs/entity/EntityNpcDragon.java index 6f490583f..a9df9da99 100644 --- a/src/main/java/noppes/npcs/entity/EntityNpcDragon.java +++ b/src/main/java/noppes/npcs/entity/EntityNpcDragon.java @@ -94,6 +94,7 @@ public class EntityNpcDragon extends EntityNPCInterface { field_40172_ax += f1 * 0.5F; } super.onLivingUpdate(); + this.isFlying = isNpcFlying(); } // public float ticks = 0.0f; diff --git a/src/main/java/noppes/npcs/entity/EntityNpcPony.java b/src/main/java/noppes/npcs/entity/EntityNpcPony.java index ed6ca617c..81ba41233 100644 --- a/src/main/java/noppes/npcs/entity/EntityNpcPony.java +++ b/src/main/java/noppes/npcs/entity/EntityNpcPony.java @@ -35,4 +35,10 @@ public class EntityNpcPony extends EntityNPCInterface { super.onUpdate(); } + @Override + public void onLivingUpdate() { + super.onLivingUpdate(); + this.isFlying = isNpcFlying(); + } + } diff --git a/src/main/java/noppes/npcs/roles/RoleMount.java b/src/main/java/noppes/npcs/roles/RoleMount.java index a1a07c800..dd1ec0ca7 100644 --- a/src/main/java/noppes/npcs/roles/RoleMount.java +++ b/src/main/java/noppes/npcs/roles/RoleMount.java @@ -10,12 +10,13 @@ public class RoleMount extends RoleInterface { private float offsetX; private float offsetY; private float offsetZ; - private static final float MIN_JUMP_STRENGTH = 0.1F; - private static final float MAX_JUMP_STRENGTH = 3.0F; - private boolean storedReturnToStart; private float jumpStrength = 1.0F; private boolean allowSprint = true; + private boolean flyingMountEnabled = false; + private boolean hoverMode = false; + private float flyingAscendSpeed = 0.60F; + private float flyingDescendSpeed = 0.35F; public RoleMount(EntityNPCInterface npc) { super(npc); @@ -31,6 +32,10 @@ public class RoleMount extends RoleInterface { compound.setBoolean("MountReturnFlag", storedReturnToStart); compound.setFloat("MountJumpStrength", jumpStrength); compound.setBoolean("MountAllowSprint", allowSprint); + compound.setBoolean("MountAllowFlying", flyingMountEnabled); + compound.setBoolean("MountHoverMode", hoverMode); + compound.setFloat("MountFlyingAscendSpeed", flyingAscendSpeed); + compound.setFloat("MountFlyingDescendSpeed", flyingDescendSpeed); return compound; } @@ -50,6 +55,20 @@ public class RoleMount extends RoleInterface { jumpStrength = 1.0F; } allowSprint = !compound.hasKey("MountAllowSprint") || compound.getBoolean("MountAllowSprint"); + flyingMountEnabled = compound.hasKey("MountAllowFlying") && compound.getBoolean("MountAllowFlying"); + hoverMode = compound.hasKey("MountHoverMode") && compound.getBoolean("MountHoverMode"); + if (compound.hasKey("MountFlyingAscendSpeed")) { + setFlyingAscendSpeed(compound.getFloat("MountFlyingAscendSpeed")); + } else { + flyingAscendSpeed = 0.60F; + } + if (compound.hasKey("MountFlyingDescendSpeed")) { + setFlyingDescendSpeed(compound.getFloat("MountFlyingDescendSpeed")); + } else if (compound.hasKey("MountFlyingFallSpeed")) { + setFlyingDescendSpeed(compound.getFloat("MountFlyingFallSpeed")); + } else { + flyingDescendSpeed = 0.35F; + } npc.ais.returnToStart = false; } @@ -106,7 +125,7 @@ public class RoleMount extends RoleInterface { } public void setJumpStrength(float value) { - this.jumpStrength = ValueUtil.clamp(value, MIN_JUMP_STRENGTH, MAX_JUMP_STRENGTH); + this.jumpStrength = ValueUtil.clamp(value, 0.1F, 3.0F); } public float getJumpStrength() { @@ -121,6 +140,38 @@ public class RoleMount extends RoleInterface { return allowSprint; } + public void setFlyingMountEnabled(boolean enabled) { + this.flyingMountEnabled = enabled; + } + + public boolean isFlyingMountEnabled() { + return flyingMountEnabled; + } + + public void setHoverModeEnabled(boolean enabled) { + this.hoverMode = enabled; + } + + public boolean isHoverModeEnabled() { + return hoverMode; + } + + public void setFlyingAscendSpeed(float value) { + this.flyingAscendSpeed = ValueUtil.clamp(value, 0.1F, 3.0F); + } + + public float getFlyingAscendSpeed() { + return flyingAscendSpeed; + } + + public void setFlyingDescendSpeed(float value) { + this.flyingDescendSpeed = ValueUtil.clamp(value, 0.05F, 3.0F); + } + + public float getFlyingDescendSpeed() { + return flyingDescendSpeed; + } + public void setReturnToStartPreference(boolean value) { this.storedReturnToStart = value; npc.ais.returnToStart = false; @@ -141,6 +192,10 @@ public class RoleMount extends RoleInterface { } public void onDisable() { + // Dismount any rider when mount role is disabled + if (npc.riddenByEntity != null) { + npc.riddenByEntity.mountEntity(null); + } npc.ais.returnToStart = storedReturnToStart; } diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 0103a9af2..6e85e1555 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -36,6 +36,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus public int digSpeed = 1; public int armorType = -2; //-2: Fits in no armor slot, -1: Fits in all slots, 0 - 4: Fits in Head -> Boots slot respectively public int enchantability; + public int attackSpeed = 10; public long lastInited = -1; @@ -228,6 +229,18 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus return this.enchantability; } + public int getAttackSpeed() { + return this.attackSpeed; + } + + public void setAttackSpeed(int speed) { + if(speed <= 0) { + speed = 10; + } + this.attackSpeed = speed; + saveItemData(); + } + public void setEnchantability(int enchantability) { this.enchantability = enchantability; saveItemData(); @@ -270,6 +283,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus compound.setInteger("DigSpeed", this.digSpeed); compound.setInteger("ArmorType", this.armorType); compound.setInteger("Enchantability", this.enchantability); + compound.setInteger("AttackSpeed", this.attackSpeed); compound.setInteger("MaxItemUseDuration", this.maxItemUseDuration); compound.setInteger("ItemUseAction", this.itemUseAction); @@ -286,6 +300,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus this.digSpeed = compound.getInteger("DigSpeed"); this.armorType = compound.getInteger("ArmorType"); this.enchantability = compound.getInteger("Enchantability"); + setAttackSpeed(compound.getInteger("AttackSpeed")); this.maxItemUseDuration = compound.getInteger("MaxItemUseDuration"); this.itemUseAction = compound.getInteger("ItemUseAction"); diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomizableItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomizableItem.java index 224427c12..77e51df2c 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomizableItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomizableItem.java @@ -30,6 +30,7 @@ public abstract class ScriptCustomizableItem extends ScriptItemStack implements public abstract int getEnchantability(); + public abstract int getAttackSpeed(); public String getTexture() { return this.itemDisplay.texture == null ? "" : this.itemDisplay.texture; diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java index 6e5e10b76..3a777134c 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java @@ -102,6 +102,11 @@ public class ScriptLinkedItem extends ScriptCustomizableItem implements IItemLin return this.linkedItem.enchantability; } + @Override + public int getAttackSpeed() { + return this.linkedItem.attackSpeed; + } + @Override public String getTexture() { return this.itemDisplay.texture == null ? this.linkedItem.display.texture : this.itemDisplay.texture; diff --git a/src/main/java/noppes/npcs/util/NPCMountUtil.java b/src/main/java/noppes/npcs/util/NPCMountUtil.java new file mode 100644 index 000000000..6303f67b9 --- /dev/null +++ b/src/main/java/noppes/npcs/util/NPCMountUtil.java @@ -0,0 +1,336 @@ +package noppes.npcs.util; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.util.MathHelper; +import noppes.npcs.CustomNpcs; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.constants.EnumRoleType; +import noppes.npcs.roles.RoleMount; + +public final class NPCMountUtil { + private static final int MOUNT_FLIGHT_TOGGLE_WINDOW = 7; + + private NPCMountUtil() { + } + + public static class MountState { + public Entity lastRider; + public boolean flightMode; + public boolean jumpPressed; + public int flightToggleTimer; + } + + public static boolean handleMountedMovement(EntityNPCInterface npc, MountState state, float strafe, float forward) { + if (npc.advanced.role != EnumRoleType.Mount || !(npc.roleInterface instanceof RoleMount)) { + resetMountedFlightState(npc, state); + return false; + } + if (npc.riddenByEntity != null && !(npc.riddenByEntity instanceof EntityPlayer)) { + npc.riddenByEntity.mountEntity(null); + return false; + } + if (!(npc.riddenByEntity instanceof EntityPlayer)) { + resetMountedFlightState(npc, state); + return false; + } + + RoleMount mount = (RoleMount) npc.roleInterface; + EntityPlayer rider = (EntityPlayer) npc.riddenByEntity; + updateMountedFlightState(npc, state, mount, rider); + + npc.prevRotationYaw = npc.rotationYaw = rider.rotationYaw; + npc.rotationPitch = rider.rotationPitch * 0.5F; + npc.setPositionAndRotation(npc.posX, npc.posY, npc.posZ, npc.rotationYaw, npc.rotationPitch); + npc.renderYawOffset = npc.rotationYaw; + npc.rotationYawHead = npc.rotationYaw; + + float controlledStrafe = rider.moveStrafing; + float controlledForward = rider.moveForward; + if (controlledForward <= 0.0F) { + controlledForward *= 0.25F; + } + + float moveSpeed = getMountMoveSpeed(npc); + boolean riderSprinting = rider.isSprinting() && mount.isSprintAllowed(); + if (riderSprinting) { + moveSpeed *= 1.2F; + } + + if (!mount.isSprintAllowed() && rider.isSprinting()) { + rider.setSprinting(false); + } + + if (moveSpeed <= 0.0F) { + controlledStrafe = 0.0F; + controlledForward = 0.0F; + } + + npc.setSprinting(riderSprinting); + npc.stepHeight = 1.0F; + + npc.getNavigator().clearPathEntity(); + npc.setAttackTarget(null); + npc.setRevengeTarget(null); + + npc.fallDistance = 0.0F; + rider.fallDistance = 0.0F; + + applyMountedVerticalMotion(npc, state, mount, rider, moveSpeed, controlledForward); + npc.performMountedMovement(controlledStrafe, controlledForward, moveSpeed); + + return true; + } + + private static float getMountMoveSpeed(EntityNPCInterface npc) { + return Math.max(0.0F, npc.getSpeed()); + } + + private static void applyMountedVerticalMotion(EntityNPCInterface npc, MountState state, RoleMount mount, EntityLivingBase rider, float moveSpeed, float forward) { + if (mount == null) { + return; + } + double jumpFactor = MathHelper.clamp_double(mount.getJumpStrength(), 0.1D, 3.0D); + boolean flyingEnabled = mount.isFlyingMountEnabled(); + + npc.setNpcJumpingState(false); + + if (flyingEnabled) { + double ascendSpeed = MathHelper.clamp_double(mount.getFlyingAscendSpeed(), 0.1D, 3.0D); + double descendSpeed = MathHelper.clamp_double(mount.getFlyingDescendSpeed(), 0.05D, 3.0D); + boolean flightMode = isMountInFlightMode(state); + boolean jumpPressed = rider.isJumping; + boolean descendPressed = rider instanceof EntityPlayer && isSpecialKeyDown((EntityPlayer) rider); + + if (flightMode) { + double previousMotionY = npc.motionY; + double newMotionY = 0.0D; + + if (jumpPressed) { + newMotionY += ascendSpeed; + } else if (descendPressed) { + newMotionY -= descendSpeed; + } + + newMotionY = MathHelper.clamp_double(newMotionY, -descendSpeed, ascendSpeed); + npc.motionY = newMotionY; + npc.setNpcFlyingState(true); + npc.setNpcJumpingState(jumpPressed); + npc.isAirBorne = true; + npc.fallDistance = 0.0F; + + return; + } + + if (jumpPressed && npc.onGround) { + npc.motionY = jumpFactor; + npc.setNpcJumpingState(true); + npc.isAirBorne = true; + npc.fallDistance = 0.0F; + if (!npc.worldObj.isRemote) { + net.minecraftforge.common.ForgeHooks.onLivingJump(npc); + } + return; + } + + if (!npc.onGround) { + npc.motionY = Math.max(npc.motionY, -descendSpeed); + npc.setNpcFlyingState(!npc.isInWater()); + } else { + npc.setNpcFlyingState(false); + } + + return; + } + + if (npc.onGround) { + if (rider.isJumping) { + npc.motionY = jumpFactor; + npc.setNpcJumpingState(true); + npc.isAirBorne = true; + npc.fallDistance = 0.0F; + if (!npc.worldObj.isRemote) { + net.minecraftforge.common.ForgeHooks.onLivingJump(npc); + } + } + return; + } + + npc.setNpcFlyingState(false); + } + + public static void handleMountRiderState(EntityNPCInterface npc, MountState state) { + if (npc.worldObj.isRemote) { + if (npc.advanced.role != EnumRoleType.Mount || !(npc.roleInterface instanceof RoleMount)) { + state.lastRider = null; + } + return; + } + + if (npc.advanced.role != EnumRoleType.Mount || !(npc.roleInterface instanceof RoleMount)) { + state.lastRider = null; + return; + } + + RoleMount mount = (RoleMount) npc.roleInterface; + Entity currentRider = npc.riddenByEntity; + + // Validate rider is alive and valid - dismount dead/invalid riders + if (currentRider != null && !currentRider.isEntityAlive()) { + currentRider.mountEntity(null); + stabilizeDismountedRider(currentRider); + haltMountedMotion(npc, state); + applyUnriddenFlightDescent(npc, state, mount); + currentRider = null; + } + + // Only allow EntityPlayer as riders + if (currentRider != null && !(currentRider instanceof EntityPlayer)) { + Entity dismount = currentRider; + dismount.mountEntity(null); + stabilizeDismountedRider(dismount); + haltMountedMotion(npc, state); + applyUnriddenFlightDescent(npc, state, mount); + currentRider = null; + } + if (currentRider != state.lastRider) { + if (state.lastRider != null) { + stabilizeDismountedRider(state.lastRider); + } + if (currentRider == null) { + haltMountedMotion(npc, state); + applyUnriddenFlightDescent(npc, state, mount); + } else { + resetMountedFlightState(npc, state); + } + state.lastRider = currentRider; + } else if (currentRider == null) { + applyUnriddenFlightDescent(npc, state, mount); + } + } + + public static boolean isFlyingMountWithFlightEnabled(EntityNPCInterface npc) { + return npc.advanced.role == EnumRoleType.Mount && npc.roleInterface instanceof RoleMount && ((RoleMount) npc.roleInterface).isFlyingMountEnabled(); + } + + public static void haltMountedMotion(EntityNPCInterface npc, MountState state) { + npc.motionX = 0.0D; + npc.motionY = 0.0D; + npc.motionZ = 0.0D; + npc.isAirBorne = false; + npc.setSprinting(false); + npc.moveForward = 0.0F; + npc.moveStrafing = 0.0F; + npc.setAIMoveSpeed(0.0F); + npc.limbSwingAmount = 0.0F; + npc.limbSwing = 0.0F; + npc.getNavigator().clearPathEntity(); + npc.fallDistance = 0.0F; + resetMountedFlightState(npc, state); + } + + public static void applyUnriddenFlightDescent(EntityNPCInterface npc, MountState state, RoleMount mount) { + if (mount == null || !mount.isFlyingMountEnabled()) { + npc.setNpcFlyingState(false); + npc.setNpcJumpingState(false); + return; + } + if (npc.canFly()) { + return; + } + if (npc.onGround) { + npc.motionY = 0.0D; + npc.setNpcFlyingState(false); + npc.setNpcJumpingState(false); + return; + } + + double descendSpeed = MathHelper.clamp_double(mount.getFlyingDescendSpeed(), 0.05D, 3.0D); + if (descendSpeed <= 0.0D) { + npc.setNpcFlyingState(false); + npc.setNpcJumpingState(false); + return; + } + + npc.motionY = -descendSpeed; + npc.isAirBorne = true; + npc.fallDistance = 0.0F; + npc.setNpcFlyingState(true); + npc.setNpcJumpingState(false); + } + + public static void stabilizeDismountedRider(Entity rider) { + boolean wasOnGround = rider.onGround; + double previousMotionX = rider.motionX; + double previousMotionY = rider.motionY; + double previousMotionZ = rider.motionZ; + float previousFall = rider.fallDistance; + + if (wasOnGround) { + rider.motionX = 0.0D; + rider.motionY = 0.0D; + rider.motionZ = 0.0D; + rider.fallDistance = 0.0F; + rider.onGround = true; + } else { + rider.motionX = previousMotionX; + rider.motionY = previousMotionY; + rider.motionZ = previousMotionZ; + rider.fallDistance = previousFall; + } + } + + private static void updateMountedFlightState(EntityNPCInterface npc, MountState state, RoleMount mount, EntityPlayer rider) { + if (mount == null || rider == null || !mount.isFlyingMountEnabled()) { + resetMountedFlightState(npc, state); + return; + } + + if (npc.onGround) { + state.flightMode = false; + } + + boolean jumpPressed = rider.isJumping; + if (jumpPressed && !state.jumpPressed) { + if (state.flightToggleTimer > 0) { + state.flightMode = !state.flightMode; + state.flightToggleTimer = 0; + } else { + state.flightToggleTimer = MOUNT_FLIGHT_TOGGLE_WINDOW; + } + } + + if (state.flightToggleTimer > 0) { + state.flightToggleTimer--; + } + + state.jumpPressed = jumpPressed; + } + + public static boolean isMountInFlightMode(MountState state) { + return state.flightMode; + } + + public static void resetMountedFlightState(EntityNPCInterface npc, MountState state) { + state.flightMode = false; + state.jumpPressed = false; + state.flightToggleTimer = 0; + npc.setNpcFlyingState(false); + npc.setNpcJumpingState(false); + } + + private static boolean isSpecialKeyDown(EntityPlayer rider) { + if (rider == null) { + return false; + } + if (rider.worldObj.isRemote) { + PlayerData data = CustomNpcs.proxy.getPlayerData(rider); + return data != null && data.isSpecialKeyDown(); + } + PlayerData data = PlayerDataController.Instance.getPlayerData(rider); + return data != null && data.isSpecialKeyDown(); + } +} diff --git a/src/main/resources/assets/customnpcs/lang/cs_CZ.lang b/src/main/resources/assets/customnpcs/lang/cs_CZ.lang index e574a1fb1..c45e3dade 100644 --- a/src/main/resources/assets/customnpcs/lang/cs_CZ.lang +++ b/src/main/resources/assets/customnpcs/lang/cs_CZ.lang @@ -440,10 +440,11 @@ item.npcCrown.name=Koruna item.npcCrown2.name=Koruna key.npcInventory=NPC inventář role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/de_DE.lang b/src/main/resources/assets/customnpcs/lang/de_DE.lang index 86269b7b0..32170875b 100644 --- a/src/main/resources/assets/customnpcs/lang/de_DE.lang +++ b/src/main/resources/assets/customnpcs/lang/de_DE.lang @@ -872,10 +872,11 @@ companion.stage=Bühne key.npcInventory=NPC Inventar role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 6026ed59a..466acfb99 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1,6 +1,7 @@ customnpcs.defaultTextColor=404040 key.categories.customnpc=CustomNPC+ +key.customnpcs.special=Special Key key.npcInventory=NPC Inventory entity.CustomNpc.name=Custom Npc @@ -418,13 +419,16 @@ role.mailman=Mailman role.marketname=Linked Marketname role.companion=Companion role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting -role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint +role.mount.fly=Flying +role.mount.hover=Hover +role.mount.ascendspeed=Ascend Speed +role.mount.descendspeed=Descend Speed +role.mount.walkspeed=Walk Speed: %s trader.insufficient=Insufficient Currency trader.sufficient=Sufficient Currency diff --git a/src/main/resources/assets/customnpcs/lang/es_AR.lang b/src/main/resources/assets/customnpcs/lang/es_AR.lang index f0d9533a2..03e80082e 100644 --- a/src/main/resources/assets/customnpcs/lang/es_AR.lang +++ b/src/main/resources/assets/customnpcs/lang/es_AR.lang @@ -500,10 +500,11 @@ item.npcMusket.name=Mosquete item.npcExcalibur.name=Excalibur key.npcInventory=Inventario de NPC role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/es_MX.lang b/src/main/resources/assets/customnpcs/lang/es_MX.lang index 9b3ee6a1f..7ea1b25b2 100644 --- a/src/main/resources/assets/customnpcs/lang/es_MX.lang +++ b/src/main/resources/assets/customnpcs/lang/es_MX.lang @@ -500,10 +500,11 @@ item.npcMusket.name=Mosquete item.npcExcalibur.name=Excalibur key.npcInventory=Inventario de NPC role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/fr_FR.lang b/src/main/resources/assets/customnpcs/lang/fr_FR.lang index 6fc66154c..cea83f824 100644 --- a/src/main/resources/assets/customnpcs/lang/fr_FR.lang +++ b/src/main/resources/assets/customnpcs/lang/fr_FR.lang @@ -542,10 +542,11 @@ item.npcMusket.name=Mousquet item.npcExcalibur.name=Excalibur key.npcInventory=Inventaire Pnj role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/hu_HU.lang b/src/main/resources/assets/customnpcs/lang/hu_HU.lang index 63ee943af..db2fc6c38 100644 --- a/src/main/resources/assets/customnpcs/lang/hu_HU.lang +++ b/src/main/resources/assets/customnpcs/lang/hu_HU.lang @@ -542,10 +542,11 @@ item.npcMusket.name=Muskéta item.npcExcalibur.name=Excalibur key.npcInventory=NPC felszerelés role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/id_ID.lang b/src/main/resources/assets/customnpcs/lang/id_ID.lang index 2b53a9639..025320030 100644 --- a/src/main/resources/assets/customnpcs/lang/id_ID.lang +++ b/src/main/resources/assets/customnpcs/lang/id_ID.lang @@ -317,10 +317,11 @@ item.npcRuby.name=Permata Rubi role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/ko_KR.lang b/src/main/resources/assets/customnpcs/lang/ko_KR.lang index 89f7c8d7d..e3a672289 100644 --- a/src/main/resources/assets/customnpcs/lang/ko_KR.lang +++ b/src/main/resources/assets/customnpcs/lang/ko_KR.lang @@ -797,10 +797,11 @@ enchantment.poison=독 key.npcInventory=NPC 인벤토리 role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/nl_NL.lang b/src/main/resources/assets/customnpcs/lang/nl_NL.lang index acae2f273..297cfa1fc 100644 --- a/src/main/resources/assets/customnpcs/lang/nl_NL.lang +++ b/src/main/resources/assets/customnpcs/lang/nl_NL.lang @@ -232,10 +232,11 @@ item.npcNinjaHead.name=Ninja Helm item.npcNinjaChest.name=Ninja Borst item.npcNinjaPants.name=Ninja Broek role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/pl_PL.lang b/src/main/resources/assets/customnpcs/lang/pl_PL.lang index ff860e0ab..e84de64ee 100644 --- a/src/main/resources/assets/customnpcs/lang/pl_PL.lang +++ b/src/main/resources/assets/customnpcs/lang/pl_PL.lang @@ -498,10 +498,11 @@ item.npcMusket.name=Muszkiet item.npcExcalibur.name=Excalibur key.npcInventory=Inwentarz npc role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/pt_BR.lang b/src/main/resources/assets/customnpcs/lang/pt_BR.lang index 066697e8c..6d07b2df0 100644 --- a/src/main/resources/assets/customnpcs/lang/pt_BR.lang +++ b/src/main/resources/assets/customnpcs/lang/pt_BR.lang @@ -498,10 +498,11 @@ item.npcMusket.name=Mosquete item.npcExcalibur.name=Excalibur key.npcInventory=Inventário NPC role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/ru_RU.lang b/src/main/resources/assets/customnpcs/lang/ru_RU.lang index a34739f0d..2c6f76422 100644 --- a/src/main/resources/assets/customnpcs/lang/ru_RU.lang +++ b/src/main/resources/assets/customnpcs/lang/ru_RU.lang @@ -1021,10 +1021,11 @@ questgui.accept=Принять key.npcInventory=Инвентарь NPC role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/sv_SE.lang b/src/main/resources/assets/customnpcs/lang/sv_SE.lang index 70a29ebc9..7e536400b 100644 --- a/src/main/resources/assets/customnpcs/lang/sv_SE.lang +++ b/src/main/resources/assets/customnpcs/lang/sv_SE.lang @@ -308,10 +308,11 @@ item.npcPendant.name=Pendant item.npcBlueprint.name=Blueprint item.npcRing.name=Ring role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/zh_CN.lang b/src/main/resources/assets/customnpcs/lang/zh_CN.lang index 00fb8c796..32105c7db 100644 --- a/src/main/resources/assets/customnpcs/lang/zh_CN.lang +++ b/src/main/resources/assets/customnpcs/lang/zh_CN.lang @@ -1244,10 +1244,11 @@ hitbox.height=身高比例 key.npcInventory=NPC物品栏 role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key diff --git a/src/main/resources/assets/customnpcs/lang/zh_TW.lang b/src/main/resources/assets/customnpcs/lang/zh_TW.lang index a4f3d4ae5..b7f2c04f5 100644 --- a/src/main/resources/assets/customnpcs/lang/zh_TW.lang +++ b/src/main/resources/assets/customnpcs/lang/zh_TW.lang @@ -314,10 +314,11 @@ item.npcSaphire.name=藍寶石 item.npcAmethyst.name=紫水晶 item.npcRuby.name=紅寶石 role.mount=Mount -role.mount.offsetx=Seat Offset X -role.mount.offsety=Seat Offset Y -role.mount.offsetz=Seat Offset Z -role.mount.jumpheight=Jump Strength -role.mount.sprint=Allow Sprinting +role.mount.offsetx=Seat X +role.mount.offsety=Seat Y +role.mount.offsetz=Seat Z +role.mount.jumpheight=Jump +role.mount.sprint=Sprint role.mount.reset=Reset Position -role.mount.walkspeed=Walking speed: %s +role.mount.walkspeed=Walk Speed: %s +key.customnpcs.special=Special Key