mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 21:17:32 +00:00
Merge branch 'dev' into janino-scripts
This commit is contained in:
commit
3f8373d76f
48 files changed
+3333
-2035
No files matched your search
@@ -0,0 +1,834 @@
|
||||
# CustomNPC+ 1.11 Market Update - Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
The 1.11 Market Update introduces a comprehensive economy system for CustomNPC+:
|
||||
|
||||
1. **Currency System** - Built-in CNPC+ currency with optional Vault integration
|
||||
2. **Trader Enhancements** - Stock system, currency-based trades, reset timers
|
||||
3. **Auction House** - Global auction system with bidding, buyout, and claims
|
||||
|
||||
---
|
||||
|
||||
## 1. Currency System
|
||||
|
||||
### Status: IMPLEMENTED
|
||||
|
||||
The currency system provides a unified economy for CNPC+ features.
|
||||
|
||||
### Features
|
||||
- **Built-in CNPC+ Currency** - Stored in PlayerData, persists across profile slots
|
||||
- **Vault Integration** - Optional, when enabled all operations use Vault instead
|
||||
- **Scripting API** - Full access via `IPlayer` methods
|
||||
|
||||
### Key Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `PlayerCurrencyData.java` | Per-player currency storage, Vault delegation |
|
||||
| `IPlayerCurrencyData.java` | API interface for currency operations |
|
||||
| `ConfigMarket.java` | Currency configuration (UseVault, CurrencyName, StartingBalance, MaxBalance) |
|
||||
| `VaultUtil.java` | Vault API integration via reflection |
|
||||
|
||||
### Configuration (ConfigMarket.java)
|
||||
```
|
||||
Market.Currency:
|
||||
- Use Vault: false
|
||||
- Currency Name: "Coins"
|
||||
- Starting Balance: 0
|
||||
- Max Balance: 2147483647
|
||||
```
|
||||
|
||||
### Scripting API (IPlayer)
|
||||
```javascript
|
||||
player.getCurrencyBalance() // Get balance
|
||||
player.setCurrencyBalance(amount) // Set balance
|
||||
player.depositCurrency(amount) // Add currency (returns success)
|
||||
player.withdrawCurrency(amount) // Remove currency (returns success)
|
||||
player.canAffordCurrency(amount) // Check if can afford
|
||||
player.isUsingVaultCurrency() // Check if Vault is active
|
||||
player.getFormattedCurrencyBalance() // Get formatted string
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Trader Enhancements
|
||||
|
||||
### Status: COMPLETE
|
||||
|
||||
---
|
||||
|
||||
### 2.1 Stock System - IMPLEMENTED
|
||||
|
||||
Traders can have limited stock that resets on configurable schedules.
|
||||
|
||||
#### Stock Modes
|
||||
| Mode | Description | Status |
|
||||
|------|-------------|--------|
|
||||
| **Global** | All players share the same stock pool | ✅ Working |
|
||||
| **Per-Player** | Each player has their own stock counts | ✅ Working |
|
||||
|
||||
#### Stock Reset Types (EnumStockReset)
|
||||
| Type | Description | Status |
|
||||
|------|-------------|--------|
|
||||
| `NONE` | Stock never resets | ✅ Working |
|
||||
| `MCDAILY` | Resets every MC day (24000 ticks) | ✅ Working |
|
||||
| `MCWEEKLY` | Resets every 7 MC days (168000 ticks) | ✅ Working |
|
||||
| `MCCUSTOM` | Custom MC tick interval | ✅ Working |
|
||||
| `RLDAILY` | Resets every 24 real-life hours | ✅ Working |
|
||||
| `RLWEEKLY` | Resets every 7 real-life days | ✅ Working |
|
||||
| `RLCUSTOM` | Custom real-time interval | ✅ Working |
|
||||
|
||||
#### Stock UI Features - IMPLEMENTED
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Stock count per item | ✅ | Shows to right of output slot |
|
||||
| Out of stock indicator | ✅ | Dark overlay + red count |
|
||||
| Reset timer countdown | ✅ | Real-time client-side countdown in top-right |
|
||||
| "Reopen Trader" message | ✅ | Shows when countdown reaches 0 |
|
||||
| Admin stock configuration | ✅ | SubGuiNpcTraderStock |
|
||||
| Per-slot stock limits | ✅ | SubGuiNpcTraderStockSlots |
|
||||
| Reset stock button | ✅ | With confirmation dialog |
|
||||
| Reset cooldown button | ✅ | With confirmation dialog |
|
||||
|
||||
#### Key Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `TraderStock.java` | Core stock data class with per-player/global modes |
|
||||
| `TraderStock.PlayerTraderStock` | Inner class for per-player tracking |
|
||||
| `EnumStockReset.java` | Reset type enum with intervals |
|
||||
| `SubGuiNpcTraderStock.java` | Main stock configuration GUI |
|
||||
| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limit editor |
|
||||
| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation |
|
||||
| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Trader Currency Support - IMPLEMENTED
|
||||
|
||||
Trades can require BOTH items AND currency (additive costs).
|
||||
|
||||
#### Trade Entry Structure
|
||||
Each trade slot can have:
|
||||
- **Sold Item** - What the player receives ✅
|
||||
- **Currency 1** - First item cost (existing) ✅
|
||||
- **Currency 2** - Second item cost (existing) ✅
|
||||
- **Currency Cost** - CNPC+ Currency / Vault cost ✅ NEW
|
||||
|
||||
#### Example Trades
|
||||
| Sold Item | Item Cost | Currency Cost |
|
||||
|-----------|-----------|---------------|
|
||||
| Diamond | 2 Emeralds | $0 |
|
||||
| Diamond Sword | 5 Diamonds | $100 |
|
||||
| Special Armor | None | $500 |
|
||||
| Enchanted Book | 1 Book + 10 Lapis | $50 |
|
||||
|
||||
#### Implementation Details
|
||||
- Currency cost stored as `long[] currencyCost[18]` in RoleTrader
|
||||
- Default to 0 (no currency cost) for legacy compatibility ✅
|
||||
- Check both item AND currency requirements before purchase ✅
|
||||
- Withdraw currency on successful purchase ✅
|
||||
- NBT serialization for persistence ✅
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Player Trader GUI - IMPLEMENTED
|
||||
|
||||
#### Layout (256px wide)
|
||||
- 3 columns × 6 rows = 18 trade slots
|
||||
- Column width: 80px
|
||||
- Per slot: [Currency1][Currency2] = [Output]
|
||||
- Player inventory positioned below
|
||||
|
||||
#### Display Features
|
||||
| Element | Position | Status |
|
||||
|---------|----------|--------|
|
||||
| "Trader" label | Top-left | ✅ |
|
||||
| Reset countdown | Top-right | ✅ Real-time ticking |
|
||||
| Stock count | Right of each trade | ✅ Green/Red |
|
||||
| Player balance | Bottom-right | ✅ |
|
||||
| Status message | Bottom-left | ✅ On hover |
|
||||
|
||||
#### Status Messages (on hover)
|
||||
| Condition | Message | Color |
|
||||
|-----------|---------|-------|
|
||||
| Out of stock | "Out of Stock ($COST)" | Red |
|
||||
| Can't afford balance | "Insufficient Balance ($COST)" | Red |
|
||||
| Missing item currency | "Unavailable ($COST)" | Red |
|
||||
| Slot disabled | "Slot Disabled ($COST)" | Orange |
|
||||
| Can purchase | "Available ($COST)" | Green |
|
||||
|
||||
Cost suffix only appears if trade has currency cost > 0.
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Multi-Player Sync - IMPLEMENTED
|
||||
|
||||
#### MarketRegistry System
|
||||
Central registry for linked market synchronization across all NPCs sharing the same market.
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `MarketRegistry.java` | Central registry tracking traders and viewers per market |
|
||||
| `marketTraders` | `Map<String, Set<RoleTrader>>` - All traders using each market |
|
||||
| `marketViewers` | `Map<String, Set<EntityPlayerMP>>` - All players viewing each market |
|
||||
|
||||
#### Viewer Tracking System
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Register viewer on GUI open | ✅ | For shared stock mode (linked or local) |
|
||||
| Unregister on GUI close | ✅ | Via onContainerClosed |
|
||||
| Sync after purchase | ✅ | All viewers updated market-wide |
|
||||
| Per-player mode optimization | ✅ | Only sync to purchasing player |
|
||||
| Disconnected player cleanup | ✅ | Removed before sync |
|
||||
| NPC unload cleanup | ✅ | Via delete() → onUnload() |
|
||||
|
||||
#### Sync Behavior by Mode
|
||||
| Mode | Linked Market | Behavior |
|
||||
|------|---------------|----------|
|
||||
| Shared Stock | Yes | All viewers across ALL NPCs with same market see update immediately |
|
||||
| Shared Stock | No | All viewers of THIS NPC see update immediately |
|
||||
| Per-Player Stock | Yes/No | Only purchasing player receives update |
|
||||
|
||||
#### Data Flow
|
||||
```
|
||||
Purchase on NPC1 (market="shop1")
|
||||
↓
|
||||
consumeStock() → Market.save() → MarketRegistry.syncMarket("shop1")
|
||||
↓
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 1. Update ALL traders' stock data │
|
||||
│ (NPC1, NPC2, NPC3... all with "shop1") │
|
||||
│ 2. Sync ALL viewers across ALL those NPCs │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.5 Key Files Modified/Created
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `MarketRegistry.java` | NEW - Central registry for linked market sync |
|
||||
| `RoleTrader.java` | currencyCost[], viewer tracking (local + registry), syncAllViewers(), resetCooldown(), delete() cleanup |
|
||||
| `ContainerNPCTrader.java` | Layout constants, currency/stock validation, viewer registration |
|
||||
| `GuiNPCTrader.java` | Client-side countdown, stock display, status messages |
|
||||
| `GetTraderData.java` | Network packet for requesting trader data (IGuiData pattern) |
|
||||
| `TraderStock.java` | Stock management with per-player/global modes, thread-safe methods |
|
||||
| `EnumStockReset.java` | Reset type definitions with lang key support |
|
||||
| `SubGuiNpcTraderStock.java` | Stock configuration dialog, custom time validation |
|
||||
| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limits |
|
||||
| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation |
|
||||
| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation |
|
||||
| `GuiNPCInterface.java` | Fixed SubGui background tiling for smaller widths |
|
||||
| `Market.java` | Fixed save logic for linked markets |
|
||||
| `IRoleTrader.java` | Extended API with stock manipulation methods |
|
||||
| `ScriptRoleTrader.java` | Implemented new API methods |
|
||||
|
||||
---
|
||||
|
||||
### 2.6 Resolved Issues
|
||||
|
||||
All previously identified issues have been fixed:
|
||||
|
||||
#### Critical Issues - FIXED
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| Market load overwrites reset | Reordered `interact()` to load market BEFORE stock reset check |
|
||||
| recordHistory flag inverted | Removed inverted early return in `Market.save()` |
|
||||
| API returns wrong item | Fixed `getSellOption()` to return `inventorySold` instead of `inventoryCurrency` |
|
||||
|
||||
#### Medium Priority - FIXED
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| Thread safety | Added `synchronized` to `getAvailableStock()`, `consumeStock()`, `resetStock()` |
|
||||
| Per-player cleanup | `playerStock.clear()` called on stock reset |
|
||||
| Linked market sync | Implemented `MarketRegistry` for market-wide sync |
|
||||
| Custom time validation | Added min validation (1 tick MC, 1000ms RL) in `SubGuiNpcTraderStock` |
|
||||
|
||||
#### Low Priority - FIXED
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| lastPurchaseTime unused | Removed from `PlayerTraderStock` |
|
||||
| Currency format | Verified working correctly for all ranges |
|
||||
| Hardcoded reset types | Now uses `EnumStockReset.getDisplayNames()` with lang keys |
|
||||
|
||||
---
|
||||
|
||||
### 2.7 Scripting API - IMPLEMENTED
|
||||
|
||||
Full API available via `IRoleTrader` interface:
|
||||
|
||||
```javascript
|
||||
// Stock Configuration
|
||||
trader.isStockEnabled()
|
||||
trader.setStockEnabled(boolean)
|
||||
trader.isPerPlayerStock()
|
||||
trader.setPerPlayerStock(boolean)
|
||||
trader.getStockResetType() // Returns int (0-6 for EnumStockReset)
|
||||
trader.setStockResetType(int)
|
||||
trader.getCustomResetTime()
|
||||
trader.setCustomResetTime(long)
|
||||
|
||||
// Stock Limits
|
||||
trader.getMaxStock(slot) // Max stock for slot (-1 = unlimited)
|
||||
trader.setMaxStock(slot, amount)
|
||||
|
||||
// Stock Queries
|
||||
trader.getAvailableStock(slot) // Global mode
|
||||
trader.getAvailableStock(slot, player) // Per-player mode
|
||||
trader.getCurrentStock(slot) // Current stock (global)
|
||||
trader.getPlayerPurchased(slot, player) // Amount purchased by player
|
||||
|
||||
// Stock Manipulation
|
||||
trader.setCurrentStock(slot, amount) // Set current stock (global)
|
||||
trader.setPlayerPurchased(slot, player, amount) // Set player's purchased amount
|
||||
trader.resetStock() // Reset all stock to max
|
||||
trader.resetCooldown() // Reset cooldown timer
|
||||
|
||||
// Time Queries
|
||||
trader.getLastResetTime() // Timestamp of last reset
|
||||
trader.getTimeUntilReset() // Time remaining until next reset
|
||||
|
||||
// Currency Cost System
|
||||
trader.getCurrencyCost(slot)
|
||||
trader.setCurrencyCost(slot, amount)
|
||||
trader.hasCurrencyCost(slot)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Auction House System
|
||||
|
||||
### Status: NOT STARTED (Fresh implementation)
|
||||
|
||||
### 3.1 Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ AuctionController │
|
||||
│ - Manages all auction data │
|
||||
│ - Handles listing/bidding/buyout logic │
|
||||
│ - Processes claims │
|
||||
│ - Saves/loads auction data │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ AuctionNotification │
|
||||
│ - Tracks pending notifications per player │
|
||||
│ - Sends notifications on login │
|
||||
│ - Types: Won, Outbid, Sold, Expired, Claimed │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 GUI Structure
|
||||
|
||||
```
|
||||
GuiAuctionHouse (Base GUI - opened via Auctioneer NPC)
|
||||
│
|
||||
├── GuiAuction (Browse/Search auctions)
|
||||
│ - Paginated item display (ContainerAuction)
|
||||
│ - Next/Back page buttons
|
||||
│ - Filter button → opens GuiAuctionFilter
|
||||
│ - My Auctions button → opens GuiAuctionClaims
|
||||
│ - Sell button → opens GuiAuctionSell
|
||||
│ - Click item → SubGuiAuctionDetails
|
||||
│
|
||||
├── GuiAuctionSell (Create new listing)
|
||||
│ - Item slot for item to sell
|
||||
│ - Starting price input
|
||||
│ - Buyout price input (0 = no buyout)
|
||||
│ - Listing fee display
|
||||
│ - Create button
|
||||
│
|
||||
├── GuiAuctionClaims (My Auctions / Claims)
|
||||
│ - Active listings tab
|
||||
│ - Pending claims tab (items + currency to collect)
|
||||
│ - Cancel listing button
|
||||
│ - Claim all button
|
||||
│
|
||||
└── GuiAuctionFilter (Filter settings)
|
||||
- Search text
|
||||
- Sort options (EnumAuctionSort)
|
||||
- Price range (min/max)
|
||||
- Show my listings only toggle
|
||||
```
|
||||
|
||||
### 3.3 Admin GUI
|
||||
|
||||
```
|
||||
GuiManageAuction (Admin tool - like ManageMagic, ManageQuests)
|
||||
│
|
||||
├── Active Auctions Tab
|
||||
│ - View all auctions
|
||||
│ - Cancel any auction (with reason)
|
||||
│ - View auction details
|
||||
│
|
||||
├── Blacklist Tab (Stub for future)
|
||||
│ - Add/remove blacklisted items
|
||||
│ - Add/remove blacklisted mods
|
||||
│
|
||||
└── Settings Tab
|
||||
- View current config values
|
||||
- Quick reference for config file
|
||||
```
|
||||
|
||||
### 3.4 Data Structures
|
||||
|
||||
#### AuctionListing
|
||||
```java
|
||||
public class AuctionListing {
|
||||
int id; // Unique listing ID
|
||||
UUID sellerUUID; // Seller's UUID
|
||||
String sellerName; // Seller's display name
|
||||
ItemStack item; // Item being sold
|
||||
long startingPrice; // Minimum starting bid
|
||||
long buyoutPrice; // Instant buy price (0 = no buyout)
|
||||
long currentBid; // Current highest bid
|
||||
UUID highBidderUUID; // Current high bidder (null if no bids)
|
||||
String highBidderName; // High bidder display name
|
||||
int bidCount; // Number of bids placed
|
||||
long createdTime; // When listing was created
|
||||
long endTime; // When auction ends
|
||||
EnumAuctionStatus status; // ACTIVE, ENDED, CANCELLED, CLAIMED
|
||||
}
|
||||
```
|
||||
|
||||
#### AuctionClaim
|
||||
```java
|
||||
public class AuctionClaim {
|
||||
int id; // Unique claim ID
|
||||
UUID playerUUID; // Player who can claim
|
||||
int listingId; // Related auction listing
|
||||
EnumClaimType type; // ITEM, CURRENCY, REFUND
|
||||
ItemStack item; // Item to claim (if type = ITEM)
|
||||
long currency; // Currency to claim (if type = CURRENCY/REFUND)
|
||||
long createdTime; // When claim was created
|
||||
boolean claimed; // Whether claimed
|
||||
}
|
||||
```
|
||||
|
||||
#### AuctionFilter
|
||||
```java
|
||||
public class AuctionFilter {
|
||||
String searchText; // Item name search
|
||||
EnumAuctionSort sortBy; // Sort order
|
||||
long minPrice; // Minimum price filter
|
||||
long maxPrice; // Maximum price filter (0 = no max)
|
||||
boolean myListingsOnly; // Show only my listings
|
||||
}
|
||||
```
|
||||
|
||||
#### Enums
|
||||
```java
|
||||
public enum EnumAuctionStatus {
|
||||
ACTIVE, // Auction is live
|
||||
ENDED, // Auction ended (winner determined)
|
||||
CANCELLED, // Cancelled by seller or admin
|
||||
CLAIMED // All claims processed
|
||||
}
|
||||
|
||||
public enum EnumAuctionSort {
|
||||
NEWEST, // Most recently listed
|
||||
ENDING_SOON, // Ending soonest
|
||||
PRICE_LOW, // Lowest current price
|
||||
PRICE_HIGH, // Highest current price
|
||||
MOST_BIDS // Most bid activity
|
||||
}
|
||||
|
||||
public enum EnumClaimType {
|
||||
ITEM, // Winner claims item
|
||||
CURRENCY, // Seller claims sale currency
|
||||
REFUND // Outbid player claims refund
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 AuctionController
|
||||
|
||||
Main controller handling all auction logic.
|
||||
|
||||
#### Core Methods
|
||||
```java
|
||||
// Listing Management
|
||||
int createListing(EntityPlayer seller, ItemStack item, long startingPrice, long buyoutPrice)
|
||||
void cancelListing(int listingId, EntityPlayer player, boolean isAdmin)
|
||||
List<AuctionListing> getActiveListings(AuctionFilter filter, int page, int pageSize)
|
||||
AuctionListing getListing(int listingId)
|
||||
int getPlayerListingCount(UUID playerUUID)
|
||||
|
||||
// Bidding
|
||||
boolean placeBid(int listingId, EntityPlayer bidder, long bidAmount)
|
||||
boolean buyout(int listingId, EntityPlayer buyer)
|
||||
|
||||
// Claims
|
||||
List<AuctionClaim> getPlayerClaims(UUID playerUUID)
|
||||
boolean claimItem(int claimId, EntityPlayer player)
|
||||
boolean claimCurrency(int claimId, EntityPlayer player)
|
||||
void claimAll(EntityPlayer player)
|
||||
|
||||
// Lifecycle
|
||||
void processEndedAuctions() // Called on tick, creates claims for ended auctions
|
||||
void onPlayerLogin(EntityPlayer player) // Send pending notifications
|
||||
|
||||
// Persistence
|
||||
void save()
|
||||
void load()
|
||||
```
|
||||
|
||||
#### Auction Flow
|
||||
|
||||
**Creating a Listing:**
|
||||
1. Validate item not blacklisted
|
||||
2. Check player has listing slots available
|
||||
3. Deduct listing fee from player
|
||||
4. Remove item from player inventory
|
||||
5. Create AuctionListing with ACTIVE status
|
||||
6. Save
|
||||
|
||||
**Placing a Bid:**
|
||||
1. Validate auction is ACTIVE
|
||||
2. Validate bid > current bid + minimum increment
|
||||
3. Validate bidder has enough currency
|
||||
4. Deduct bid amount from bidder
|
||||
5. If previous bidder exists, create REFUND claim for them
|
||||
6. Update listing with new bid/bidder
|
||||
7. Apply snipe protection if needed
|
||||
8. Save
|
||||
|
||||
**Buyout:**
|
||||
1. Validate auction is ACTIVE and has buyout price
|
||||
2. Validate buyer has enough currency
|
||||
3. Deduct buyout amount from buyer
|
||||
4. If previous bidder exists, create REFUND claim
|
||||
5. Create ITEM claim for buyer
|
||||
6. Create CURRENCY claim for seller (minus sales tax)
|
||||
7. Set status to ENDED
|
||||
8. Save
|
||||
|
||||
**Auction Ends (no buyout):**
|
||||
1. If bids exist:
|
||||
- Create ITEM claim for high bidder
|
||||
- Create CURRENCY claim for seller (minus sales tax)
|
||||
2. If no bids:
|
||||
- Create ITEM claim for seller (item returned)
|
||||
3. Set status to ENDED
|
||||
4. Save
|
||||
|
||||
### 3.6 Snipe Protection
|
||||
|
||||
When a bid is placed with less than `SnipeProtectionMinutes` remaining:
|
||||
- Extend the auction end time TO `SnipeProtectionMinutes` from now
|
||||
- Never extend beyond `SnipeProtectionMinutes`
|
||||
- Default: 2 minutes
|
||||
|
||||
Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00
|
||||
|
||||
### 3.7 Notifications
|
||||
|
||||
#### AuctionNotification Class
|
||||
```java
|
||||
public class AuctionNotification {
|
||||
UUID playerUUID;
|
||||
EnumNotificationType type;
|
||||
int listingId;
|
||||
String message;
|
||||
long timestamp;
|
||||
}
|
||||
|
||||
public enum EnumNotificationType {
|
||||
AUCTION_WON, // You won an auction
|
||||
AUCTION_OUTBID, // You were outbid
|
||||
AUCTION_SOLD, // Your item sold
|
||||
AUCTION_EXPIRED, // Your auction expired (no bids)
|
||||
CLAIM_READY // You have items/currency to claim
|
||||
}
|
||||
```
|
||||
|
||||
Notifications are queued and sent when player logs in.
|
||||
|
||||
### 3.8 Permissions
|
||||
|
||||
| Permission | Default | Description |
|
||||
|------------|---------|-------------|
|
||||
| `customnpcs.auction.use` | true | Can use auction house |
|
||||
| `customnpcs.auction.sell` | true | Can create listings |
|
||||
| `customnpcs.auction.slots.X` | 5 | Max listing slots (X = number) |
|
||||
| `customnpcs.auction.admin` | op | Admin auction management |
|
||||
|
||||
---
|
||||
|
||||
## 4. Configuration
|
||||
|
||||
### ConfigMarket.java Structure
|
||||
|
||||
```java
|
||||
// =========================================
|
||||
// Market General
|
||||
// =========================================
|
||||
public static boolean MarketEnabled = true;
|
||||
|
||||
// =========================================
|
||||
// Currency (Market.Currency)
|
||||
// =========================================
|
||||
public static boolean UseVault = false;
|
||||
public static String CurrencyName = "Coins";
|
||||
public static long StartingBalance = 0;
|
||||
public static long MaxBalance = Long.MAX_VALUE;
|
||||
|
||||
// =========================================
|
||||
// Trader (Market.Trader)
|
||||
// =========================================
|
||||
public static boolean EnableStockByDefault = false;
|
||||
public static String DefaultResetType = "NONE";
|
||||
|
||||
// =========================================
|
||||
// Auction (Market.Auction)
|
||||
// =========================================
|
||||
public static boolean AuctionEnabled = true;
|
||||
public static int AuctionDuration = 24; // Hours
|
||||
public static long ListingFee = 10; // Flat fee to create listing
|
||||
public static double SalesTaxPercent = 0.05; // 5% tax on sales
|
||||
public static int DefaultMaxListings = 5; // Per player
|
||||
public static int SnipeProtectionMinutes = 2; // Snipe protection timer
|
||||
public static int ClaimExpirationDays = 30; // Days before unclaimed items deleted
|
||||
public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. File Structure
|
||||
|
||||
### New Files to Create
|
||||
|
||||
```
|
||||
constants/
|
||||
EnumAuctionStatus.java
|
||||
EnumAuctionSort.java
|
||||
EnumClaimType.java
|
||||
EnumNotificationType.java
|
||||
|
||||
controllers/
|
||||
AuctionController.java
|
||||
AuctionNotification.java
|
||||
|
||||
controllers/data/
|
||||
AuctionListing.java
|
||||
AuctionClaim.java
|
||||
AuctionFilter.java
|
||||
|
||||
roles/
|
||||
RoleAuctioneer.java
|
||||
|
||||
containers/
|
||||
ContainerAuction.java
|
||||
|
||||
client/gui/player/
|
||||
GuiAuctionHouse.java (Base GUI)
|
||||
GuiAuction.java (Browse auctions)
|
||||
GuiAuctionSell.java (Create listing)
|
||||
GuiAuctionClaims.java (My auctions/claims)
|
||||
GuiAuctionFilter.java (Filter settings)
|
||||
SubGuiAuctionDetails.java (View listing details)
|
||||
|
||||
client/gui/global/
|
||||
GuiManageAuction.java (Admin management)
|
||||
|
||||
network/packets/
|
||||
AuctionDataPacket.java (Server → Client data sync)
|
||||
AuctionActionPacket.java (Client → Server actions)
|
||||
```
|
||||
|
||||
### Files to Modify
|
||||
|
||||
```
|
||||
CustomNpcs.java - Initialize AuctionController
|
||||
ServerTickHandler.java - Process ended auctions, login notifications
|
||||
EnumRoleType.java - Add Auctioneer
|
||||
EnumGuiType.java - Add auction GUI types
|
||||
DataAdvanced.java - Handle Auctioneer role
|
||||
PacketHandler.java - Register auction packets
|
||||
CustomNpcsPermissions.java - Auction permissions
|
||||
ConfigMarket.java - Add Auction section
|
||||
RoleTrader.java - Add currency cost per trade
|
||||
ContainerNPCTrader.java - Currency + stock integration
|
||||
en_US.lang - All auction lang keys
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Data Storage
|
||||
|
||||
### Auction Data File
|
||||
Location: `customnpcs/auction.dat`
|
||||
|
||||
Structure:
|
||||
```
|
||||
NBTTagCompound root
|
||||
├── NextListingId: int
|
||||
├── NextClaimId: int
|
||||
├── Listings: NBTTagList
|
||||
│ └── [AuctionListing NBT...]
|
||||
├── Claims: NBTTagList
|
||||
│ └── [AuctionClaim NBT...]
|
||||
└── PendingNotifications: NBTTagList
|
||||
└── [AuctionNotification NBT...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Lang Keys
|
||||
|
||||
```properties
|
||||
# Auction House
|
||||
auction.title=Auction House
|
||||
auction.browse=Browse
|
||||
auction.sell=Sell
|
||||
auction.claims=My Auctions
|
||||
auction.filter=Filter
|
||||
|
||||
# Browse
|
||||
auction.nextPage=Next >
|
||||
auction.prevPage=< Prev
|
||||
auction.page=Page %d of %d
|
||||
auction.noResults=No auctions found
|
||||
auction.refresh=Refresh
|
||||
|
||||
# Listing Display
|
||||
auction.seller=Seller: %s
|
||||
auction.currentBid=Current Bid: %s
|
||||
auction.buyout=Buyout: %s
|
||||
auction.timeLeft=Time Left: %s
|
||||
auction.bids=%d bids
|
||||
auction.noBids=No bids
|
||||
|
||||
# Create Listing
|
||||
auction.sell.title=Create Listing
|
||||
auction.sell.startingPrice=Starting Price
|
||||
auction.sell.buyoutPrice=Buyout Price
|
||||
auction.sell.noBuyout=(0 = no buyout)
|
||||
auction.sell.fee=Listing Fee: %s
|
||||
auction.sell.create=Create Listing
|
||||
auction.sell.noItem=Place an item to sell
|
||||
|
||||
# Claims
|
||||
auction.claims.title=My Auctions
|
||||
auction.claims.active=Active Listings
|
||||
auction.claims.pending=Pending Claims
|
||||
auction.claims.claimAll=Claim All
|
||||
auction.claims.cancel=Cancel Listing
|
||||
auction.claims.empty=No pending claims
|
||||
|
||||
# Filter
|
||||
auction.filter.title=Filter Auctions
|
||||
auction.filter.search=Search
|
||||
auction.filter.minPrice=Min Price
|
||||
auction.filter.maxPrice=Max Price
|
||||
auction.filter.myOnly=My Listings Only
|
||||
auction.filter.apply=Apply
|
||||
|
||||
# Sort Options
|
||||
auction.sort.newest=Newest
|
||||
auction.sort.endingSoon=Ending Soon
|
||||
auction.sort.priceLow=Price: Low to High
|
||||
auction.sort.priceHigh=Price: High to Low
|
||||
auction.sort.mostBids=Most Bids
|
||||
|
||||
# Notifications
|
||||
auction.notify.won=You won the auction for %s!
|
||||
auction.notify.outbid=You were outbid on %s!
|
||||
auction.notify.sold=Your %s sold for %s!
|
||||
auction.notify.expired=Your auction for %s expired with no bids.
|
||||
auction.notify.claim=You have items to claim at the Auction House.
|
||||
|
||||
# Errors
|
||||
auction.error.notEnoughCurrency=Not enough currency!
|
||||
auction.error.auctionEnded=This auction has ended.
|
||||
auction.error.bidTooLow=Bid must be at least %s.
|
||||
auction.error.maxListings=You have reached your maximum listings.
|
||||
auction.error.blacklisted=This item cannot be auctioned.
|
||||
auction.error.noAuctioneer=You must visit an Auctioneer NPC.
|
||||
|
||||
# Trader Currency
|
||||
trader.currencyCost=Currency Cost
|
||||
trader.outOfStock=Out of Stock
|
||||
trader.stock=Stock: %d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Order
|
||||
|
||||
### Phase 1: Trader Currency Integration - COMPLETE ✅
|
||||
1. ~~Add `currencyCost` field to RoleTrader trades~~
|
||||
2. ~~Update ContainerNPCTrader to check/deduct currency~~
|
||||
3. ~~Update trader setup GUI with currency input~~
|
||||
4. ~~Test with existing item trades (backward compatibility)~~
|
||||
|
||||
### Phase 2: Trader Stock UI - COMPLETE ✅
|
||||
1. ~~Add stock display to player trader GUI~~
|
||||
2. ~~Show "Out of Stock" indicator~~
|
||||
3. ~~Verify reset timers working~~
|
||||
4. ~~Real-time client-side countdown~~
|
||||
5. ~~Status messages with costs~~
|
||||
6. ~~Reset cooldown button with confirmation~~
|
||||
|
||||
### Phase 2.5: Trader Sync System - COMPLETE ✅
|
||||
1. ~~Implement MarketRegistry for linked market sync~~
|
||||
2. ~~Viewer tracking (market-wide and local)~~
|
||||
3. ~~NPC unload cleanup~~
|
||||
4. ~~Thread safety for stock operations~~
|
||||
|
||||
### Phase 3: Auction Core
|
||||
1. Create enums (EnumAuctionStatus, EnumAuctionSort, etc.)
|
||||
2. Create data classes (AuctionListing, AuctionClaim, AuctionFilter)
|
||||
3. Create AuctionController with core logic
|
||||
4. Create RoleAuctioneer
|
||||
5. Add to CustomNpcs initialization
|
||||
|
||||
### Phase 4: Auction GUI
|
||||
1. Create GuiAuctionHouse base
|
||||
2. Create GuiAuction (browse)
|
||||
3. Create GuiAuctionSell
|
||||
4. Create GuiAuctionClaims
|
||||
5. Create GuiAuctionFilter
|
||||
6. Create SubGuiAuctionDetails
|
||||
7. Create ContainerAuction
|
||||
|
||||
### Phase 5: Auction Network
|
||||
1. Create AuctionDataPacket
|
||||
2. Create AuctionActionPacket
|
||||
3. Register packets
|
||||
|
||||
### Phase 6: Admin & Polish
|
||||
1. Create GuiManageAuction
|
||||
2. Add permissions
|
||||
3. Add notifications system
|
||||
4. Add lang keys
|
||||
5. Testing
|
||||
|
||||
---
|
||||
|
||||
## 9. Design Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Claims System | Everything to claims | Prevents offline handling issues |
|
||||
| Auction Duration | Single config value | Simplicity over flexibility |
|
||||
| Snipe Protection | Reset to X minutes | Fair bidding, prevents last-second sniping |
|
||||
| Listing Fees | Flat fee only | Simple, predictable |
|
||||
| Sales Tax | Percentage of sale | Scales with item value |
|
||||
| Data Storage | Single .dat file | Consistent with quests/dialogs |
|
||||
| Blacklist | Stub for now | Complex feature for later |
|
||||
| Currency per Trade | Additive with items | Backward compatible, flexible |
|
||||
|
||||
---
|
||||
|
||||
## 10. Future Considerations (Not in 1.11)
|
||||
|
||||
- Category-based filtering
|
||||
- Advanced blacklist system (items, mods, NBT patterns)
|
||||
- Auction history/statistics
|
||||
- Command-based auction access
|
||||
- Auction house tax sink
|
||||
- Reserve prices
|
||||
- Private auctions
|
||||
File diff suppressed because it is too large.
Load diff
+1
-1
Submodule src/api updated: 866be1fef5...339e0f164e.
@@ -31,9 +31,6 @@ public class Market {
|
||||
NBTTagCompound roleCompound = r.writeNBT(new NBTTagCompound());
|
||||
roleCompound.setString("SaveName", name);
|
||||
putMarketCache(name, roleCompound);
|
||||
if (r.recordHistory)
|
||||
return;
|
||||
|
||||
saveFile(roleCompound);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ import kamkeel.npcs.network.packets.player.CompanionActionPacket;
|
||||
import kamkeel.npcs.network.packets.player.DialogSelectPacket;
|
||||
import kamkeel.npcs.network.packets.player.FollowerPacket;
|
||||
import kamkeel.npcs.network.packets.player.GetNPCRole;
|
||||
import kamkeel.npcs.network.packets.player.GetTraderData;
|
||||
import kamkeel.npcs.network.packets.player.InputDevicePacket;
|
||||
import kamkeel.npcs.network.packets.player.MailActionPacket;
|
||||
import kamkeel.npcs.network.packets.player.SaveBookPacket;
|
||||
@@ -579,6 +580,7 @@ public class PacketHandler {
|
||||
PLAYER_PACKET.registerPacket(new CompanionActionPacket());
|
||||
|
||||
PLAYER_PACKET.registerPacket(new GetNPCRole());
|
||||
PLAYER_PACKET.registerPacket(new GetTraderData());
|
||||
|
||||
PLAYER_PACKET.registerPacket(new InputDevicePacket());
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ public enum EnumPlayerPacket {
|
||||
SaveBook,
|
||||
CompanionAction,
|
||||
GetRole,
|
||||
GetTraderData,
|
||||
InputDevice,
|
||||
SpecialKeyState,
|
||||
ScreenSize,
|
||||
@@ -33,4 +34,7 @@ public enum EnumPlayerPacket {
|
||||
ProfileGet,
|
||||
ProfileGetInfo,
|
||||
SyncRevisionInfo,
|
||||
|
||||
// Auction
|
||||
AuctionAction,
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package kamkeel.npcs.network.packets.player;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.network.AbstractPacket;
|
||||
import kamkeel.npcs.network.PacketChannel;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.enums.EnumPlayerPacket;
|
||||
import kamkeel.npcs.network.packets.data.large.GuiDataPacket;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.controllers.data.PlayerData;
|
||||
import noppes.npcs.roles.RoleTrader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Client requests trader-specific GUI data from server.
|
||||
* Server responds with player balance, stock info, currency costs via GuiDataPacket.
|
||||
*/
|
||||
public class GetTraderData extends AbstractPacket {
|
||||
public static final String packetName = "Player|GetTraderData";
|
||||
|
||||
@Override
|
||||
public Enum getType() {
|
||||
return EnumPlayerPacket.GetTraderData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketChannel getChannel() {
|
||||
return PacketHandler.PLAYER_PACKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsNPC() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
// No data needed for request
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
|
||||
if (!(player instanceof EntityPlayerMP))
|
||||
return;
|
||||
if (!(npc.roleInterface instanceof RoleTrader))
|
||||
return;
|
||||
|
||||
RoleTrader role = (RoleTrader) npc.roleInterface;
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
|
||||
// Player balance
|
||||
PlayerData data = PlayerData.get(player);
|
||||
compound.setLong("Balance", data.currencyData.getBalance());
|
||||
|
||||
// Stock info
|
||||
compound.setBoolean("StockEnabled", role.stock.enableStock);
|
||||
compound.setLong("ResetTimeMillis", role.getResetTimeRemainingMillis());
|
||||
|
||||
// Available stock per slot (player-specific if perPlayer mode)
|
||||
String playerName = player.getCommandSenderName();
|
||||
int[] stock = new int[18];
|
||||
for (int i = 0; i < 18; i++) {
|
||||
stock[i] = role.getAvailableStock(i, playerName);
|
||||
}
|
||||
compound.setIntArray("Stock", stock);
|
||||
|
||||
// Currency cost per slot
|
||||
for (int i = 0; i < 18; i++) {
|
||||
compound.setLong("Cost" + i, role.getCurrencyCost(i));
|
||||
}
|
||||
|
||||
GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound);
|
||||
}
|
||||
}
|
||||
@@ -384,6 +384,7 @@ public class NoppesUtilServer {
|
||||
if (gui == EnumGuiType.PlayerFollower || gui == EnumGuiType.PlayerFollowerHire || gui == EnumGuiType.PlayerTrader || gui == EnumGuiType.PlayerTransporter) {
|
||||
sendRoleData(player, npc);
|
||||
}
|
||||
// Note: Trader-specific data (balance, stock, costs) is requested by client via GetTraderData packet
|
||||
}
|
||||
|
||||
private static ArrayList<String> getScrollData(EntityPlayer player, EnumGuiType gui, EntityNPCInterface npc) {
|
||||
|
||||
@@ -144,20 +144,20 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// TOP TABS using GuiMenuTopButton
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "ability.tab.general");
|
||||
GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "menu.general");
|
||||
generalTab.active = (activeTab == TAB_GENERAL);
|
||||
addTopButton(generalTab);
|
||||
|
||||
GuiMenuTopButton lastTab = generalTab;
|
||||
|
||||
if (hasTypeSettings) {
|
||||
GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "ability.tab.type");
|
||||
GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "gui.type");
|
||||
typeTab.active = (activeTab == TAB_TYPE);
|
||||
addTopButton(typeTab);
|
||||
lastTab = typeTab;
|
||||
}
|
||||
|
||||
GuiMenuTopButton targetTab = new GuiMenuTopButton(92, lastTab, "ability.tab.target");
|
||||
GuiMenuTopButton targetTab = new GuiMenuTopButton(92, lastTab, "script.target");
|
||||
targetTab.active = (activeTab == TAB_TARGET);
|
||||
addTopButton(targetTab);
|
||||
lastTab = targetTab;
|
||||
@@ -435,7 +435,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// SOUNDS SECTION
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
addLabel(new GuiNpcLabel(30, "ability.sounds", col1LabelX, y));
|
||||
addLabel(new GuiNpcLabel(30, "advanced.sounds", col1LabelX, y));
|
||||
y += 14;
|
||||
|
||||
// Wind Up Sound
|
||||
@@ -455,7 +455,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// ANIMATIONS SECTION
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
addLabel(new GuiNpcLabel(33, "ability.animations", col1LabelX, y));
|
||||
addLabel(new GuiNpcLabel(33, "menu.animations", col1LabelX, y));
|
||||
y += 14;
|
||||
|
||||
// Wind Up Animation
|
||||
@@ -486,7 +486,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
|
||||
// Telegraph Type (separate column)
|
||||
String typeKey = ability.getTelegraphType().name().toLowerCase();
|
||||
addLabel(new GuiNpcLabel(42, "ability.telegraphType", guiLeft + 200, y + 5));
|
||||
addLabel(new GuiNpcLabel(43, "ability.telegraph." + typeKey, guiLeft + 280, y + 5));
|
||||
String telegraphLangKey = typeKey.equals("none") ? "gui.none" : "telegraph." + typeKey;
|
||||
addLabel(new GuiNpcLabel(43, telegraphLangKey, guiLeft + 280, y + 5));
|
||||
|
||||
y += 24;
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ public class SubGuiAbilityBeam extends SubGuiAbilityConfig {
|
||||
int col2FieldX = guiLeft + 205;
|
||||
|
||||
// Row 1: Damage + Damage Interval
|
||||
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5));
|
||||
addTextField(createFloatField(100, fieldX, y, 50, beam.getDamage()));
|
||||
|
||||
addLabel(new GuiNpcLabel(101, "ability.dmgInterval", col2LabelX, y + 5));
|
||||
|
||||
@@ -27,10 +27,10 @@ public class SubGuiAbilityCharge extends SubGuiAbilityConfig {
|
||||
int col2FieldX = guiLeft + 205;
|
||||
|
||||
// Row 1: Damage + Speed
|
||||
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5));
|
||||
addTextField(createFloatField(100, fieldX, y, 50, charge.getDamage()));
|
||||
|
||||
addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5));
|
||||
addTextField(createFloatField(101, col2FieldX, y, 50, charge.getChargeSpeed()));
|
||||
|
||||
y += 24;
|
||||
|
||||
@@ -28,10 +28,10 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig {
|
||||
int col2FieldX = guiLeft + 205;
|
||||
|
||||
// Row 1: Damage + Range
|
||||
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5));
|
||||
addTextField(createFloatField(100, fieldX, y, 50, cutter.getDamage()));
|
||||
|
||||
addLabel(new GuiNpcLabel(101, "ability.range", col2LabelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(101, "gui.range", col2LabelX, y + 5));
|
||||
addTextField(createFloatField(101, col2FieldX, y, 50, cutter.getRange()));
|
||||
|
||||
y += 24;
|
||||
|
||||
@@ -28,7 +28,7 @@ public class SubGuiAbilityDash extends SubGuiAbilityConfig {
|
||||
int col2FieldX = guiLeft + 205;
|
||||
|
||||
// Row 1: Dash Mode
|
||||
addLabel(new GuiNpcLabel(100, "ability.dashMode", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5));
|
||||
String[] dashModes = {"Aggressive", "Defensive"};
|
||||
addButton(new GuiNpcButton(100, fieldX, y, 80, 20, dashModes, dash.getDashMode().ordinal()));
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig {
|
||||
addLabel(new GuiNpcLabel(102, "ability.healRadius", labelX, y + 5));
|
||||
addTextField(createFloatField(102, fieldX, y, 50, heal.getHealRadius()));
|
||||
|
||||
addLabel(new GuiNpcLabel(103, "ability.instant", col2LabelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(103, "dialog.instant", col2LabelX, y + 5));
|
||||
addButton(new GuiNpcButton(103, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0));
|
||||
|
||||
y += 24;
|
||||
|
||||
@@ -27,7 +27,7 @@ public class SubGuiAbilityHeavyHit extends SubGuiAbilityConfig {
|
||||
int col2FieldX = guiLeft + 205;
|
||||
|
||||
// Row 1: Damage + Potion Time
|
||||
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5));
|
||||
addTextField(createFloatField(100, fieldX, y, 50, heavyHit.getDamage()));
|
||||
|
||||
addLabel(new GuiNpcLabel(101, "ability.potionTime", col2LabelX, y + 5));
|
||||
|
||||
@@ -28,16 +28,16 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig {
|
||||
int col2FieldX = guiLeft + 205;
|
||||
|
||||
// Row 1: Damage + Speed
|
||||
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5));
|
||||
addTextField(createFloatField(100, fieldX, y, 50, orb.getDamage()));
|
||||
|
||||
addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5));
|
||||
addTextField(createFloatField(101, col2FieldX, y, 50, orb.getOrbSpeed()));
|
||||
|
||||
y += 24;
|
||||
|
||||
// Row 2: Orb Size + Max Distance
|
||||
addLabel(new GuiNpcLabel(102, "ability.orbSize", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(102, "stats.size", labelX, y + 5));
|
||||
addTextField(createFloatField(102, fieldX, y, 50, orb.getOrbSize()));
|
||||
|
||||
addLabel(new GuiNpcLabel(103, "ability.maxDist", col2LabelX, y + 5));
|
||||
|
||||
@@ -28,10 +28,10 @@ public class SubGuiAbilityProjectile extends SubGuiAbilityConfig {
|
||||
int col2FieldX = guiLeft + 205;
|
||||
|
||||
// Row 1: Damage + Speed
|
||||
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5));
|
||||
addTextField(createFloatField(100, fieldX, y, 50, projectile.getDamage()));
|
||||
|
||||
addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5));
|
||||
addTextField(createFloatField(101, col2FieldX, y, 50, projectile.getSpeed()));
|
||||
|
||||
y += 24;
|
||||
|
||||
@@ -27,7 +27,7 @@ public class SubGuiAbilityShockwave extends SubGuiAbilityConfig {
|
||||
int col2FieldX = guiLeft + 205;
|
||||
|
||||
// Row 1: Damage + Push Radius
|
||||
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5));
|
||||
addTextField(createFloatField(100, fieldX, y, 50, shockwave.getDamage()));
|
||||
|
||||
addLabel(new GuiNpcLabel(101, "ability.pushRadius", col2LabelX, y + 5));
|
||||
|
||||
@@ -27,7 +27,7 @@ public class SubGuiAbilitySlam extends SubGuiAbilityConfig {
|
||||
int col2FieldX = guiLeft + 205;
|
||||
|
||||
// Row 1: Damage + Radius
|
||||
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5));
|
||||
addTextField(createFloatField(100, fieldX, y, 50, slam.getDamage()));
|
||||
|
||||
addLabel(new GuiNpcLabel(101, "ability.radius", col2LabelX, y + 5));
|
||||
|
||||
@@ -71,7 +71,7 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig {
|
||||
}
|
||||
|
||||
// Damage + Damage Radius
|
||||
addLabel(new GuiNpcLabel(106, "ability.damage", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(106, "enchantment.damage", labelX, y + 5));
|
||||
addTextField(createFloatField(106, fieldX, y, 50, teleport.getDamage()));
|
||||
|
||||
addLabel(new GuiNpcLabel(107, "ability.dmgRadius", col2LabelX, y + 5));
|
||||
|
||||
@@ -38,7 +38,7 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig {
|
||||
y += 24;
|
||||
|
||||
// Row 2: Damage + Damage Radius
|
||||
addLabel(new GuiNpcLabel(102, "ability.damage", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5));
|
||||
addTextField(createFloatField(102, fieldX, y, 50, trap.getDamage()));
|
||||
|
||||
addLabel(new GuiNpcLabel(103, "ability.dmgRadius", col2LabelX, y + 5));
|
||||
|
||||
@@ -37,7 +37,7 @@ public class SubGuiAbilityVortex extends SubGuiAbilityConfig {
|
||||
y += 24;
|
||||
|
||||
// Row 2: Damage + Knockback
|
||||
addLabel(new GuiNpcLabel(102, "ability.damage", labelX, y + 5));
|
||||
addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5));
|
||||
addTextField(createFloatField(102, fieldX, y, 50, vortex.getDamage()));
|
||||
|
||||
addLabel(new GuiNpcLabel(103, "ability.knockback", col2LabelX, y + 5));
|
||||
|
||||
@@ -49,7 +49,7 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData {
|
||||
super.initGui();
|
||||
int y = guiTop + 10;
|
||||
this.addButton(new GuiNpcButton(3, guiLeft + 85 + 160, y, 52, 20, "selectServer.edit"));
|
||||
this.addButton(new GuiNpcButton(8, guiLeft + 85, y, 155, 20, new String[]{"role.none", "role.trader", "role.follower", "role.bank", "role.transporter", "role.mailman", NoppesStringUtils.translate("role.companion", "(WIP)"), "role.mount"}, npc.advanced.role.ordinal()));
|
||||
this.addButton(new GuiNpcButton(8, guiLeft + 85, y, 155, 20, new String[]{"role.none", "role.trader", "role.follower", "role.bank", "role.transporter", "role.mailman", NoppesStringUtils.translate("role.companion", "(WIP)"), "role.mount", "role.auctioneer"}, npc.advanced.role.ordinal()));
|
||||
getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman);
|
||||
|
||||
this.addButton(new GuiNpcButton(4, guiLeft + 85 + 160, y += 22, 52, 20, "selectServer.edit"));
|
||||
|
||||
@@ -1,136 +1,335 @@
|
||||
package noppes.npcs.client.gui.player;
|
||||
|
||||
import kamkeel.npcs.network.PacketClient;
|
||||
import kamkeel.npcs.network.packets.player.GetTraderData;
|
||||
import net.minecraft.client.renderer.RenderHelper;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import noppes.npcs.NoppesUtilPlayer;
|
||||
import noppes.npcs.client.CustomNpcResourceListener;
|
||||
import noppes.npcs.client.gui.util.GuiContainerNPCInterface;
|
||||
import noppes.npcs.client.gui.util.IGuiData;
|
||||
import noppes.npcs.containers.ContainerNPCTrader;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import noppes.npcs.roles.RoleTrader;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL12;
|
||||
|
||||
public class GuiNPCTrader extends GuiContainerNPCInterface {
|
||||
private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/trader.png");
|
||||
public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData {
|
||||
private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/tradersetup.png");
|
||||
private final ResourceLocation slot = new ResourceLocation("customnpcs", "textures/gui/slot.png");
|
||||
private RoleTrader role;
|
||||
private ContainerNPCTrader container;
|
||||
|
||||
// Layout constants matching ContainerNPCTrader
|
||||
private static final int COLUMN_WIDTH = ContainerNPCTrader.COLUMN_WIDTH;
|
||||
private static final int COLUMN_START_X = ContainerNPCTrader.COLUMN_START_X;
|
||||
private static final int ROW_HEIGHT = ContainerNPCTrader.ROW_HEIGHT;
|
||||
private static final int ROW_START_Y = ContainerNPCTrader.ROW_START_Y;
|
||||
private static final int CURRENCY1_OFFSET = ContainerNPCTrader.CURRENCY1_OFFSET;
|
||||
private static final int CURRENCY2_OFFSET = ContainerNPCTrader.CURRENCY2_OFFSET;
|
||||
private static final int OUTPUT_OFFSET = ContainerNPCTrader.OUTPUT_OFFSET;
|
||||
|
||||
// Data received from server via IGuiData
|
||||
private long playerBalance = 0;
|
||||
private int[] availableStock = new int[18];
|
||||
private long[] currencyCost = new long[18];
|
||||
private boolean stockEnabled = false;
|
||||
|
||||
// Client-side countdown: target time when stock resets (System.currentTimeMillis())
|
||||
private long resetTargetTime = -1;
|
||||
|
||||
// Current hover status for bottom display
|
||||
private String statusMessage = "";
|
||||
private int statusColor = 0xFFFFFF;
|
||||
|
||||
public GuiNPCTrader(EntityNPCInterface npc, ContainerNPCTrader container) {
|
||||
super(npc, container);
|
||||
this.container = container;
|
||||
role = (RoleTrader) npc.roleInterface;
|
||||
closeOnEsc = true;
|
||||
ySize = 224;
|
||||
xSize = 223;
|
||||
this.title = "role.trader";
|
||||
ySize = 216;
|
||||
xSize = 256;
|
||||
this.title = "";
|
||||
|
||||
// Initialize stock to unlimited
|
||||
for (int i = 0; i < 18; i++) {
|
||||
availableStock[i] = Integer.MAX_VALUE;
|
||||
currencyCost[i] = 0;
|
||||
}
|
||||
|
||||
// Request trader data from server (balance, stock, currency costs)
|
||||
PacketClient.sendClient(new GetTraderData());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawGuiContainerBackgroundLayer(float f, int i, int j) {
|
||||
public void setGuiData(NBTTagCompound compound) {
|
||||
playerBalance = compound.getLong("Balance");
|
||||
stockEnabled = compound.getBoolean("StockEnabled");
|
||||
|
||||
// Calculate target time for client-side countdown
|
||||
long remainingMillis = compound.getLong("ResetTimeMillis");
|
||||
if (remainingMillis > 0) {
|
||||
resetTargetTime = System.currentTimeMillis() + remainingMillis;
|
||||
} else {
|
||||
resetTargetTime = -1;
|
||||
}
|
||||
|
||||
int[] stock = compound.getIntArray("Stock");
|
||||
if (stock != null && stock.length == 18) {
|
||||
availableStock = stock;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 18; i++) {
|
||||
currencyCost[i] = compound.getLong("Cost" + i);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawGuiContainerBackgroundLayer(float f, int mouseX, int mouseY) {
|
||||
// Reset status each frame
|
||||
statusMessage = "";
|
||||
statusColor = 0xFFFFFF;
|
||||
|
||||
this.drawWorldBackground(0);
|
||||
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
mc.renderEngine.bindTexture(resource);
|
||||
drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize);
|
||||
|
||||
// Draw "Trader" label in TOP LEFT (above the GUI)
|
||||
String traderLabel = StatCollector.translateToLocal("role.trader");
|
||||
fontRendererObj.drawString(traderLabel, guiLeft + 4, guiTop - 8, 0xFFFFFF);
|
||||
|
||||
// Draw reset timer in TOP RIGHT (above the GUI)
|
||||
if (stockEnabled && resetTargetTime > 0) {
|
||||
String timerText = getResetCountdownText();
|
||||
int timerWidth = fontRendererObj.getStringWidth(timerText);
|
||||
int timerColor = timerText.equals(StatCollector.translateToLocal("trader.reopentrader")) ? 0xFFAA00 : 0xFFFFFF;
|
||||
fontRendererObj.drawString(timerText, guiLeft + xSize - timerWidth - 4, guiTop - 8, timerColor);
|
||||
}
|
||||
|
||||
RenderHelper.enableGUIStandardItemLighting();
|
||||
GL11.glEnable(GL12.GL_RESCALE_NORMAL);
|
||||
GL11.glEnable(GL11.GL_COLOR_MATERIAL);
|
||||
GL11.glEnable(GL11.GL_LIGHTING);
|
||||
|
||||
mc.renderEngine.bindTexture(slot);
|
||||
for (int slot = 0; slot < 18; slot++) {
|
||||
int x = guiLeft + slot % 3 * 72 + 10;
|
||||
int y = guiTop + slot / 3 * 21 + 6;
|
||||
// Draw trade slots (3 columns x 6 rows, 80px column width)
|
||||
for (int slotIdx = 0; slotIdx < 18; slotIdx++) {
|
||||
int col = slotIdx % 3;
|
||||
int row = slotIdx / 3;
|
||||
int x = guiLeft + COLUMN_START_X + col * COLUMN_WIDTH;
|
||||
int y = guiTop + ROW_START_Y + row * ROW_HEIGHT;
|
||||
|
||||
ItemStack item = role.inventoryCurrency.items.get(slot);
|
||||
ItemStack item2 = role.inventoryCurrency.items.get(slot + 18);
|
||||
// Get item currencies
|
||||
ItemStack item = role.inventoryCurrency.items.get(slotIdx);
|
||||
ItemStack item2 = role.inventoryCurrency.items.get(slotIdx + 18);
|
||||
if (item == null) {
|
||||
item = item2;
|
||||
item2 = null;
|
||||
}
|
||||
if (NoppesUtilPlayer.compareItems(item, item2, false, false)) {
|
||||
if (item != null && item2 != null && NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) {
|
||||
item = item.copy();
|
||||
item.stackSize += item2.stackSize;
|
||||
item2 = null;
|
||||
}
|
||||
|
||||
ItemStack sold = role.inventorySold.items.get(slot);
|
||||
ItemStack sold = role.inventorySold.items.get(slotIdx);
|
||||
|
||||
// Draw slot backgrounds (18x18 each)
|
||||
GL11.glColor4f(1, 1, 1, 1);
|
||||
mc.renderEngine.bindTexture(this.slot);
|
||||
drawTexturedModalRect(x + 42, y, 0, 0, 18, 18);
|
||||
if (item != null && sold != null) {
|
||||
|
||||
// Currency slot 1
|
||||
drawTexturedModalRect(x + CURRENCY1_OFFSET, y, 0, 0, 18, 18);
|
||||
// Currency slot 2
|
||||
drawTexturedModalRect(x + CURRENCY2_OFFSET, y, 0, 0, 18, 18);
|
||||
// Output slot
|
||||
drawTexturedModalRect(x + OUTPUT_OFFSET, y, 0, 0, 18, 18);
|
||||
|
||||
// "=" sign between currency and output
|
||||
fontRendererObj.drawString("=", x + 40, y + 5, CustomNpcResourceListener.DefaultTextColor);
|
||||
|
||||
// Draw currency items (centered: +1 offset for 16x16 item in 18x18 slot)
|
||||
if (sold != null) {
|
||||
RenderHelper.enableGUIStandardItemLighting();
|
||||
|
||||
// Draw secondary currency (slot 1)
|
||||
if (item2 != null) {
|
||||
itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item2, x, y + 1);
|
||||
itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item2, x, y + 1);
|
||||
itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item2, x + CURRENCY1_OFFSET + 1, y + 1);
|
||||
itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item2, x + CURRENCY1_OFFSET + 1, y + 1);
|
||||
}
|
||||
GL11.glColor4f(1, 1, 1, 1);
|
||||
itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item, x + 18, y + 1);
|
||||
itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item, x + 18, y + 1);
|
||||
|
||||
// Draw primary currency (slot 2)
|
||||
if (item != null) {
|
||||
GL11.glColor4f(1, 1, 1, 1);
|
||||
itemRender.renderItemAndEffectIntoGUI(fontRendererObj, mc.renderEngine, item, x + CURRENCY2_OFFSET + 1, y + 1);
|
||||
itemRender.renderItemOverlayIntoGUI(fontRendererObj, mc.renderEngine, item, x + CURRENCY2_OFFSET + 1, y + 1);
|
||||
}
|
||||
|
||||
RenderHelper.disableStandardItemLighting();
|
||||
|
||||
fontRendererObj.drawString("=", x + 36, y + 5, CustomNpcResourceListener.DefaultTextColor);
|
||||
// Draw stock count to the RIGHT of the trade (after output slot)
|
||||
if (stockEnabled && availableStock[slotIdx] < Integer.MAX_VALUE) {
|
||||
String stockText = String.valueOf(availableStock[slotIdx]);
|
||||
int stockColor = availableStock[slotIdx] > 0 ? 0x00AA00 : 0xAA0000;
|
||||
fontRendererObj.drawString(stockText, x + OUTPUT_OFFSET + 20, y + 5, stockColor);
|
||||
}
|
||||
|
||||
// Draw dark overlay for out of stock items
|
||||
if (stockEnabled && availableStock[slotIdx] <= 0) {
|
||||
drawGradientRect(x + OUTPUT_OFFSET + 1, y + 1, x + OUTPUT_OFFSET + 17, y + 17, 0xC0101010, 0xC0101010);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GL11.glEnable(GL11.GL_LIGHTING);
|
||||
GL11.glEnable(GL11.GL_DEPTH_TEST);
|
||||
RenderHelper.enableStandardItemLighting();
|
||||
super.drawGuiContainerBackgroundLayer(f, i, j);
|
||||
|
||||
super.drawGuiContainerBackgroundLayer(f, mouseX, mouseY);
|
||||
|
||||
// Draw player balance (BOTTOM RIGHT, below GUI)
|
||||
int infoY = guiTop + ySize + 2;
|
||||
String balanceText = StatCollector.translateToLocal("trader.balance") + ": $" + formatCurrency(playerBalance);
|
||||
int balanceWidth = fontRendererObj.getStringWidth(balanceText);
|
||||
fontRendererObj.drawString(balanceText, guiLeft + xSize - balanceWidth - 4, infoY, 0x36e82a);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawGuiContainerForegroundLayer(int par1, int par2) {
|
||||
for (int slot = 0; slot < 18; slot++) {
|
||||
int x = slot % 3 * 72 + 10;
|
||||
int y = slot / 3 * 21 + 6;
|
||||
protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) {
|
||||
for (int slotIdx = 0; slotIdx < 18; slotIdx++) {
|
||||
int col = slotIdx % 3;
|
||||
int row = slotIdx / 3;
|
||||
int x = COLUMN_START_X + col * COLUMN_WIDTH;
|
||||
int y = ROW_START_Y + row * ROW_HEIGHT;
|
||||
|
||||
ItemStack item = role.inventoryCurrency.items.get(slot);
|
||||
ItemStack item2 = role.inventoryCurrency.items.get(slot + 18);
|
||||
ItemStack item = role.inventoryCurrency.items.get(slotIdx);
|
||||
ItemStack item2 = role.inventoryCurrency.items.get(slotIdx + 18);
|
||||
if (item == null) {
|
||||
item = item2;
|
||||
item2 = null;
|
||||
}
|
||||
if (NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) {
|
||||
if (item != null && item2 != null && NoppesUtilPlayer.compareItems(item, item2, role.ignoreDamage, role.ignoreNBT)) {
|
||||
item = item.copy();
|
||||
item.stackSize += item2.stackSize;
|
||||
item2 = null;
|
||||
}
|
||||
ItemStack sold = role.inventorySold.items.get(slot);
|
||||
if (item == null || sold == null)
|
||||
|
||||
ItemStack sold = role.inventorySold.items.get(slotIdx);
|
||||
if (sold == null) {
|
||||
continue;
|
||||
|
||||
if (this.func_146978_c(x + 43, y + 1, 16, 16, par1, par2)) {
|
||||
if (!container.canBuy(slot, player)) {
|
||||
GL11.glTranslatef(0, 0, 300);
|
||||
if (item != null && !NoppesUtilPlayer.compareItems(player, item, role.ignoreDamage, role.ignoreNBT))
|
||||
this.drawGradientRect(x + 17, y, x + 35, y + 18, 0x70771010, 0x70771010);
|
||||
if (item2 != null && !NoppesUtilPlayer.compareItems(player, item2, role.ignoreDamage, role.ignoreNBT))
|
||||
this.drawGradientRect(x - 1, y, x + 17, y + 18, 0x70771010, 0x70771010);
|
||||
|
||||
String title = StatCollector.translateToLocal("trader.insufficient");
|
||||
this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0xDD0000);
|
||||
GL11.glTranslatef(0, 0, -300);
|
||||
} else if (!container.isSlotEnabled(slot, player)) {
|
||||
String title = StatCollector.translateToLocal("trader.slotdisabled");
|
||||
this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0xFF4000);
|
||||
} else {
|
||||
String title = StatCollector.translateToLocal("trader.sufficient");
|
||||
this.fontRendererObj.drawString(title, (xSize - fontRendererObj.getStringWidth(title)) / 2, 131, 0x00DD00);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.func_146978_c(x, y, 16, 16, par1, par2) && item2 != null) {
|
||||
this.renderToolTip(item2, par1 - guiLeft, par2 - guiTop);
|
||||
// Check if hovering over output slot (18x18 area)
|
||||
if (this.func_146978_c(x + OUTPUT_OFFSET, y, 18, 18, mouseX, mouseY)) {
|
||||
updateSlotStatus(slotIdx, item, item2, x, y);
|
||||
}
|
||||
if (this.func_146978_c(x + 18, y, 16, 16, par1, par2)) {
|
||||
this.renderToolTip(item, par1 - guiLeft, par2 - guiTop);
|
||||
|
||||
// Tooltips for currency items
|
||||
if (this.func_146978_c(x + CURRENCY1_OFFSET, y, 18, 18, mouseX, mouseY) && item2 != null) {
|
||||
this.renderToolTip(item2, mouseX - guiLeft, mouseY - guiTop);
|
||||
}
|
||||
if (this.func_146978_c(x + CURRENCY2_OFFSET, y, 18, 18, mouseX, mouseY) && item != null) {
|
||||
this.renderToolTip(item, mouseX - guiLeft, mouseY - guiTop);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw status message in BOTTOM LEFT (in foreground coordinates)
|
||||
if (!statusMessage.isEmpty()) {
|
||||
fontRendererObj.drawString(statusMessage, 4, ySize + 4, statusColor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status message when hovering over a trade slot
|
||||
*/
|
||||
private void updateSlotStatus(int slotIdx, ItemStack item, ItemStack item2, int x, int y) {
|
||||
long cost = currencyCost[slotIdx];
|
||||
String costSuffix = cost > 0 ? " ($" + formatCurrency(cost) + ")" : "";
|
||||
|
||||
// Check conditions in order of priority
|
||||
if (stockEnabled && availableStock[slotIdx] <= 0) {
|
||||
// Out of stock
|
||||
statusMessage = StatCollector.translateToLocal("trader.outofstock") + costSuffix;
|
||||
statusColor = 0xDD0000;
|
||||
} else if (cost > 0 && playerBalance < cost) {
|
||||
// Can't afford currency cost
|
||||
statusMessage = StatCollector.translateToLocal("trader.insufficient.currency") + costSuffix;
|
||||
statusColor = 0xDD0000;
|
||||
} else if (!container.canBuy(slotIdx, player)) {
|
||||
// Missing item currency - highlight which ones
|
||||
GL11.glTranslatef(0, 0, 300);
|
||||
if (item != null && !NoppesUtilPlayer.compareItems(player, item, role.ignoreDamage, role.ignoreNBT)) {
|
||||
this.drawGradientRect(x + CURRENCY2_OFFSET, y, x + CURRENCY2_OFFSET + 18, y + 18, 0x70771010, 0x70771010);
|
||||
}
|
||||
if (item2 != null && !NoppesUtilPlayer.compareItems(player, item2, role.ignoreDamage, role.ignoreNBT)) {
|
||||
this.drawGradientRect(x + CURRENCY1_OFFSET, y, x + CURRENCY1_OFFSET + 18, y + 18, 0x70771010, 0x70771010);
|
||||
}
|
||||
GL11.glTranslatef(0, 0, -300);
|
||||
statusMessage = StatCollector.translateToLocal("trader.unavailable") + costSuffix;
|
||||
statusColor = 0xDD0000;
|
||||
} else if (!container.isSlotEnabled(slotIdx, player)) {
|
||||
// Slot disabled
|
||||
statusMessage = StatCollector.translateToLocal("trader.slotdisabled") + costSuffix;
|
||||
statusColor = 0xFF4000;
|
||||
} else {
|
||||
// Can buy!
|
||||
statusMessage = StatCollector.translateToLocal("trader.available") + costSuffix;
|
||||
statusColor = 0x00DD00;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the countdown text for stock reset timer.
|
||||
* Calculated client-side each frame for smooth countdown.
|
||||
*/
|
||||
private String getResetCountdownText() {
|
||||
if (resetTargetTime <= 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
long remaining = resetTargetTime - System.currentTimeMillis();
|
||||
if (remaining <= 0) {
|
||||
return StatCollector.translateToLocal("trader.reopentrader");
|
||||
}
|
||||
|
||||
// Format as HH:MM:SS or D days HH:MM:SS
|
||||
long seconds = (remaining / 1000) % 60;
|
||||
long minutes = (remaining / 60000) % 60;
|
||||
long hours = (remaining / 3600000) % 24;
|
||||
long days = remaining / 86400000;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(StatCollector.translateToLocal("trader.restock")).append(": ");
|
||||
|
||||
if (days > 0) {
|
||||
sb.append(days).append("d ");
|
||||
}
|
||||
sb.append(String.format("%02d:%02d:%02d", hours, minutes, seconds));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency for display (e.g., 1000 -> "1,000")
|
||||
*/
|
||||
private String formatCurrency(long amount) {
|
||||
if (amount < 1000) {
|
||||
return String.valueOf(amount);
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String str = String.valueOf(amount);
|
||||
int count = 0;
|
||||
for (int i = str.length() - 1; i >= 0; i--) {
|
||||
if (count > 0 && count % 3 == 0) {
|
||||
sb.insert(0, ',');
|
||||
}
|
||||
sb.insert(0, str.charAt(i));
|
||||
count++;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,7 +8,7 @@ import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import noppes.npcs.client.CustomNpcResourceListener;
|
||||
import noppes.npcs.client.gui.util.GuiContainerNPCInterface2;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButtonYesNo;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.GuiNpcTextField;
|
||||
import noppes.npcs.client.gui.util.ITextfieldListener;
|
||||
@@ -22,6 +22,9 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex
|
||||
private final ResourceLocation slot = new ResourceLocation("customnpcs", "textures/gui/slot.png");
|
||||
private RoleTrader role;
|
||||
|
||||
// Currency cost textfield IDs start at 100 (100-117 for slots 0-17)
|
||||
private static final int CURRENCY_FIELD_ID_START = 100;
|
||||
|
||||
public GuiNpcTraderSetup(EntityNPCInterface npc, ContainerNPCTraderSetup container) {
|
||||
super(npc, container);
|
||||
ySize = 220;
|
||||
@@ -34,17 +37,30 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex
|
||||
super.initGui();
|
||||
buttonList.clear();
|
||||
setBackground("tradersetup.png");
|
||||
|
||||
// Linked Market name
|
||||
addLabel(new GuiNpcLabel(0, "role.marketname", guiLeft + 214, guiTop + 150));
|
||||
addTextField(new GuiNpcTextField(0, this, guiLeft + 214, guiTop + 160, 180, 20, role.marketName));
|
||||
|
||||
addLabel(new GuiNpcLabel(1, "gui.ignoreDamage", guiLeft + 260, guiTop + 29));
|
||||
addButton(new GuiNpcButtonYesNo(1, guiLeft + 340, guiTop + 24, role.ignoreDamage));
|
||||
// Settings button (opens SubGuiNpcTraderSettings)
|
||||
addButton(new GuiNpcButton(1, guiLeft + 214, guiTop + 184, 88, 20, "gui.settings"));
|
||||
getButton(1).setHoverText("gui.settings.hover");
|
||||
|
||||
addLabel(new GuiNpcLabel(2, "gui.ignoreNBT", guiLeft + 260, guiTop + 51));
|
||||
addButton(new GuiNpcButtonYesNo(2, guiLeft + 340, guiTop + 46, role.ignoreNBT));
|
||||
// Stock Options button (opens SubGuiNpcTraderStock)
|
||||
addButton(new GuiNpcButton(2, guiLeft + 306, guiTop + 184, 88, 20, "stock.options"));
|
||||
getButton(2).setHoverText("stock.options.hover");
|
||||
|
||||
addLabel(new GuiNpcLabel(3, "gui.recordHistory", guiLeft + 260, guiTop + 73));
|
||||
addButton(new GuiNpcButtonYesNo(3, guiLeft + 340, guiTop + 68, role.recordHistory));
|
||||
// Currency cost textfields for each slot (FIRST - before item slots)
|
||||
// Column width: 130px for better spacing
|
||||
for (int i = 0; i < 18; i++) {
|
||||
int x = guiLeft + i % 3 * 130 + 15; // First position after "$" label
|
||||
int y = guiTop + i / 3 * 22 + 14;
|
||||
long cost = role.getCurrencyCost(i);
|
||||
GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? String.valueOf(cost) : "");
|
||||
field.setMaxStringLength(8);
|
||||
field.setIntegersOnly();
|
||||
addTextField(field);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,13 +73,12 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex
|
||||
@Override
|
||||
public void actionPerformed(GuiButton guibutton) {
|
||||
if (guibutton.id == 1) {
|
||||
role.ignoreDamage = ((GuiNpcButtonYesNo) guibutton).getBoolean();
|
||||
// Open Settings SubGui
|
||||
setSubGui(new SubGuiNpcTraderSettings(role));
|
||||
}
|
||||
if (guibutton.id == 2) {
|
||||
role.ignoreNBT = ((GuiNpcButtonYesNo) guibutton).getBoolean();
|
||||
}
|
||||
if (guibutton.id == 3) {
|
||||
role.recordHistory = ((GuiNpcButtonYesNo) guibutton).getBoolean();
|
||||
// Open Stock Options SubGui
|
||||
setSubGui(new SubGuiNpcTraderStock(role));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,19 +86,31 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex
|
||||
protected void drawGuiContainerBackgroundLayer(float f, int i, int j) {
|
||||
super.drawGuiContainerBackgroundLayer(f, i, j);
|
||||
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
// Column width: 130px for better spacing
|
||||
for (int slot = 0; slot < 18; slot++) {
|
||||
int x = guiLeft + slot % 3 * 94 + 7;
|
||||
int x = guiLeft + slot % 3 * 130 + 7;
|
||||
int y = guiTop + slot / 3 * 22 + 4;
|
||||
|
||||
mc.renderEngine.bindTexture(this.slot);
|
||||
GL11.glColor4f(1, 1, 1, 1);
|
||||
drawTexturedModalRect(x - 1, y, 0, 0, 18, 18);
|
||||
drawTexturedModalRect(x + 17, y, 0, 0, 18, 18);
|
||||
// "$" label for currency cost
|
||||
fontRendererObj.drawString("$", x, y + 5, CustomNpcResourceListener.DefaultTextColor);
|
||||
|
||||
fontRendererObj.drawString("=", x + 36, y + 5, CustomNpcResourceListener.DefaultTextColor);
|
||||
// Currency textfield is at x + 8 (drawn by textfield system)
|
||||
|
||||
// Item Cost 1 slot
|
||||
mc.renderEngine.bindTexture(this.slot);
|
||||
GL11.glColor4f(1, 1, 1, 1);
|
||||
drawTexturedModalRect(x + 42, y, 0, 0, 18, 18);
|
||||
drawTexturedModalRect(x + 50, y, 0, 0, 18, 18);
|
||||
|
||||
// Item Cost 2 slot
|
||||
drawTexturedModalRect(x + 68, y, 0, 0, 18, 18);
|
||||
|
||||
// "=" sign
|
||||
fontRendererObj.drawString("=", x + 88, y + 5, CustomNpcResourceListener.DefaultTextColor);
|
||||
|
||||
// Output slot
|
||||
mc.renderEngine.bindTexture(this.slot);
|
||||
GL11.glColor4f(1, 1, 1, 1);
|
||||
drawTexturedModalRect(x + 104, y, 0, 0, 18, 18);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +122,23 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex
|
||||
|
||||
@Override
|
||||
public void unFocused(GuiNpcTextField guiNpcTextField) {
|
||||
String name = guiNpcTextField.getText();
|
||||
if (!name.equalsIgnoreCase(role.marketName)) {
|
||||
role.marketName = name;
|
||||
PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, true));
|
||||
int id = guiNpcTextField.id;
|
||||
|
||||
// Handle market name field
|
||||
if (id == 0) {
|
||||
String name = guiNpcTextField.getText();
|
||||
if (!name.equalsIgnoreCase(role.marketName)) {
|
||||
role.marketName = name;
|
||||
PacketClient.sendClient(new TraderMarketSavePacket(role.marketName, true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle currency cost fields (IDs 100-117)
|
||||
if (id >= CURRENCY_FIELD_ID_START && id < CURRENCY_FIELD_ID_START + 18) {
|
||||
int slot = id - CURRENCY_FIELD_ID_START;
|
||||
long cost = guiNpcTextField.getInteger();
|
||||
role.setCurrencyCost(slot, cost);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package noppes.npcs.client.gui.roles;
|
||||
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
import noppes.npcs.roles.RoleTrader;
|
||||
|
||||
/**
|
||||
* Confirmation dialog for resetting trader stock cooldown.
|
||||
*/
|
||||
public class SubGuiNpcTraderCooldownReset extends SubGuiInterface {
|
||||
|
||||
private final RoleTrader role;
|
||||
public boolean confirmed = false;
|
||||
|
||||
public SubGuiNpcTraderCooldownReset(RoleTrader role) {
|
||||
this.role = role;
|
||||
setBackground("menubg.png");
|
||||
xSize = 200;
|
||||
ySize = 100;
|
||||
closeOnEsc = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
int y = guiTop + 15;
|
||||
|
||||
addLabel(new GuiNpcLabel(0, "stock.cooldown.confirm", guiLeft + 10, y));
|
||||
y += 12;
|
||||
addLabel(new GuiNpcLabel(1, "stock.cooldown.warning", guiLeft + 10, y));
|
||||
|
||||
y += 35;
|
||||
|
||||
addButton(new GuiNpcButton(0, guiLeft + 30, y, 60, 20, "gui.yes"));
|
||||
addButton(new GuiNpcButton(1, guiLeft + 110, y, 60, 20, "gui.no"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buttonEvent(GuiButton guibutton) {
|
||||
if (guibutton.id == 0) {
|
||||
// Reset the cooldown
|
||||
role.resetCooldown();
|
||||
confirmed = true;
|
||||
close();
|
||||
} else if (guibutton.id == 1) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package noppes.npcs.client.gui.roles;
|
||||
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButtonYesNo;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
import noppes.npcs.roles.RoleTrader;
|
||||
|
||||
public class SubGuiNpcTraderSettings extends SubGuiInterface {
|
||||
|
||||
private final RoleTrader role;
|
||||
|
||||
public SubGuiNpcTraderSettings(RoleTrader role) {
|
||||
this.role = role;
|
||||
setBackground("menubg.png");
|
||||
xSize = 176;
|
||||
ySize = 136;
|
||||
closeOnEsc = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
int y = guiTop + 14;
|
||||
|
||||
addLabel(new GuiNpcLabel(1, "gui.ignoreDamage", guiLeft + 10, y + 5));
|
||||
addButton(new GuiNpcButtonYesNo(1, guiLeft + 120, y, role.ignoreDamage));
|
||||
getButton(1).setHoverText("gui.ignoreDamage.hover");
|
||||
|
||||
y += 28;
|
||||
|
||||
addLabel(new GuiNpcLabel(2, "gui.ignoreNBT", guiLeft + 10, y + 5));
|
||||
addButton(new GuiNpcButtonYesNo(2, guiLeft + 120, y, role.ignoreNBT));
|
||||
getButton(2).setHoverText("gui.ignoreNBT.hover");
|
||||
|
||||
y += 28;
|
||||
|
||||
addLabel(new GuiNpcLabel(3, "gui.recordHistory", guiLeft + 10, y + 5));
|
||||
addButton(new GuiNpcButtonYesNo(3, guiLeft + 120, y, role.recordHistory));
|
||||
getButton(3).setHoverText("gui.recordHistory.hover");
|
||||
|
||||
y += 34;
|
||||
|
||||
addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, y, 60, 20, "gui.done"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buttonEvent(GuiButton button) {
|
||||
switch (button.id) {
|
||||
case 0:
|
||||
close();
|
||||
break;
|
||||
case 1:
|
||||
role.ignoreDamage = ((GuiNpcButtonYesNo) button).getBoolean();
|
||||
break;
|
||||
case 2:
|
||||
role.ignoreNBT = ((GuiNpcButtonYesNo) button).getBoolean();
|
||||
break;
|
||||
case 3:
|
||||
role.recordHistory = ((GuiNpcButtonYesNo) button).getBoolean();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package noppes.npcs.client.gui.roles;
|
||||
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButtonYesNo;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.GuiNpcTextField;
|
||||
import noppes.npcs.client.gui.util.ISubGuiListener;
|
||||
import noppes.npcs.client.gui.util.ITextfieldListener;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
import noppes.npcs.client.gui.util.SubGuiNpcCooldownPicker;
|
||||
import noppes.npcs.constants.EnumStockReset;
|
||||
import noppes.npcs.roles.RoleTrader;
|
||||
|
||||
public class SubGuiNpcTraderStock extends SubGuiInterface implements ISubGuiListener, ITextfieldListener {
|
||||
|
||||
private final RoleTrader role;
|
||||
|
||||
public SubGuiNpcTraderStock(RoleTrader role) {
|
||||
this.role = role;
|
||||
setBackground("menubg.png");
|
||||
xSize = 220;
|
||||
ySize = 220;
|
||||
closeOnEsc = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
int y = guiTop + 10;
|
||||
|
||||
// Enable Stock
|
||||
addLabel(new GuiNpcLabel(1, "stock.enable", guiLeft + 10, y + 5));
|
||||
addButton(new GuiNpcButtonYesNo(1, guiLeft + 140, y, role.stock.enableStock));
|
||||
getButton(1).setHoverText("stock.enable.hover");
|
||||
|
||||
y += 24;
|
||||
|
||||
// Per-Player Stock
|
||||
addLabel(new GuiNpcLabel(2, "stock.perplayer", guiLeft + 10, y + 5));
|
||||
addButton(new GuiNpcButtonYesNo(2, guiLeft + 140, y, role.stock.perPlayer));
|
||||
getButton(2).setHoverText("stock.perplayer.hover");
|
||||
|
||||
y += 24;
|
||||
|
||||
// Reset Type
|
||||
addLabel(new GuiNpcLabel(3, "stock.resettype", guiLeft + 10, y + 5));
|
||||
addButton(new GuiNpcButton(3, guiLeft + 90, y, 120, 20, EnumStockReset.getDisplayNames(), role.stock.resetType.ordinal()));
|
||||
getButton(3).setHoverText("stock.resettype.hover");
|
||||
|
||||
y += 24;
|
||||
|
||||
// Custom Time button (only visible for custom types)
|
||||
EnumStockReset resetType = role.stock.resetType;
|
||||
if (resetType == EnumStockReset.MCCUSTOM || resetType == EnumStockReset.RLCUSTOM) {
|
||||
addLabel(new GuiNpcLabel(4, "stock.customtime", guiLeft + 10, y + 5));
|
||||
addButton(new GuiNpcButton(4, guiLeft + 90, y, 120, 20, "gui.edit"));
|
||||
getButton(4).setHoverText("stock.customtime.hover");
|
||||
y += 24;
|
||||
}
|
||||
|
||||
// Default Max Stock (applies to all slots as default)
|
||||
addLabel(new GuiNpcLabel(5, "stock.defaultmax", guiLeft + 10, y + 5));
|
||||
GuiNpcTextField defaultStock = new GuiNpcTextField(5, this, fontRendererObj, guiLeft + 140, y, 60, 20, getDefaultStockString());
|
||||
defaultStock.setIntegersOnly();
|
||||
defaultStock.setMinMaxDefault(-1, Integer.MAX_VALUE, -1);
|
||||
addTextField(defaultStock);
|
||||
|
||||
y += 26;
|
||||
|
||||
// Per-Slot Stock button
|
||||
addButton(new GuiNpcButton(5, guiLeft + 10, y, 95, 20, "stock.perslot"));
|
||||
getButton(5).setHoverText("stock.perslot.hover");
|
||||
|
||||
// Reset Stock button
|
||||
addButton(new GuiNpcButton(6, guiLeft + 115, y, 95, 20, "stock.reset"));
|
||||
getButton(6).setHoverText("stock.reset.hover");
|
||||
|
||||
y += 24;
|
||||
|
||||
// Reset Cooldown button
|
||||
addButton(new GuiNpcButton(7, guiLeft + 10, y, 200, 20, "stock.resetcooldown"));
|
||||
getButton(7).setHoverText("stock.resetcooldown.hover");
|
||||
|
||||
// Done button
|
||||
addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, guiTop + ySize - 40, 60, 20, "gui.done"));
|
||||
}
|
||||
|
||||
private String getDefaultStockString() {
|
||||
// Check if all stocks are the same, if so return that value
|
||||
int first = role.stock.maxStock[0];
|
||||
for (int i = 1; i < 18; i++) {
|
||||
if (role.stock.maxStock[i] != first) {
|
||||
return "-1"; // Mixed values, return unlimited
|
||||
}
|
||||
}
|
||||
return String.valueOf(first);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buttonEvent(GuiButton button) {
|
||||
switch (button.id) {
|
||||
case 0: // Done
|
||||
close();
|
||||
break;
|
||||
case 1: // Enable Stock
|
||||
role.stock.enableStock = ((GuiNpcButtonYesNo) button).getBoolean();
|
||||
break;
|
||||
case 2: // Per-Player
|
||||
role.stock.perPlayer = ((GuiNpcButtonYesNo) button).getBoolean();
|
||||
break;
|
||||
case 3: // Reset Type
|
||||
int value = ((GuiNpcButton) button).getValue();
|
||||
role.stock.resetType = EnumStockReset.values()[value];
|
||||
initGui(); // Refresh to show/hide custom time button
|
||||
break;
|
||||
case 4: // Custom Time
|
||||
boolean isMC = role.stock.resetType == EnumStockReset.MCCUSTOM;
|
||||
setSubGui(new SubGuiNpcCooldownPicker(isMC, role.stock.customResetTime));
|
||||
break;
|
||||
case 5: // Per-Slot Stock
|
||||
setSubGui(new SubGuiNpcTraderStockSlots(role));
|
||||
break;
|
||||
case 6: // Reset Stock
|
||||
setSubGui(new SubGuiNpcTraderStockReset(role));
|
||||
break;
|
||||
case 7: // Reset Cooldown
|
||||
setSubGui(new SubGuiNpcTraderCooldownReset(role));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subGuiClosed(SubGuiInterface subGui) {
|
||||
if (subGui instanceof SubGuiNpcCooldownPicker) {
|
||||
long value = ((SubGuiNpcCooldownPicker) subGui).cooldownValue;
|
||||
// Validate minimum: 1 tick for MC time, 1000ms (1 second) for real time
|
||||
boolean isMC = role.stock.resetType == EnumStockReset.MCCUSTOM;
|
||||
long minimum = isMC ? 1 : 1000;
|
||||
role.stock.customResetTime = Math.max(minimum, value);
|
||||
}
|
||||
// Refresh after per-slot editing
|
||||
if (subGui instanceof SubGuiNpcTraderStockSlots) {
|
||||
initGui();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unFocused(GuiNpcTextField textField) {
|
||||
if (textField.id == 5) {
|
||||
int value = textField.getInteger();
|
||||
// Apply to all slots
|
||||
for (int i = 0; i < 18; i++) {
|
||||
role.stock.setMaxStock(i, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package noppes.npcs.client.gui.roles;
|
||||
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
import noppes.npcs.roles.RoleTrader;
|
||||
|
||||
/**
|
||||
* Confirmation dialog for resetting trader stock.
|
||||
*/
|
||||
public class SubGuiNpcTraderStockReset extends SubGuiInterface {
|
||||
|
||||
private final RoleTrader role;
|
||||
public boolean confirmed = false;
|
||||
|
||||
public SubGuiNpcTraderStockReset(RoleTrader role) {
|
||||
this.role = role;
|
||||
setBackground("menubg.png");
|
||||
xSize = 200;
|
||||
ySize = 100;
|
||||
closeOnEsc = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
int y = guiTop + 15;
|
||||
|
||||
addLabel(new GuiNpcLabel(0, "stock.reset.confirm", guiLeft + 10, y));
|
||||
y += 12;
|
||||
addLabel(new GuiNpcLabel(1, "stock.reset.warning", guiLeft + 10, y));
|
||||
|
||||
y += 35;
|
||||
|
||||
addButton(new GuiNpcButton(0, guiLeft + 30, y, 60, 20, "gui.yes"));
|
||||
addButton(new GuiNpcButton(1, guiLeft + 110, y, 60, 20, "gui.no"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buttonEvent(GuiButton guibutton) {
|
||||
if (guibutton.id == 0) {
|
||||
// Reset the stock
|
||||
long currentTime = role.stock.resetType.isRealTime()
|
||||
? System.currentTimeMillis()
|
||||
: (role.npc != null && role.npc.worldObj != null ? role.npc.worldObj.getTotalWorldTime() : 0);
|
||||
role.stock.resetStock(currentTime);
|
||||
confirmed = true;
|
||||
close();
|
||||
} else if (guibutton.id == 1) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package noppes.npcs.client.gui.roles;
|
||||
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.GuiNpcTextField;
|
||||
import noppes.npcs.client.gui.util.ITextfieldListener;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
import noppes.npcs.roles.RoleTrader;
|
||||
|
||||
/**
|
||||
* Per-slot stock management dialog.
|
||||
* Shows all 18 slots with their max stock values.
|
||||
*/
|
||||
public class SubGuiNpcTraderStockSlots extends SubGuiInterface implements ITextfieldListener {
|
||||
|
||||
private final RoleTrader role;
|
||||
|
||||
public SubGuiNpcTraderStockSlots(RoleTrader role) {
|
||||
this.role = role;
|
||||
setBackground("menubg.png");
|
||||
xSize = 256;
|
||||
ySize = 216;
|
||||
closeOnEsc = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
// Title
|
||||
addLabel(new GuiNpcLabel(0, "stock.perslot.title", guiLeft + 10, guiTop + 6));
|
||||
|
||||
// 18 slots in 3 columns x 6 rows
|
||||
int startY = guiTop + 22;
|
||||
int colWidth = 80;
|
||||
|
||||
for (int i = 0; i < 18; i++) {
|
||||
int col = i % 3;
|
||||
int row = i / 3;
|
||||
|
||||
int x = guiLeft + 10 + col * colWidth;
|
||||
int y = startY + row * 28;
|
||||
|
||||
// Slot label
|
||||
addLabel(new GuiNpcLabel(i + 1, "Slot " + (i + 1) + ":", x, y + 5));
|
||||
|
||||
// Max stock textfield
|
||||
int maxStock = role.stock.maxStock[i];
|
||||
String value = maxStock < 0 ? "-1" : String.valueOf(maxStock);
|
||||
GuiNpcTextField field = new GuiNpcTextField(i, this, fontRendererObj, x + 42, y, 32, 18, value);
|
||||
field.setIntegersOnly();
|
||||
field.setMinMaxDefault(-1, Integer.MAX_VALUE, -1);
|
||||
addTextField(field);
|
||||
}
|
||||
|
||||
// Done button
|
||||
addButton(new GuiNpcButton(0, guiLeft + (xSize - 60) / 2, guiTop + ySize - 26, 60, 20, "gui.done"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buttonEvent(GuiButton button) {
|
||||
if (button.id == 0) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unFocused(GuiNpcTextField textField) {
|
||||
int slot = textField.id;
|
||||
if (slot >= 0 && slot < 18) {
|
||||
int value = textField.getInteger();
|
||||
role.stock.setMaxStock(slot, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
this.addLabel(new GuiNpcLabel(0, "gui.animations", guiLeft + 8, guiTop + 4));
|
||||
this.addLabel(new GuiNpcLabel(0, "menu.animations", guiLeft + 8, guiTop + 4));
|
||||
|
||||
this.addButton(new GuiNpcButton(1, guiLeft + 4, guiTop + ySize - 35, 50, 20, "gui.clear"));
|
||||
this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, guiTop + ySize - 35, 50, 20, "gui.done"));
|
||||
|
||||
@@ -224,6 +224,15 @@ public abstract class GuiContainerNPCInterface extends GuiContainer {
|
||||
RenderHelper.disableStandardItemLighting();
|
||||
subgui.drawScreen(i, j, f);
|
||||
}
|
||||
|
||||
// Draw hover text for buttons
|
||||
boolean subGui = hasSubGui();
|
||||
for (GuiCustomScroll scroll : new ArrayList<GuiCustomScroll>(scrolls.values())) {
|
||||
scroll.drawHover(i, j);
|
||||
}
|
||||
for (GuiNpcButton button : buttons.values()) {
|
||||
button.drawHover(i, j, subGui);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -61,9 +61,9 @@ public abstract class GuiContainerNPCInterface2 extends GuiContainerNPCInterface
|
||||
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
mc.renderEngine.bindTexture(defaultBackground2);
|
||||
if (this instanceof GuiNpcTraderSetup)
|
||||
drawTexturedModalRect(guiLeft + xSize - 210, guiTop, 46, 0, 256, 220);
|
||||
drawTexturedModalRect(guiLeft + xSize - 210, guiTop, 46, 0, 210, 220);
|
||||
else
|
||||
drawTexturedModalRect(guiLeft + xSize - 230, guiTop, 26, 0, 256, 220);
|
||||
drawTexturedModalRect(guiLeft + xSize - 230, guiTop, 26, 0, 230, 220);
|
||||
|
||||
menu.drawElements(fontRendererObj, i, j, mc, f);
|
||||
|
||||
|
||||
@@ -421,10 +421,23 @@ public abstract class GuiNPCInterface extends GuiScreen {
|
||||
GL11.glScalef(bgScale * bgScaleX, bgScale * bgScaleY, bgScale * bgScaleZ);
|
||||
mc.renderEngine.bindTexture(background);
|
||||
if (xSize > 256) {
|
||||
// GUI wider than texture: draw left portion, then right portion from texture edge
|
||||
drawTexturedModalRect(0, 0, 0, 0, 250, ySize);
|
||||
drawTexturedModalRect(250, 0, 256 - (xSize - 250), 0, xSize - 250, ySize);
|
||||
} else
|
||||
} else if (xSize < 256) {
|
||||
// GUI narrower than texture: stitch left and right edges together
|
||||
// Left portion (first half of GUI)
|
||||
int leftWidth = xSize / 2;
|
||||
// Right portion width (remaining)
|
||||
int rightWidth = xSize - leftWidth;
|
||||
// Draw left side from texture start
|
||||
drawTexturedModalRect(0, 0, 0, 0, leftWidth, ySize);
|
||||
// Draw right side from texture end (256 - rightWidth)
|
||||
drawTexturedModalRect(leftWidth, 0, 256 - rightWidth, 0, rightWidth, ySize);
|
||||
} else {
|
||||
// GUI exactly 256px: draw full texture
|
||||
drawTexturedModalRect(0, 0, 0, 0, xSize, ySize);
|
||||
}
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package noppes.npcs.config;
|
||||
|
||||
import cpw.mods.fml.common.FMLLog;
|
||||
import net.minecraftforge.common.config.Configuration;
|
||||
import org.apache.logging.log4j.Level;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ConfigMarket {
|
||||
public static Configuration config;
|
||||
|
||||
public final static String MARKET = "Market";
|
||||
public final static String CURRENCY = "Market.Currency";
|
||||
public final static String TRADER = "Market.Trader";
|
||||
|
||||
// =========================================
|
||||
// Market General
|
||||
// =========================================
|
||||
public static boolean MarketEnabled = true;
|
||||
|
||||
// =========================================
|
||||
// Currency Settings
|
||||
// =========================================
|
||||
public static boolean UseVault = false;
|
||||
public static String CurrencyName = "Coins";
|
||||
public static long StartingBalance = 0;
|
||||
public static long MaxBalance = Long.MAX_VALUE;
|
||||
|
||||
// =========================================
|
||||
// Trader Settings
|
||||
// =========================================
|
||||
public static boolean EnableStockByDefault = false;
|
||||
public static String DefaultResetType = "NONE";
|
||||
|
||||
public static void init(File configFile) {
|
||||
config = new Configuration(configFile);
|
||||
|
||||
try {
|
||||
config.load();
|
||||
|
||||
// =========================================
|
||||
// Market General
|
||||
// =========================================
|
||||
MarketEnabled = config.get(MARKET, "Enable Market System", true,
|
||||
"Enable the market/economy system").getBoolean(true);
|
||||
|
||||
// =========================================
|
||||
// Currency Settings
|
||||
// =========================================
|
||||
config.setCategoryComment(CURRENCY, "CNPC+ Currency system settings. When UseVault is enabled, all currency operations use Vault instead of the built-in system.");
|
||||
|
||||
UseVault = config.get(CURRENCY, "Use Vault", false,
|
||||
"If true, use Vault API for all currency operations instead of CNPC+ built-in currency. The built-in currency data will be preserved but unused.").getBoolean(false);
|
||||
|
||||
CurrencyName = config.get(CURRENCY, "Currency Name", "Coins",
|
||||
"Display name for the currency (e.g. 'Coins', 'Gold', 'Credits')").getString();
|
||||
|
||||
StartingBalance = config.get(CURRENCY, "Starting Balance", 0,
|
||||
"Starting currency balance for new players").getInt(0);
|
||||
|
||||
MaxBalance = config.get(CURRENCY, "Max Balance", Integer.MAX_VALUE,
|
||||
"Maximum currency balance a player can have").getInt(Integer.MAX_VALUE);
|
||||
|
||||
// =========================================
|
||||
// Trader Settings
|
||||
// =========================================
|
||||
config.setCategoryComment(TRADER, "Trader stock system settings");
|
||||
|
||||
EnableStockByDefault = config.get(TRADER, "Enable Stock By Default", false,
|
||||
"Enable stock system by default for new traders").getBoolean(false);
|
||||
|
||||
DefaultResetType = config.get(TRADER, "Default Reset Type", "NONE",
|
||||
"Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY)").getString();
|
||||
|
||||
// Set category order
|
||||
config.setCategoryPropertyOrder(MARKET, new ArrayList<>(Arrays.asList(
|
||||
"Enable Market System"
|
||||
)));
|
||||
|
||||
config.setCategoryPropertyOrder(CURRENCY, new ArrayList<>(Arrays.asList(
|
||||
"Use Vault",
|
||||
"Currency Name",
|
||||
"Starting Balance",
|
||||
"Max Balance"
|
||||
)));
|
||||
|
||||
config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList(
|
||||
"Enable Stock By Default",
|
||||
"Default Reset Type"
|
||||
)));
|
||||
|
||||
} catch (Exception e) {
|
||||
FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its market configuration");
|
||||
} finally {
|
||||
if (config.hasChanged()) {
|
||||
config.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ public class LoadConfiguration {
|
||||
public static File debugConfigFile;
|
||||
public static File clientConfigFile;
|
||||
public static File experimentalConfigFile;
|
||||
public static File marketConfigFile;
|
||||
|
||||
public static void init(String configpath) {
|
||||
mainConfigFile = new File(configpath + "main.cfg");
|
||||
@@ -17,6 +18,7 @@ public class LoadConfiguration {
|
||||
scriptConfigFile = new File(configpath + "script.cfg");
|
||||
debugConfigFile = new File(configpath + "debug.cfg");
|
||||
experimentalConfigFile = new File(configpath + "experimental.cfg");
|
||||
marketConfigFile = new File(configpath + "market.cfg");
|
||||
|
||||
ConfigMain.init(mainConfigFile);
|
||||
ConfigItem.init(itemConfigFile);
|
||||
@@ -24,5 +26,6 @@ public class LoadConfiguration {
|
||||
ConfigScript.init(scriptConfigFile);
|
||||
ConfigDebug.init(debugConfigFile);
|
||||
ConfigExperimental.init(experimentalConfigFile);
|
||||
ConfigMarket.init(marketConfigFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package noppes.npcs.constants;
|
||||
|
||||
import net.minecraft.util.StatCollector;
|
||||
|
||||
/**
|
||||
* Stock reset types for Trader NPCs.
|
||||
* Mirrors EnumQuestRepeat pattern for consistency.
|
||||
*/
|
||||
public enum EnumStockReset {
|
||||
NONE("stock.reset.never"),
|
||||
MCDAILY("stock.reset.mcdaily"),
|
||||
MCWEEKLY("stock.reset.mcweekly"),
|
||||
MCCUSTOM("stock.reset.mccustom"),
|
||||
RLDAILY("stock.reset.rldaily"),
|
||||
RLWEEKLY("stock.reset.rlweekly"),
|
||||
RLCUSTOM("stock.reset.rlcustom");
|
||||
|
||||
private final String langKey;
|
||||
|
||||
EnumStockReset(String langKey) {
|
||||
this.langKey = langKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reset interval in milliseconds for real-time types
|
||||
* or ticks for MC-time types
|
||||
*/
|
||||
public long getDefaultInterval() {
|
||||
switch (this) {
|
||||
case MCDAILY:
|
||||
return 24000; // MC ticks
|
||||
case MCWEEKLY:
|
||||
return 168000; // MC ticks (7 days)
|
||||
case RLDAILY:
|
||||
return 86400000L; // 24 hours in ms
|
||||
case RLWEEKLY:
|
||||
return 604800000L; // 7 days in ms
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this reset type uses real-world time
|
||||
*/
|
||||
public boolean isRealTime() {
|
||||
return this == RLDAILY || this == RLWEEKLY || this == RLCUSTOM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this reset type uses Minecraft time
|
||||
*/
|
||||
public boolean isMinecraftTime() {
|
||||
return this == MCDAILY || this == MCWEEKLY || this == MCCUSTOM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display name for GUI (translated)
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
return StatCollector.translateToLocal(langKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all display names for dropdown menus
|
||||
*/
|
||||
public static String[] getDisplayNames() {
|
||||
EnumStockReset[] values = values();
|
||||
String[] names = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
names[i] = values[i].getDisplayName();
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,71 @@
|
||||
package noppes.npcs.containers;
|
||||
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.inventory.Slot;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import noppes.npcs.NoppesUtilPlayer;
|
||||
import noppes.npcs.controllers.data.PlayerData;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import noppes.npcs.roles.RoleTrader;
|
||||
|
||||
|
||||
public class ContainerNPCTrader extends ContainerNpcInterface {
|
||||
public RoleTrader role;
|
||||
private final EntityNPCInterface npc;
|
||||
|
||||
// Layout constants for 256px wide GUI
|
||||
public static final int COLUMN_WIDTH = 80;
|
||||
public static final int COLUMN_START_X = 8;
|
||||
public static final int ROW_HEIGHT = 22;
|
||||
public static final int ROW_START_Y = 6;
|
||||
|
||||
// Slot offsets within each column (for 18x18 slots, items render centered)
|
||||
public static final int CURRENCY1_OFFSET = 2; // Primary currency slot
|
||||
public static final int CURRENCY2_OFFSET = 20; // Secondary currency slot
|
||||
public static final int OUTPUT_OFFSET = 50; // Output slot
|
||||
|
||||
public ContainerNPCTrader(EntityNPCInterface npc, EntityPlayer player) {
|
||||
super(player);
|
||||
this.npc = npc;
|
||||
role = (RoleTrader) npc.roleInterface;
|
||||
|
||||
for (int i = 0; i < 18; i++) {
|
||||
int x = 53;
|
||||
x += i % 3 * 72;
|
||||
int y = 7;
|
||||
y += i / 3 * 21;
|
||||
// Register this player as viewing the trader (server-side only)
|
||||
// Needed for shared stock (not per-player) to sync updates to other viewers
|
||||
// Works for both linked markets (MarketRegistry) and normal traders (local viewers)
|
||||
if (player instanceof EntityPlayerMP && !role.stock.perPlayer) {
|
||||
role.registerViewer((EntityPlayerMP) player);
|
||||
}
|
||||
|
||||
ItemStack item = role.inventoryCurrency.items.get(i);
|
||||
ItemStack item2 = role.inventoryCurrency.items.get(i + 18);
|
||||
if (item == null) {
|
||||
item = item2;
|
||||
item2 = null;
|
||||
}
|
||||
addSlotToContainer(new Slot(role.inventorySold, i, x, y));
|
||||
// Trade slots: 3 columns x 6 rows, 80px column width
|
||||
// Layout per column: [Slot1][Slot2] = [Output]
|
||||
for (int i = 0; i < 18; i++) {
|
||||
int col = i % 3;
|
||||
int row = i / 3;
|
||||
int x = COLUMN_START_X + col * COLUMN_WIDTH;
|
||||
int y = ROW_START_Y + row * ROW_HEIGHT;
|
||||
// Output slot position (slot texture is 18x18, item renders centered at +1)
|
||||
addSlotToContainer(new Slot(role.inventorySold, i, x + OUTPUT_OFFSET + 1, y + 1));
|
||||
}
|
||||
|
||||
for (int i1 = 0; i1 < 3; i1++) {
|
||||
for (int l1 = 0; l1 < 9; l1++) {
|
||||
addSlotToContainer(new Slot(player.inventory, l1 + i1 * 9 + 9, 32 + l1 * 18, 140 + i1 * 18));
|
||||
addSlotToContainer(new Slot(player.inventory, l1 + i1 * 9 + 9, 48 + l1 * 18, 137 + i1 * 18));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Player hotbar
|
||||
for (int j1 = 0; j1 < 9; j1++) {
|
||||
addSlotToContainer(new Slot(player.inventory, j1, 32 + j1 * 18, 198));
|
||||
addSlotToContainer(new Slot(player.inventory, j1, 48 + j1 * 18, 195));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContainerClosed(EntityPlayer player) {
|
||||
super.onContainerClosed(player);
|
||||
// Unregister this player from viewer tracking (only if registered for shared stock)
|
||||
if (player instanceof EntityPlayerMP && !role.stock.perPlayer) {
|
||||
role.unregisterViewer((EntityPlayerMP) player);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,17 +88,57 @@ public class ContainerNPCTrader extends ContainerNpcInterface {
|
||||
ItemStack item = slot.getStack();
|
||||
if (!canGivePlayer(item, entityplayer))
|
||||
return null;
|
||||
if (!canBuy(i, entityplayer))
|
||||
return null;
|
||||
if (!isSlotEnabled(i, entityplayer))
|
||||
return null;
|
||||
|
||||
// Check stock availability
|
||||
String playerName = entityplayer.getCommandSenderName();
|
||||
if (!role.hasStock(i, playerName, 1)) {
|
||||
return null; // Out of stock
|
||||
}
|
||||
|
||||
// Item-based currency
|
||||
if (!canBuy(i, entityplayer))
|
||||
return null;
|
||||
|
||||
// Currency cost check (additive to item costs)
|
||||
long currencyCost = role.getCurrencyCost(i);
|
||||
if (currencyCost > 0) {
|
||||
PlayerData data = PlayerData.get(entityplayer);
|
||||
if (data.currencyData.getBalance() < currencyCost) {
|
||||
return null; // Can't afford currency cost
|
||||
}
|
||||
}
|
||||
|
||||
// Consume item currency
|
||||
NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i), role.ignoreDamage, role.ignoreNBT);
|
||||
NoppesUtilPlayer.consumeItem(entityplayer, role.inventoryCurrency.getStackInSlot(i + 18), role.ignoreDamage, role.ignoreNBT);
|
||||
|
||||
// Withdraw currency cost (after item consumption to maintain order)
|
||||
if (currencyCost > 0) {
|
||||
PlayerData data = PlayerData.get(entityplayer);
|
||||
data.currencyData.withdraw(currencyCost);
|
||||
}
|
||||
|
||||
// Consume stock
|
||||
role.consumeStock(i, playerName, 1);
|
||||
|
||||
ItemStack soldItem = item.copy();
|
||||
givePlayer(soldItem, entityplayer);
|
||||
role.addPurchase(i, entityplayer.getDisplayName());
|
||||
return soldItem;
|
||||
|
||||
// Sync updated data after purchase
|
||||
// For per-player stocks: sync only to the purchasing player
|
||||
// For shared stocks: consumeStock() handles sync (market-wide or local)
|
||||
if (entityplayer instanceof EntityPlayerMP) {
|
||||
if (role.stock.perPlayer) {
|
||||
// Per-player: only sync to purchasing player (their own stock/balance)
|
||||
role.syncToPlayer((EntityPlayerMP) entityplayer);
|
||||
}
|
||||
// Shared stock sync is already handled in consumeStock()
|
||||
}
|
||||
|
||||
return soldItem;
|
||||
}
|
||||
|
||||
public boolean isSlotEnabled(int slot, EntityPlayer player) {
|
||||
|
||||
@@ -13,15 +13,19 @@ public class ContainerNPCTraderSetup extends Container {
|
||||
public ContainerNPCTraderSetup(EntityNPCInterface npc, EntityPlayer player) {
|
||||
role = (RoleTrader) npc.roleInterface;
|
||||
|
||||
// New layout: $[Currency TextField], Item Cost 1, Item Cost 2, =, Output
|
||||
// Column width: 130px for better spacing
|
||||
for (int i = 0; i < 18; i++) {
|
||||
int x = 7;
|
||||
x += i % 3 * 94;
|
||||
x += i % 3 * 130;
|
||||
int y = 15;
|
||||
y += i / 3 * 22;
|
||||
addSlotToContainer(new Slot(role.inventoryCurrency, i + 18, x, y));
|
||||
addSlotToContainer(new Slot(role.inventoryCurrency, i, x + 18, y));
|
||||
addSlotToContainer(new Slot(role.inventorySold, i, x + 43, y));
|
||||
|
||||
// Item Cost 1 (primary currency)
|
||||
addSlotToContainer(new Slot(role.inventoryCurrency, i, x + 51, y));
|
||||
// Item Cost 2 (secondary currency)
|
||||
addSlotToContainer(new Slot(role.inventoryCurrency, i + 18, x + 69, y));
|
||||
// Output (sold item)
|
||||
addSlotToContainer(new Slot(role.inventorySold, i, x + 105, y));
|
||||
}
|
||||
|
||||
for (int i1 = 0; i1 < 3; i1++) {
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package noppes.npcs.controllers;
|
||||
|
||||
import foxz.utils.Market;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.roles.RoleTrader;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Central registry for linked market synchronization.
|
||||
* Tracks which RoleTrader instances use which markets,
|
||||
* and which players are viewing each market.
|
||||
*
|
||||
* This enables immediate sync across all traders sharing
|
||||
* the same market when a purchase occurs.
|
||||
*/
|
||||
public class MarketRegistry {
|
||||
|
||||
// marketName -> Set of traders using this market (WeakHashMap for auto-cleanup)
|
||||
private static final Map<String, Set<RoleTrader>> marketTraders = new ConcurrentHashMap<>();
|
||||
|
||||
// marketName -> Set of players viewing this market (across all NPCs)
|
||||
private static final Map<String, Set<EntityPlayerMP>> marketViewers = new ConcurrentHashMap<>();
|
||||
|
||||
// ==================== Trader Registration ====================
|
||||
|
||||
/**
|
||||
* Register a trader as using a linked market.
|
||||
* Should be called when NPC loads or marketName is set.
|
||||
*/
|
||||
public static void registerTrader(String marketName, RoleTrader trader) {
|
||||
if (marketName == null || marketName.isEmpty() || trader == null) return;
|
||||
|
||||
Set<RoleTrader> traders = marketTraders.computeIfAbsent(marketName,
|
||||
k -> Collections.newSetFromMap(new WeakHashMap<>()));
|
||||
|
||||
synchronized (traders) {
|
||||
traders.add(trader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a trader from a linked market.
|
||||
* Should be called when NPC unloads or marketName changes.
|
||||
*/
|
||||
public static void unregisterTrader(String marketName, RoleTrader trader) {
|
||||
if (marketName == null || marketName.isEmpty() || trader == null) return;
|
||||
|
||||
Set<RoleTrader> traders = marketTraders.get(marketName);
|
||||
if (traders != null) {
|
||||
synchronized (traders) {
|
||||
traders.remove(trader);
|
||||
if (traders.isEmpty()) {
|
||||
marketTraders.remove(marketName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a trader's market registration.
|
||||
* Handles unregistering from old market and registering to new.
|
||||
*/
|
||||
public static void updateTraderMarket(String oldMarket, String newMarket, RoleTrader trader) {
|
||||
if (trader == null) return;
|
||||
|
||||
if (oldMarket != null && !oldMarket.isEmpty()) {
|
||||
unregisterTrader(oldMarket, trader);
|
||||
}
|
||||
if (newMarket != null && !newMarket.isEmpty()) {
|
||||
registerTrader(newMarket, trader);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Viewer Registration ====================
|
||||
|
||||
/**
|
||||
* Register a player as viewing a market (via any NPC).
|
||||
* Should be called when player opens trader GUI with linked market.
|
||||
*/
|
||||
public static void registerViewer(String marketName, EntityPlayerMP player) {
|
||||
if (marketName == null || marketName.isEmpty() || player == null) return;
|
||||
|
||||
Set<EntityPlayerMP> viewers = marketViewers.computeIfAbsent(marketName,
|
||||
k -> Collections.newSetFromMap(new WeakHashMap<>()));
|
||||
|
||||
synchronized (viewers) {
|
||||
viewers.add(player);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a player from viewing a market.
|
||||
* Should be called when player closes trader GUI.
|
||||
*/
|
||||
public static void unregisterViewer(String marketName, EntityPlayerMP player) {
|
||||
if (marketName == null || marketName.isEmpty() || player == null) return;
|
||||
|
||||
Set<EntityPlayerMP> viewers = marketViewers.get(marketName);
|
||||
if (viewers != null) {
|
||||
synchronized (viewers) {
|
||||
viewers.remove(player);
|
||||
if (viewers.isEmpty()) {
|
||||
marketViewers.remove(marketName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Synchronization ====================
|
||||
|
||||
/**
|
||||
* Sync stock data to all traders and viewers of a market.
|
||||
* Called after a purchase to update all linked traders and their viewers.
|
||||
*
|
||||
* @param marketName The market to sync
|
||||
*/
|
||||
public static void syncMarket(String marketName) {
|
||||
if (marketName == null || marketName.isEmpty()) return;
|
||||
|
||||
// Get latest data from cache
|
||||
NBTTagCompound marketData = Market.getMarketCache(marketName);
|
||||
if (marketData == null) return;
|
||||
|
||||
// Update all traders' internal stock data
|
||||
Set<RoleTrader> traders = marketTraders.get(marketName);
|
||||
if (traders != null) {
|
||||
NBTTagCompound stockData = marketData.getCompoundTag("Stock");
|
||||
|
||||
synchronized (traders) {
|
||||
// Clean up any null references (WeakHashMap cleanup)
|
||||
traders.removeIf(t -> t == null || t.npc == null);
|
||||
|
||||
for (RoleTrader trader : traders) {
|
||||
// Update stock data from cache
|
||||
if (stockData != null && stockData.hasKey("EnableStock")) {
|
||||
trader.stock.readFromNBT(stockData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sync all viewers (across all NPCs for this market)
|
||||
syncMarketViewers(marketName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync stock data to all players viewing a specific market.
|
||||
* Each viewer gets data relative to their own player (for per-player stock display).
|
||||
*/
|
||||
public static void syncMarketViewers(String marketName) {
|
||||
if (marketName == null || marketName.isEmpty()) return;
|
||||
|
||||
Set<EntityPlayerMP> viewers = marketViewers.get(marketName);
|
||||
if (viewers == null || viewers.isEmpty()) return;
|
||||
|
||||
// Find any trader with this market to use for sync (they all have same data now)
|
||||
Set<RoleTrader> traders = marketTraders.get(marketName);
|
||||
if (traders == null || traders.isEmpty()) return;
|
||||
|
||||
RoleTrader syncSource = null;
|
||||
synchronized (traders) {
|
||||
for (RoleTrader trader : traders) {
|
||||
if (trader != null && trader.npc != null) {
|
||||
syncSource = trader;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (syncSource == null) return;
|
||||
|
||||
// Sync to all viewers
|
||||
synchronized (viewers) {
|
||||
// Remove disconnected players
|
||||
viewers.removeIf(p -> p == null || p.playerNetServerHandler == null);
|
||||
|
||||
for (EntityPlayerMP viewer : viewers) {
|
||||
syncSource.syncToPlayer(viewer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Utility ====================
|
||||
|
||||
/**
|
||||
* Get count of traders registered to a market (for debugging/info).
|
||||
*/
|
||||
public static int getTraderCount(String marketName) {
|
||||
Set<RoleTrader> traders = marketTraders.get(marketName);
|
||||
return traders != null ? traders.size() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of viewers for a market (for debugging/info).
|
||||
*/
|
||||
public static int getViewerCount(String marketName) {
|
||||
Set<EntityPlayerMP> viewers = marketViewers.get(marketName);
|
||||
return viewers != null ? viewers.size() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registrations (for server shutdown/reload).
|
||||
*/
|
||||
public static void clear() {
|
||||
marketTraders.clear();
|
||||
marketViewers.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package noppes.npcs.controllers.data;
|
||||
|
||||
import kamkeel.npcs.util.VaultUtil;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.api.handler.IPlayerCurrencyData;
|
||||
import noppes.npcs.config.ConfigMarket;
|
||||
|
||||
/**
|
||||
* Stores player currency balance.
|
||||
* This data is NOT profile slot bound - all profile slots share the same currency pool.
|
||||
* When Vault API is available and UseVault config is enabled, operations delegate to Vault.
|
||||
* The built-in CNPC+ currency data is always preserved in NBT, but unused when Vault is active.
|
||||
*/
|
||||
public class PlayerCurrencyData implements IPlayerCurrencyData {
|
||||
private final PlayerData playerData;
|
||||
|
||||
// Built-in CNPC+ currency (always saved, used as fallback)
|
||||
private long balance;
|
||||
private long lifetimeEarned;
|
||||
private long lifetimeSpent;
|
||||
|
||||
public PlayerCurrencyData(PlayerData playerData) {
|
||||
this.playerData = playerData;
|
||||
this.balance = ConfigMarket.StartingBalance;
|
||||
this.lifetimeEarned = 0;
|
||||
this.lifetimeSpent = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Vault should be used for currency operations.
|
||||
* Requires both config enabled AND Vault actually available.
|
||||
*/
|
||||
private boolean shouldUseVault() {
|
||||
return ConfigMarket.UseVault && VaultUtil.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get player name for Vault operations
|
||||
*/
|
||||
private String getPlayerName() {
|
||||
if (playerData != null && playerData.player != null) {
|
||||
return playerData.player.getCommandSenderName();
|
||||
}
|
||||
return playerData != null ? playerData.playername : null;
|
||||
}
|
||||
|
||||
public void readFromNBT(NBTTagCompound compound) {
|
||||
if (compound.hasKey("CurrencyBalance")) {
|
||||
this.balance = compound.getLong("CurrencyBalance");
|
||||
} else {
|
||||
this.balance = ConfigMarket.StartingBalance;
|
||||
}
|
||||
this.lifetimeEarned = compound.getLong("CurrencyLifetimeEarned");
|
||||
this.lifetimeSpent = compound.getLong("CurrencyLifetimeSpent");
|
||||
}
|
||||
|
||||
public void writeToNBT(NBTTagCompound compound) {
|
||||
// Always save built-in currency data (preserved even when using Vault)
|
||||
compound.setLong("CurrencyBalance", this.balance);
|
||||
compound.setLong("CurrencyLifetimeEarned", this.lifetimeEarned);
|
||||
compound.setLong("CurrencyLifetimeSpent", this.lifetimeSpent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current balance
|
||||
*/
|
||||
@Override
|
||||
public long getBalance() {
|
||||
if (shouldUseVault()) {
|
||||
String playerName = getPlayerName();
|
||||
if (playerName != null) {
|
||||
return (long) VaultUtil.getBalance(playerName);
|
||||
}
|
||||
}
|
||||
return balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the balance directly (use with caution)
|
||||
*/
|
||||
@Override
|
||||
public void setBalance(long balance) {
|
||||
if (balance < 0) {
|
||||
balance = 0;
|
||||
}
|
||||
if (balance > ConfigMarket.MaxBalance) {
|
||||
balance = ConfigMarket.MaxBalance;
|
||||
}
|
||||
|
||||
if (shouldUseVault()) {
|
||||
String playerName = getPlayerName();
|
||||
if (playerName != null) {
|
||||
VaultUtil.setBalance(playerName, balance);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add currency to balance (deposit)
|
||||
* @param amount Amount to add
|
||||
* @return true if successful, false if would exceed max balance
|
||||
*/
|
||||
@Override
|
||||
public boolean deposit(long amount) {
|
||||
if (amount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shouldUseVault()) {
|
||||
String playerName = getPlayerName();
|
||||
if (playerName != null) {
|
||||
boolean success = VaultUtil.addMoney(playerName, amount);
|
||||
if (success) {
|
||||
this.lifetimeEarned += amount;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
// Built-in currency
|
||||
long newBalance = this.balance + amount;
|
||||
if (newBalance > ConfigMarket.MaxBalance) {
|
||||
return false;
|
||||
}
|
||||
if (newBalance < this.balance) {
|
||||
// Overflow protection
|
||||
return false;
|
||||
}
|
||||
this.balance = newBalance;
|
||||
this.lifetimeEarned += amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove currency from balance (withdraw)
|
||||
* @param amount Amount to remove
|
||||
* @return true if successful, false if insufficient funds
|
||||
*/
|
||||
@Override
|
||||
public boolean withdraw(long amount) {
|
||||
if (amount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shouldUseVault()) {
|
||||
String playerName = getPlayerName();
|
||||
if (playerName != null) {
|
||||
boolean success = VaultUtil.withdrawMoney(playerName, amount);
|
||||
if (success) {
|
||||
this.lifetimeSpent += amount;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
// Built-in currency
|
||||
if (this.balance < amount) {
|
||||
return false;
|
||||
}
|
||||
this.balance -= amount;
|
||||
this.lifetimeSpent += amount;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player can afford an amount
|
||||
*/
|
||||
@Override
|
||||
public boolean canAfford(long amount) {
|
||||
if (shouldUseVault()) {
|
||||
String playerName = getPlayerName();
|
||||
if (playerName != null) {
|
||||
return VaultUtil.has(playerName, amount);
|
||||
}
|
||||
}
|
||||
return this.balance >= amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lifetime earned currency
|
||||
*/
|
||||
@Override
|
||||
public long getLifetimeEarned() {
|
||||
return lifetimeEarned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lifetime spent currency
|
||||
*/
|
||||
@Override
|
||||
public long getLifetimeSpent() {
|
||||
return lifetimeSpent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the balance for display
|
||||
*/
|
||||
@Override
|
||||
public String formatBalance() {
|
||||
if (shouldUseVault()) {
|
||||
return VaultUtil.format(getBalance());
|
||||
}
|
||||
return formatAmount(this.balance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an amount for display
|
||||
*/
|
||||
public static String formatAmount(long amount) {
|
||||
return String.format("%,d %s", amount, ConfigMarket.CurrencyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Vault is being used for currency operations.
|
||||
* Returns true only if config is enabled AND Vault is actually available.
|
||||
*/
|
||||
@Override
|
||||
public boolean isUsingVault() {
|
||||
return shouldUseVault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Vault is configured (regardless of availability)
|
||||
*/
|
||||
public boolean isVaultConfigured() {
|
||||
return ConfigMarket.UseVault;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.LogWriter;
|
||||
import noppes.npcs.api.entity.ICustomNpc;
|
||||
import noppes.npcs.api.handler.IPlayerBankData;
|
||||
import noppes.npcs.api.handler.IPlayerCurrencyData;
|
||||
import noppes.npcs.api.handler.IPlayerData;
|
||||
import noppes.npcs.api.handler.IPlayerDialogData;
|
||||
import noppes.npcs.api.handler.IPlayerFactionData;
|
||||
@@ -59,6 +60,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData {
|
||||
public DataSkinOverlays skinOverlays = new DataSkinOverlays(this);
|
||||
public MagicData magicData = new MagicData();
|
||||
|
||||
// Currency data - NOT slot-bound, shared across all profile slots
|
||||
public PlayerCurrencyData currencyData = new PlayerCurrencyData(this);
|
||||
|
||||
public ActionManager actionManager = new ActionManager();
|
||||
public PlayerDataScript scriptData;
|
||||
|
||||
@@ -397,6 +401,10 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData {
|
||||
return mailData;
|
||||
}
|
||||
|
||||
public IPlayerCurrencyData getCurrencyData() {
|
||||
return currencyData;
|
||||
}
|
||||
|
||||
public synchronized void save() {
|
||||
// Don't Save this is a Modification of a Profile's PlayerData
|
||||
Profile profile = ProfileController.Instance.getProfile(UUID.fromString(uuid));
|
||||
@@ -406,6 +414,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData {
|
||||
}
|
||||
|
||||
final NBTTagCompound compound = getNBT();
|
||||
// Currency data is saved at player level, NOT per profile slot
|
||||
currencyData.writeToNBT(compound);
|
||||
|
||||
final String filename;
|
||||
if (ConfigMain.DatFormat) {
|
||||
filename = uuid + ".dat";
|
||||
@@ -442,6 +453,10 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData {
|
||||
if (data.hasNoTags()) {
|
||||
data = getNBT();
|
||||
}
|
||||
|
||||
// Load currency BEFORE setNBT - currency is player-level, NOT per profile slot
|
||||
currencyData.readFromNBT(data);
|
||||
|
||||
setNBT(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
package noppes.npcs.controllers.data;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import noppes.npcs.constants.EnumStockReset;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Manages trader stock system including:
|
||||
* - Per-slot stock limits
|
||||
* - Per-player vs per-server stock tracking
|
||||
* - Automatic stock reset timers
|
||||
*/
|
||||
public class TraderStock {
|
||||
// Stock configuration
|
||||
public boolean enableStock = false;
|
||||
public boolean perPlayer = false; // false = per-server, true = per-player
|
||||
|
||||
// Reset configuration (mirrors Quest cooldown system)
|
||||
public EnumStockReset resetType = EnumStockReset.NONE;
|
||||
public long customResetTime = 0; // For MCCUSTOM/RLCUSTOM types (in ticks or ms)
|
||||
|
||||
// Stock quantities per slot (max stock, -1 = unlimited)
|
||||
public int[] maxStock = new int[18];
|
||||
|
||||
// Current stock (per-server mode)
|
||||
public int[] currentStock = new int[18];
|
||||
public long lastResetTime = 0;
|
||||
|
||||
// Per-player stock tracking
|
||||
public HashMap<String, PlayerTraderStock> playerStock = new HashMap<>();
|
||||
|
||||
public TraderStock() {
|
||||
for (int i = 0; i < 18; i++) {
|
||||
maxStock[i] = -1; // -1 = unlimited
|
||||
currentStock[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available stock for a slot
|
||||
* @param slot The slot index
|
||||
* @param playerName The player name (for per-player mode)
|
||||
* @return Available stock count, or Integer.MAX_VALUE if unlimited
|
||||
*/
|
||||
public synchronized int getAvailableStock(int slot, String playerName) {
|
||||
if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) {
|
||||
return Integer.MAX_VALUE; // Unlimited
|
||||
}
|
||||
|
||||
if (perPlayer) {
|
||||
PlayerTraderStock pStock = playerStock.get(playerName);
|
||||
if (pStock == null) {
|
||||
return maxStock[slot]; // Not purchased yet
|
||||
}
|
||||
return pStock.getStock(slot, maxStock[slot]);
|
||||
}
|
||||
|
||||
if (currentStock[slot] < 0) {
|
||||
return maxStock[slot];
|
||||
}
|
||||
return Math.max(0, currentStock[slot]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if stock is available for purchase
|
||||
*/
|
||||
public boolean hasStock(int slot, String playerName, int amount) {
|
||||
return getAvailableStock(slot, playerName) >= amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume stock when purchase is made
|
||||
* @return true if successful, false if insufficient stock
|
||||
*/
|
||||
public synchronized boolean consumeStock(int slot, String playerName, int amount) {
|
||||
if (!enableStock || slot < 0 || slot >= 18 || maxStock[slot] < 0) {
|
||||
return true; // Unlimited, always succeeds
|
||||
}
|
||||
|
||||
if (perPlayer) {
|
||||
PlayerTraderStock pStock = playerStock.computeIfAbsent(
|
||||
playerName, k -> new PlayerTraderStock());
|
||||
return pStock.consumeStock(slot, maxStock[slot], amount);
|
||||
}
|
||||
|
||||
if (currentStock[slot] < 0) {
|
||||
currentStock[slot] = maxStock[slot];
|
||||
}
|
||||
if (currentStock[slot] >= amount) {
|
||||
currentStock[slot] -= amount;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if stock should reset based on current time
|
||||
* @param currentTime Current MC world time (ticks) or real time (ms) depending on reset type
|
||||
*/
|
||||
public boolean shouldReset(long currentTime) {
|
||||
if (resetType == EnumStockReset.NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long elapsed = currentTime - lastResetTime;
|
||||
long resetInterval = getResetInterval();
|
||||
|
||||
return elapsed >= resetInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all stock to max values
|
||||
* @param currentTime Current time to record as reset time
|
||||
*/
|
||||
public synchronized void resetStock(long currentTime) {
|
||||
lastResetTime = currentTime;
|
||||
|
||||
// Reset per-server stock
|
||||
for (int i = 0; i < 18; i++) {
|
||||
currentStock[i] = maxStock[i];
|
||||
}
|
||||
|
||||
// Clear per-player stock (cleanup per-player data on reset)
|
||||
playerStock.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reset interval based on reset type
|
||||
* @return Interval in ticks (MC time) or milliseconds (real time)
|
||||
*/
|
||||
public long getResetInterval() {
|
||||
switch (resetType) {
|
||||
case MCDAILY:
|
||||
return 24000; // 1 MC day in ticks
|
||||
case MCWEEKLY:
|
||||
return 168000; // 7 MC days in ticks
|
||||
case MCCUSTOM:
|
||||
return customResetTime; // Custom ticks
|
||||
case RLDAILY:
|
||||
return 86400000L; // 24 hours in ms
|
||||
case RLWEEKLY:
|
||||
return 604800000L; // 7 days in ms
|
||||
case RLCUSTOM:
|
||||
return customResetTime; // Custom ms
|
||||
default:
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time until next reset
|
||||
* @param currentTime Current time
|
||||
* @return Time remaining until reset, or -1 if no reset scheduled
|
||||
*/
|
||||
public long getTimeUntilReset(long currentTime) {
|
||||
if (resetType == EnumStockReset.NONE) {
|
||||
return -1;
|
||||
}
|
||||
long elapsed = currentTime - lastResetTime;
|
||||
long interval = getResetInterval();
|
||||
return Math.max(0, interval - elapsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time until reset for display
|
||||
* @param currentTime Current time
|
||||
* @return Formatted string like "2 hours" or "1 day, 5 hours"
|
||||
*/
|
||||
public String getTimeUntilResetFormatted(long currentTime) {
|
||||
long remaining = getTimeUntilReset(currentTime);
|
||||
if (remaining < 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (resetType.isRealTime()) {
|
||||
// Convert ms to readable format
|
||||
long hours = remaining / 3600000L;
|
||||
long days = hours / 24;
|
||||
hours = hours % 24;
|
||||
|
||||
if (days > 0) {
|
||||
return days + " day" + (days != 1 ? "s" : "") +
|
||||
(hours > 0 ? ", " + hours + " hour" + (hours != 1 ? "s" : "") : "");
|
||||
} else if (hours > 0) {
|
||||
return hours + " hour" + (hours != 1 ? "s" : "");
|
||||
} else {
|
||||
long minutes = remaining / 60000L;
|
||||
return minutes + " minute" + (minutes != 1 ? "s" : "");
|
||||
}
|
||||
} else {
|
||||
// Convert ticks to MC time format
|
||||
long mcHours = remaining / 1000; // 1000 ticks = 1 MC hour
|
||||
long mcDays = mcHours / 24;
|
||||
mcHours = mcHours % 24;
|
||||
|
||||
if (mcDays > 0) {
|
||||
return mcDays + " MC day" + (mcDays != 1 ? "s" : "") +
|
||||
(mcHours > 0 ? ", " + mcHours + " MC hour" + (mcHours != 1 ? "s" : "") : "");
|
||||
} else {
|
||||
return mcHours + " MC hour" + (mcHours != 1 ? "s" : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set stock for a specific slot
|
||||
*/
|
||||
public void setMaxStock(int slot, int amount) {
|
||||
if (slot >= 0 && slot < 18) {
|
||||
maxStock[slot] = amount;
|
||||
if (currentStock[slot] < 0 || currentStock[slot] > amount) {
|
||||
currentStock[slot] = amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and clamp all current stock values to not exceed max stock.
|
||||
* Should be called when trader GUI opens to ensure consistency.
|
||||
* @return true if any stock was clamped
|
||||
*/
|
||||
public boolean validateStock() {
|
||||
boolean changed = false;
|
||||
for (int i = 0; i < 18; i++) {
|
||||
if (maxStock[i] >= 0) {
|
||||
// Has a max limit
|
||||
if (currentStock[i] < 0) {
|
||||
// Not initialized, set to max
|
||||
currentStock[i] = maxStock[i];
|
||||
changed = true;
|
||||
} else if (currentStock[i] > maxStock[i]) {
|
||||
// Exceeds max, clamp down
|
||||
currentStock[i] = maxStock[i];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also validate per-player stock
|
||||
for (PlayerTraderStock pStock : playerStock.values()) {
|
||||
for (int i = 0; i < 18; i++) {
|
||||
if (maxStock[i] >= 0) {
|
||||
int available = pStock.getStock(i, maxStock[i]);
|
||||
if (available < 0) {
|
||||
// Over-purchased somehow, reset
|
||||
pStock.purchasedAmounts[i] = maxStock[i];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ==================== NBT Serialization ====================
|
||||
|
||||
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
|
||||
compound.setBoolean("EnableStock", enableStock);
|
||||
compound.setBoolean("PerPlayer", perPlayer);
|
||||
compound.setInteger("ResetType", resetType.ordinal());
|
||||
compound.setLong("CustomResetTime", customResetTime);
|
||||
compound.setIntArray("MaxStock", maxStock);
|
||||
compound.setIntArray("CurrentStock", currentStock);
|
||||
compound.setLong("LastResetTime", lastResetTime);
|
||||
|
||||
// Save per-player stock
|
||||
NBTTagList playerList = new NBTTagList();
|
||||
for (Map.Entry<String, PlayerTraderStock> entry : playerStock.entrySet()) {
|
||||
NBTTagCompound playerTag = new NBTTagCompound();
|
||||
playerTag.setString("Player", entry.getKey());
|
||||
entry.getValue().writeToNBT(playerTag);
|
||||
playerList.appendTag(playerTag);
|
||||
}
|
||||
compound.setTag("PlayerStock", playerList);
|
||||
|
||||
return compound;
|
||||
}
|
||||
|
||||
public void readFromNBT(NBTTagCompound compound) {
|
||||
enableStock = compound.getBoolean("EnableStock");
|
||||
perPlayer = compound.getBoolean("PerPlayer");
|
||||
|
||||
int resetOrdinal = compound.getInteger("ResetType");
|
||||
if (resetOrdinal >= 0 && resetOrdinal < EnumStockReset.values().length) {
|
||||
resetType = EnumStockReset.values()[resetOrdinal];
|
||||
}
|
||||
|
||||
customResetTime = compound.getLong("CustomResetTime");
|
||||
|
||||
int[] loadedMax = compound.getIntArray("MaxStock");
|
||||
int[] loadedCurrent = compound.getIntArray("CurrentStock");
|
||||
|
||||
if (loadedMax != null && loadedMax.length == 18) {
|
||||
maxStock = loadedMax;
|
||||
}
|
||||
if (loadedCurrent != null && loadedCurrent.length == 18) {
|
||||
currentStock = loadedCurrent;
|
||||
}
|
||||
|
||||
lastResetTime = compound.getLong("LastResetTime");
|
||||
|
||||
// Load per-player stock
|
||||
playerStock.clear();
|
||||
NBTTagList playerList = compound.getTagList("PlayerStock", 10);
|
||||
for (int i = 0; i < playerList.tagCount(); i++) {
|
||||
NBTTagCompound playerTag = playerList.getCompoundTagAt(i);
|
||||
String playerName = playerTag.getString("Player");
|
||||
PlayerTraderStock pStock = new PlayerTraderStock();
|
||||
pStock.readFromNBT(playerTag);
|
||||
playerStock.put(playerName, pStock);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Inner Class for Per-Player Stock ====================
|
||||
|
||||
public static class PlayerTraderStock {
|
||||
public int[] purchasedAmounts = new int[18];
|
||||
|
||||
public PlayerTraderStock() {
|
||||
for (int i = 0; i < 18; i++) {
|
||||
purchasedAmounts[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int getStock(int slot, int maxStock) {
|
||||
if (slot < 0 || slot >= 18) return 0;
|
||||
return Math.max(0, maxStock - purchasedAmounts[slot]);
|
||||
}
|
||||
|
||||
public boolean consumeStock(int slot, int maxStock, int amount) {
|
||||
if (slot < 0 || slot >= 18) return false;
|
||||
if (getStock(slot, maxStock) >= amount) {
|
||||
purchasedAmounts[slot] += amount;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void writeToNBT(NBTTagCompound compound) {
|
||||
compound.setIntArray("Purchased", purchasedAmounts);
|
||||
}
|
||||
|
||||
public void readFromNBT(NBTTagCompound compound) {
|
||||
int[] loaded = compound.getIntArray("Purchased");
|
||||
if (loaded != null && loaded.length == 18) {
|
||||
purchasedAmounts = loaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1303,6 +1303,11 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
this.setRevengeTarget(null);
|
||||
this.deathTime = 0;
|
||||
//fleeingTick = 0;
|
||||
|
||||
// Restore hitbox before moving entity to prevent bounding box validation issues
|
||||
currentAnimation = EnumAnimation.NONE;
|
||||
updateHitbox();
|
||||
|
||||
if (ais.returnToStart && !hasOwner() && !this.isRemote())
|
||||
setLocationAndAngles(getStartXPos(), getStartYPos(), getStartZPos(), rotationYaw, rotationPitch);
|
||||
killedtime = 0;
|
||||
@@ -1316,8 +1321,6 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
}
|
||||
moveEntityWithHeading(0, 0);
|
||||
distanceWalkedModified = 0;
|
||||
currentAnimation = EnumAnimation.NONE;
|
||||
updateHitbox();
|
||||
updateAI = true;
|
||||
ais.movingPos = 0;
|
||||
if (getOwner() != null) {
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
package noppes.npcs.roles;
|
||||
|
||||
import foxz.utils.Market;
|
||||
import kamkeel.npcs.network.packets.data.large.GuiDataPacket;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import noppes.npcs.NBTTags;
|
||||
import noppes.npcs.NoppesUtilPlayer;
|
||||
import noppes.npcs.NoppesUtilServer;
|
||||
import noppes.npcs.NpcMiscInventory;
|
||||
import noppes.npcs.constants.EnumGuiType;
|
||||
import noppes.npcs.controllers.MarketRegistry;
|
||||
import noppes.npcs.controllers.data.PlayerData;
|
||||
import noppes.npcs.controllers.data.TraderStock;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -30,6 +38,15 @@ public class RoleTrader extends RoleInterface {
|
||||
public HashMap<String, int[]> playerPurchases;
|
||||
public HashMap<String, int[]> playerDisableSlot;
|
||||
|
||||
// Stock system
|
||||
public TraderStock stock = new TraderStock();
|
||||
|
||||
// Currency cost per trade slot (ADDITIVE to item costs, 0 = no currency required)
|
||||
public long[] currencyCost = new long[18];
|
||||
|
||||
// Local viewer tracking for non-linked traders (per-NPC sync)
|
||||
private final Set<EntityPlayerMP> localViewers = new HashSet<>();
|
||||
|
||||
public RoleTrader(EntityNPCInterface npc) {
|
||||
super(npc);
|
||||
inventoryCurrency = new NpcMiscInventory(36);
|
||||
@@ -59,14 +76,31 @@ public class RoleTrader extends RoleInterface {
|
||||
nbttagcompound.setIntArray("Purchases", purchases);
|
||||
nbttagcompound.setTag("PlayerPurchases", NBTTags.nbtStringIntegerArrayMap(playerPurchases));
|
||||
}
|
||||
|
||||
// Stock system
|
||||
nbttagcompound.setTag("Stock", stock.writeToNBT(new NBTTagCompound()));
|
||||
|
||||
// Currency cost per slot (additive to item costs)
|
||||
NBTTagList currencyList = new NBTTagList();
|
||||
for (long cost : currencyCost) {
|
||||
NBTTagCompound costTag = new NBTTagCompound();
|
||||
costTag.setLong("Cost", cost);
|
||||
currencyList.appendTag(costTag);
|
||||
}
|
||||
nbttagcompound.setTag("CurrencyCost", currencyList);
|
||||
|
||||
return nbttagcompound;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFromNBT(NBTTagCompound nbttagcompound) {
|
||||
String oldMarket = marketName;
|
||||
marketName = nbttagcompound.getString("TraderMarket");
|
||||
readNBT(nbttagcompound);
|
||||
|
||||
// Register with MarketRegistry for linked market sync
|
||||
MarketRegistry.updateTraderMarket(oldMarket, marketName, this);
|
||||
|
||||
try {
|
||||
Market.getMarket(this, marketName);
|
||||
} catch (Exception ex) {
|
||||
@@ -94,16 +128,56 @@ public class RoleTrader extends RoleInterface {
|
||||
disableSlot = new int[18];
|
||||
for (int i = 0; i < disableSlot.length; ++i) disableSlot[i] = 0;
|
||||
}
|
||||
|
||||
// Stock system
|
||||
if (nbttagcompound.hasKey("Stock")) {
|
||||
stock.readFromNBT(nbttagcompound.getCompoundTag("Stock"));
|
||||
}
|
||||
|
||||
// Currency cost per slot (additive to item costs)
|
||||
if (nbttagcompound.hasKey("CurrencyCost")) {
|
||||
NBTTagList currencyList = nbttagcompound.getTagList("CurrencyCost", 10);
|
||||
for (int i = 0; i < currencyList.tagCount() && i < 18; i++) {
|
||||
currencyCost[i] = currencyList.getCompoundTagAt(i).getLong("Cost");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interact(EntityPlayer player) {
|
||||
npc.say(player, npc.advanced.getInteractLine());
|
||||
|
||||
// Load from market FIRST to get latest shared data
|
||||
try {
|
||||
Market.getMarket(this, marketName);
|
||||
} catch (Exception ex) {
|
||||
Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex);
|
||||
}
|
||||
|
||||
// THEN check for stock reset (after loading latest market data)
|
||||
if (stock.enableStock) {
|
||||
long currentTime = stock.resetType.isRealTime()
|
||||
? System.currentTimeMillis()
|
||||
: player.worldObj.getTotalWorldTime();
|
||||
|
||||
boolean needsSave = false;
|
||||
|
||||
if (stock.shouldReset(currentTime)) {
|
||||
stock.resetStock(currentTime);
|
||||
needsSave = true;
|
||||
}
|
||||
|
||||
// Validate stock levels (clamp current to max if exceeded)
|
||||
if (stock.validateStock()) {
|
||||
needsSave = true;
|
||||
}
|
||||
|
||||
// Save to market if using shared market and changes were made
|
||||
if (needsSave && !marketName.isEmpty()) {
|
||||
Market.save(this, marketName);
|
||||
}
|
||||
}
|
||||
|
||||
NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerTrader, npc);
|
||||
}
|
||||
|
||||
@@ -138,4 +212,244 @@ public class RoleTrader extends RoleInterface {
|
||||
return map.get(name);
|
||||
}
|
||||
|
||||
// ==================== Stock System Methods ====================
|
||||
|
||||
/**
|
||||
* Check if there is stock available for a slot
|
||||
*/
|
||||
public boolean hasStock(int slot, String playerName, int amount) {
|
||||
if (!stock.enableStock) {
|
||||
return true; // Stock system disabled, always available
|
||||
}
|
||||
return stock.hasStock(slot, playerName, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available stock for a slot
|
||||
*/
|
||||
public int getAvailableStock(int slot, String playerName) {
|
||||
if (!stock.enableStock) {
|
||||
return Integer.MAX_VALUE; // Unlimited
|
||||
}
|
||||
return stock.getAvailableStock(slot, playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume stock after purchase.
|
||||
* For shared stock, syncs all viewers (market-wide for linked markets, per-NPC otherwise).
|
||||
*
|
||||
* @param slot The slot purchased from
|
||||
* @param playerName The player making the purchase
|
||||
* @param amount Amount to consume
|
||||
* @return true if stock was consumed successfully
|
||||
*/
|
||||
public boolean consumeStock(int slot, String playerName, int amount) {
|
||||
if (!stock.enableStock) {
|
||||
return true; // Stock system disabled
|
||||
}
|
||||
boolean consumed = stock.consumeStock(slot, playerName, amount);
|
||||
if (consumed) {
|
||||
// Save to market if linked
|
||||
if (!marketName.isEmpty()) {
|
||||
Market.save(this, marketName);
|
||||
}
|
||||
|
||||
// Sync viewers for shared stock (not per-player)
|
||||
if (!stock.perPlayer) {
|
||||
if (!marketName.isEmpty()) {
|
||||
// Linked market: sync all traders and viewers market-wide
|
||||
MarketRegistry.syncMarket(marketName);
|
||||
} else {
|
||||
// No linked market: sync only this NPC's viewers
|
||||
syncLocalViewers();
|
||||
}
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time until stock reset formatted for display
|
||||
*/
|
||||
public String getTimeUntilResetFormatted() {
|
||||
if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) {
|
||||
return "";
|
||||
}
|
||||
long currentTime = stock.resetType.isRealTime()
|
||||
? System.currentTimeMillis()
|
||||
: (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0);
|
||||
return stock.getTimeUntilResetFormatted(currentTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time until stock reset in real-time milliseconds.
|
||||
* Converts MC ticks to approximate real-time for client countdown display.
|
||||
* @return Remaining time in milliseconds, or -1 if no reset scheduled
|
||||
*/
|
||||
public long getResetTimeRemainingMillis() {
|
||||
if (!stock.enableStock || stock.resetType == noppes.npcs.constants.EnumStockReset.NONE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
long currentTime;
|
||||
long remaining;
|
||||
|
||||
if (stock.resetType.isRealTime()) {
|
||||
// Real time - already in milliseconds
|
||||
currentTime = System.currentTimeMillis();
|
||||
remaining = stock.getTimeUntilReset(currentTime);
|
||||
} else {
|
||||
// MC time - convert ticks to real milliseconds (1 tick = 50ms)
|
||||
currentTime = (npc != null && npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0);
|
||||
long remainingTicks = stock.getTimeUntilReset(currentTime);
|
||||
remaining = remainingTicks * 50L;
|
||||
}
|
||||
|
||||
return remaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the stock cooldown timer (for admin use)
|
||||
*/
|
||||
public void resetCooldown() {
|
||||
stock.lastResetTime = 0;
|
||||
}
|
||||
|
||||
// ==================== Currency Cost Methods ====================
|
||||
|
||||
/**
|
||||
* Get the currency cost for a slot (additive to item costs)
|
||||
*/
|
||||
public long getCurrencyCost(int slot) {
|
||||
if (slot < 0 || slot >= 18) return 0;
|
||||
return currencyCost[slot];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the currency cost for a slot (additive to item costs)
|
||||
*/
|
||||
public void setCurrencyCost(int slot, long cost) {
|
||||
if (slot >= 0 && slot < 18) {
|
||||
currencyCost[slot] = Math.max(0, cost);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a slot has a currency cost (greater than 0)
|
||||
*/
|
||||
public boolean hasCurrencyCost(int slot) {
|
||||
return slot >= 0 && slot < 18 && currencyCost[slot] > 0;
|
||||
}
|
||||
|
||||
// ==================== Viewer Tracking Methods ====================
|
||||
|
||||
/**
|
||||
* Register a player as viewing this trader's GUI.
|
||||
* - For linked markets: uses MarketRegistry (market-wide sync)
|
||||
* - For normal traders: uses local viewer set (per-NPC sync)
|
||||
*/
|
||||
public void registerViewer(EntityPlayerMP player) {
|
||||
if (player == null) return;
|
||||
|
||||
if (!marketName.isEmpty()) {
|
||||
// Linked market: use MarketRegistry for market-wide tracking
|
||||
MarketRegistry.registerViewer(marketName, player);
|
||||
} else {
|
||||
// No linked market: use local viewer set
|
||||
localViewers.add(player);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a player from viewing this trader's GUI.
|
||||
* - For linked markets: uses MarketRegistry
|
||||
* - For normal traders: uses local viewer set
|
||||
*/
|
||||
public void unregisterViewer(EntityPlayerMP player) {
|
||||
if (player == null) return;
|
||||
|
||||
if (!marketName.isEmpty()) {
|
||||
MarketRegistry.unregisterViewer(marketName, player);
|
||||
} else {
|
||||
localViewers.remove(player);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync trader data to all players viewing this trader.
|
||||
* - For linked markets: syncs ALL viewers across ALL NPCs sharing the market
|
||||
* - For normal traders: syncs only viewers of THIS specific NPC
|
||||
*/
|
||||
public void syncAllViewers() {
|
||||
if (!marketName.isEmpty()) {
|
||||
// Linked market: sync all viewers across all NPCs with this market
|
||||
MarketRegistry.syncMarketViewers(marketName);
|
||||
} else {
|
||||
// No linked market: sync only local viewers
|
||||
syncLocalViewers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync to all players viewing this specific NPC (for non-linked traders).
|
||||
*/
|
||||
private void syncLocalViewers() {
|
||||
// Remove disconnected players
|
||||
localViewers.removeIf(player -> player.playerNetServerHandler == null);
|
||||
|
||||
for (EntityPlayerMP viewer : localViewers) {
|
||||
syncToPlayer(viewer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this trader is unloaded (NPC death, chunk unload, etc.)
|
||||
* Cleans up MarketRegistry registration.
|
||||
*/
|
||||
public void onUnload() {
|
||||
if (!marketName.isEmpty()) {
|
||||
MarketRegistry.unregisterTrader(marketName, this);
|
||||
}
|
||||
localViewers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send trader data to a specific player
|
||||
*/
|
||||
public void syncToPlayer(EntityPlayerMP player) {
|
||||
if (player == null || player.playerNetServerHandler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
|
||||
// Player balance
|
||||
PlayerData data = PlayerData.get(player);
|
||||
compound.setLong("Balance", data.currencyData.getBalance());
|
||||
|
||||
// Stock info
|
||||
compound.setBoolean("StockEnabled", stock.enableStock);
|
||||
compound.setString("ResetTime", getTimeUntilResetFormatted());
|
||||
|
||||
// Available stock per slot (player-specific if perPlayer mode)
|
||||
String playerName = player.getCommandSenderName();
|
||||
int[] stockArr = new int[18];
|
||||
for (int i = 0; i < 18; i++) {
|
||||
stockArr[i] = getAvailableStock(i, playerName);
|
||||
}
|
||||
compound.setIntArray("Stock", stockArr);
|
||||
|
||||
// Currency cost per slot
|
||||
for (int i = 0; i < 18; i++) {
|
||||
compound.setLong("Cost" + i, getCurrencyCost(i));
|
||||
}
|
||||
|
||||
GuiDataPacket.sendGuiData(player, compound);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
onUnload();
|
||||
super.delete();
|
||||
}
|
||||
}
|
||||
@@ -895,4 +895,43 @@ public class ScriptPlayer<T extends EntityPlayerMP> extends ScriptLivingBase<T>
|
||||
}
|
||||
return (IPlayer[]) list.toArray(new IPlayer[list.size()]);
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// Currency Methods
|
||||
// =========================================
|
||||
|
||||
@Override
|
||||
public long getCurrencyBalance() {
|
||||
return getData().currencyData.getBalance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrencyBalance(long amount) {
|
||||
getData().currencyData.setBalance(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean depositCurrency(long amount) {
|
||||
return getData().currencyData.deposit(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean withdrawCurrency(long amount) {
|
||||
return getData().currencyData.withdraw(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canAffordCurrency(long amount) {
|
||||
return getData().currencyData.canAfford(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUsingVaultCurrency() {
|
||||
return getData().currencyData.isUsingVault();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFormattedCurrencyBalance() {
|
||||
return getData().currencyData.formatBalance();
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader
|
||||
public IItemStack getSellOption(int slot) {
|
||||
if (slot >= 18 || slot < 0) return null;
|
||||
if (role.inventorySold.items.get(slot) == null) return null;
|
||||
return NpcAPI.Instance().getIItemStack(role.inventoryCurrency.items.get(slot));
|
||||
return NpcAPI.Instance().getIItemStack(role.inventorySold.items.get(slot));
|
||||
}
|
||||
|
||||
public IItemStack[] getCurrency(int slot) {
|
||||
@@ -100,7 +100,7 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader
|
||||
|
||||
public boolean isSlotEnabled(int slot) {
|
||||
if (slot >= 18 || slot < 0) return false;
|
||||
return role.disableSlot[slot] > 0;
|
||||
return role.disableSlot[slot] <= 0; // enabled when NOT disabled
|
||||
}
|
||||
|
||||
public boolean isSlotEnabled(int slot, IPlayer player) {
|
||||
@@ -128,6 +128,132 @@ public class ScriptRoleTrader extends ScriptRoleInterface implements IRoleTrader
|
||||
role.getArrayByName(player.getDisplayName(), role.playerDisableSlot)[slot] = 0;
|
||||
}
|
||||
|
||||
// ==================== Stock System ====================
|
||||
|
||||
public boolean isStockEnabled() {
|
||||
return role.stock.enableStock;
|
||||
}
|
||||
|
||||
public void setStockEnabled(boolean enabled) {
|
||||
role.stock.enableStock = enabled;
|
||||
}
|
||||
|
||||
public boolean isPerPlayerStock() {
|
||||
return role.stock.perPlayer;
|
||||
}
|
||||
|
||||
public void setPerPlayerStock(boolean perPlayer) {
|
||||
role.stock.perPlayer = perPlayer;
|
||||
}
|
||||
|
||||
public int getStockResetType() {
|
||||
return role.stock.resetType.ordinal();
|
||||
}
|
||||
|
||||
public void setStockResetType(int type) {
|
||||
noppes.npcs.constants.EnumStockReset[] values = noppes.npcs.constants.EnumStockReset.values();
|
||||
if (type >= 0 && type < values.length) {
|
||||
role.stock.resetType = values[type];
|
||||
}
|
||||
}
|
||||
|
||||
public long getCustomResetTime() {
|
||||
return role.stock.customResetTime;
|
||||
}
|
||||
|
||||
public void setCustomResetTime(long time) {
|
||||
role.stock.customResetTime = Math.max(0, time);
|
||||
}
|
||||
|
||||
public int getMaxStock(int slot) {
|
||||
if (slot < 0 || slot >= 18) return -1;
|
||||
return role.stock.maxStock[slot];
|
||||
}
|
||||
|
||||
public void setMaxStock(int slot, int amount) {
|
||||
if (slot >= 0 && slot < 18) {
|
||||
role.stock.setMaxStock(slot, amount);
|
||||
}
|
||||
}
|
||||
|
||||
public int getAvailableStock(int slot) {
|
||||
if (slot < 0 || slot >= 18) return 0;
|
||||
return role.stock.getAvailableStock(slot, "");
|
||||
}
|
||||
|
||||
public int getAvailableStock(int slot, IPlayer player) {
|
||||
if (slot < 0 || slot >= 18) return 0;
|
||||
return role.stock.getAvailableStock(slot, player.getDisplayName());
|
||||
}
|
||||
|
||||
public void resetStock() {
|
||||
long currentTime = role.stock.resetType.isRealTime()
|
||||
? System.currentTimeMillis()
|
||||
: (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0);
|
||||
role.stock.resetStock(currentTime);
|
||||
}
|
||||
|
||||
public void resetCooldown() {
|
||||
long currentTime = role.stock.resetType.isRealTime()
|
||||
? System.currentTimeMillis()
|
||||
: (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0);
|
||||
role.stock.lastResetTime = currentTime;
|
||||
}
|
||||
|
||||
public int getCurrentStock(int slot) {
|
||||
if (slot < 0 || slot >= 18) return -1;
|
||||
return role.stock.currentStock[slot];
|
||||
}
|
||||
|
||||
public void setCurrentStock(int slot, int amount) {
|
||||
if (slot >= 0 && slot < 18) {
|
||||
role.stock.currentStock[slot] = amount;
|
||||
}
|
||||
}
|
||||
|
||||
public int getPlayerPurchased(int slot, IPlayer player) {
|
||||
if (slot < 0 || slot >= 18) return 0;
|
||||
noppes.npcs.controllers.data.TraderStock.PlayerTraderStock pStock =
|
||||
role.stock.playerStock.get(player.getDisplayName());
|
||||
if (pStock == null) return 0;
|
||||
return pStock.purchasedAmounts[slot];
|
||||
}
|
||||
|
||||
public void setPlayerPurchased(int slot, IPlayer player, int amount) {
|
||||
if (slot < 0 || slot >= 18) return;
|
||||
noppes.npcs.controllers.data.TraderStock.PlayerTraderStock pStock =
|
||||
role.stock.playerStock.computeIfAbsent(
|
||||
player.getDisplayName(),
|
||||
k -> new noppes.npcs.controllers.data.TraderStock.PlayerTraderStock()
|
||||
);
|
||||
pStock.purchasedAmounts[slot] = Math.max(0, amount);
|
||||
}
|
||||
|
||||
public long getLastResetTime() {
|
||||
return role.stock.lastResetTime;
|
||||
}
|
||||
|
||||
public long getTimeUntilReset() {
|
||||
long currentTime = role.stock.resetType.isRealTime()
|
||||
? System.currentTimeMillis()
|
||||
: (npc.worldObj != null ? npc.worldObj.getTotalWorldTime() : 0);
|
||||
return role.stock.getTimeUntilReset(currentTime);
|
||||
}
|
||||
|
||||
// ==================== Currency Cost System ====================
|
||||
|
||||
public long getCurrencyCost(int slot) {
|
||||
return role.getCurrencyCost(slot);
|
||||
}
|
||||
|
||||
public void setCurrencyCost(int slot, long cost) {
|
||||
role.setCurrencyCost(slot, cost);
|
||||
}
|
||||
|
||||
public boolean hasCurrencyCost(int slot) {
|
||||
return role.hasCurrencyCost(slot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return RoleType.TRADER;
|
||||
|
||||
@@ -429,10 +429,68 @@ role.mount.hover=Hover
|
||||
role.mount.ascendspeed=Ascend Speed
|
||||
role.mount.descendspeed=Descend Speed
|
||||
role.mount.walkspeed=Walk Speed: %s
|
||||
role.auctioneer=Auctioneer
|
||||
role.auctioneer.tabs=Tab Visibility
|
||||
role.auctioneer.showBrowse=Show Browse
|
||||
role.auctioneer.showMyListings=Show My Listings
|
||||
role.auctioneer.showMyBids=Show My Bids
|
||||
role.auctioneer.showClaims=Show Claims
|
||||
role.auctioneer.actions=Player Actions
|
||||
role.auctioneer.allowCreate=Allow Create
|
||||
role.auctioneer.allowBid=Allow Bid
|
||||
role.auctioneer.allowBuyout=Allow Buyout
|
||||
role.auctioneer.messages=Messages
|
||||
role.auctioneer.welcome=Welcome Message
|
||||
role.auctioneer.noAuction=Auction Disabled Message
|
||||
|
||||
trader.insufficient=Insufficient Currency
|
||||
trader.sufficient=Sufficient Currency
|
||||
trader.unavailable=Unavailable
|
||||
trader.insufficient.currency=Insufficient Balance
|
||||
trader.available=Available
|
||||
trader.slotdisabled=Slot Disabled
|
||||
trader.currencycost=Cost
|
||||
trader.outofstock=Out of Stock
|
||||
trader.instock=in stock
|
||||
trader.restock=Restock
|
||||
trader.balance=Balance
|
||||
trader.reopentrader=Reopen Trader
|
||||
|
||||
stock.options=Stock Options
|
||||
stock.enable=Enable Stock
|
||||
stock.perplayer=Per-Player
|
||||
stock.resettype=Reset Type
|
||||
stock.customtime=Custom Time
|
||||
stock.defaultmax=Default Max
|
||||
stock.perslot=Per-Slot Stock
|
||||
stock.reset=Reset Stock
|
||||
stock.resetcooldown=Reset Cooldown
|
||||
stock.perslot.title=Per-Slot Stock Limits
|
||||
stock.reset.confirm=Reset all stock?
|
||||
stock.reset.warning=This cannot be undone.
|
||||
stock.cooldown.confirm=Reset cooldown timer?
|
||||
stock.cooldown.warning=Stock will reset on next open.
|
||||
|
||||
stock.reset.never=Never
|
||||
stock.reset.mcdaily=MC Daily
|
||||
stock.reset.mcweekly=MC Weekly
|
||||
stock.reset.mccustom=MC Custom
|
||||
stock.reset.rldaily=Real Daily
|
||||
stock.reset.rlweekly=Real Weekly
|
||||
stock.reset.rlcustom=Real Custom
|
||||
|
||||
stock.options.hover=Configure stock limits and reset timers
|
||||
stock.enable.hover=Enable limited stock for trade slots
|
||||
stock.perplayer.hover=Track stock separately for each player
|
||||
stock.resettype.hover=When stock automatically replenishes
|
||||
stock.customtime.hover=Set a custom reset interval
|
||||
stock.defaultmax.hover=Default max stock for all slots (-1 = unlimited)
|
||||
stock.perslot.hover=Set max stock for individual slots
|
||||
stock.reset.hover=Reset all stock to max values
|
||||
stock.resetcooldown.hover=Force stock to reset on next trader open
|
||||
|
||||
gui.settings.hover=Configure trading options
|
||||
gui.ignoreDamage.hover=Ignore item damage when comparing currency
|
||||
gui.ignoreNBT.hover=Ignore NBT data when comparing currency
|
||||
gui.recordHistory.hover=Record purchase history for analytics
|
||||
|
||||
job.name=Job
|
||||
job.none=No Job
|
||||
@@ -1464,14 +1522,12 @@ ability.target.self=Self
|
||||
ability.target.aoe_self=AOE Self
|
||||
ability.target.aoe_target=AOE Target
|
||||
|
||||
ability.telegraph.none=None
|
||||
ability.telegraph.circle=Circle
|
||||
ability.telegraph.ring=Ring
|
||||
ability.telegraph.line=Line
|
||||
ability.telegraph.cone=Cone
|
||||
ability.telegraph.point=Point
|
||||
telegraph.circle=Circle
|
||||
telegraph.ring=Ring
|
||||
telegraph.line=Line
|
||||
telegraph.cone=Cone
|
||||
telegraph.point=Point
|
||||
|
||||
ability.tab.general=General
|
||||
ability.tab.timing=Timing
|
||||
ability.tab.effects=Effects
|
||||
ability.tab.telegraph=Telegraph
|
||||
@@ -1484,7 +1540,6 @@ ability.windUpSound=Wind Up Sound
|
||||
ability.activeSound=Active Sound
|
||||
ability.windUpAnim=Wind Up Anim
|
||||
ability.activeAnim=Active Anim
|
||||
gui.animations=Animations
|
||||
|
||||
ability.cnpc.slam=Slam
|
||||
ability.cnpc.heavy_hit=Heavy Hit
|
||||
@@ -1503,15 +1558,11 @@ ability.cnpc.hazard=Hazard
|
||||
ability.cnpc.trap=Trap
|
||||
|
||||
# Ability GUI - Type Tab
|
||||
ability.tab.type=Type
|
||||
|
||||
# Common Type Settings
|
||||
ability.damage=Damage
|
||||
ability.radius=Radius
|
||||
ability.knockback=Knockback
|
||||
ability.knockbackUp=KB Up
|
||||
ability.speed=Speed
|
||||
ability.range=Range
|
||||
ability.maxDist=Max Dist
|
||||
ability.minDist=Min Dist
|
||||
ability.distance=Distance
|
||||
@@ -1538,13 +1589,10 @@ ability.hitRadius=Hit Radius
|
||||
# Cutter Settings
|
||||
ability.arcAngle=Arc Angle
|
||||
ability.sweepWaves=Waves
|
||||
ability.waveInterval=Interval
|
||||
ability.rotSpeed=Rot Speed
|
||||
ability.piercing=Piercing
|
||||
|
||||
# Orb Settings
|
||||
ability.orbSpeed=Speed
|
||||
ability.orbSize=Size
|
||||
ability.homing=Homing
|
||||
ability.explosive=Explosive
|
||||
|
||||
@@ -1556,7 +1604,6 @@ ability.aoe=AOE
|
||||
ability.maxTargets=Max Targets
|
||||
|
||||
# Dash Settings
|
||||
ability.dashMode=Mode
|
||||
ability.dash.aggressive=Aggressive
|
||||
ability.dash.defensive=Defensive
|
||||
ability.mode=Mode
|
||||
@@ -1596,7 +1643,6 @@ ability.counterAnim=Counter Anim
|
||||
ability.healAmount=Amount
|
||||
ability.healPercent=Percent
|
||||
ability.healRadius=Radius
|
||||
ability.instant=Instant
|
||||
ability.healSelf=Heal Self
|
||||
ability.healAllies=Heal Allies
|
||||
|
||||
@@ -1607,9 +1653,6 @@ ability.shape=Shape
|
||||
ability.placement=Placement
|
||||
ability.affectsCaster=Hit Caster
|
||||
ability.ignoreInvuln=Ign Invuln
|
||||
ability.shape.circle=Circle
|
||||
ability.shape.ring=Ring
|
||||
ability.shape.cone=Cone
|
||||
ability.place.caster=At Caster
|
||||
ability.place.target=At Target
|
||||
ability.place.followCaster=Follow Caster
|
||||
@@ -1620,7 +1663,6 @@ ability.place.ahead=Ahead
|
||||
ability.triggerRadius=Trig Rad
|
||||
ability.armTime=Arm Time
|
||||
ability.maxTriggers=Max Trig
|
||||
ability.visible=Visible
|
||||
|
||||
# Ability Save/Load
|
||||
ability.save.confirm=Save ability preset
|
||||
@@ -1650,11 +1692,8 @@ ability.shockwaveFalloff=Damage/knockback drop off with distance
|
||||
|
||||
# Ability Config GUI
|
||||
ability.typeId=Type ID
|
||||
ability.tab.target=Target
|
||||
ability.conditions=Conditions
|
||||
ability.noTypeSettings=No type-specific settings
|
||||
ability.sounds=Sounds
|
||||
ability.animations=Animations
|
||||
|
||||
# Conditions
|
||||
condition.edit=Edit Condition
|
||||
|
||||
Reference in new issue
Block a user