mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-14 02:34:10 +00:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
921ec259ee
99 files changed
+4581
-1651
No files matched your search
@@ -1,729 +0,0 @@
|
||||
# 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: COMPLETE ✅
|
||||
|
||||
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
|
||||
- **Unified Usage** - Both Trader and Auction systems use `PlayerCurrencyData` which auto-delegates to Vault
|
||||
|
||||
### Vault Integration Flow
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PlayerCurrencyData │
|
||||
│ │
|
||||
│ shouldUseVault() = ConfigMarket.UseVault && VaultUtil.isEnabled()│
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ All methods check shouldUseVault() first: │ │
|
||||
│ │ - getBalance() → VaultUtil.getBalance() or local balance │ │
|
||||
│ │ - withdraw() → VaultUtil.withdrawMoney() or local │ │
|
||||
│ │ - deposit() → VaultUtil.addMoney() or local │ │
|
||||
│ │ - canAfford() → VaultUtil.has() or local check │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Used by: ContainerNPCTrader, AuctionController │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Key Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `PlayerTradeData.java` | Per-player trade data (currency + auction claims), Vault delegation |
|
||||
| `IPlayerTradeData.java` | API interface for trade operations (currency + claims) |
|
||||
| `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
|
||||
|
||||
// Via IPlayerData
|
||||
player.getData().getTradeData() // Get IPlayerTradeData interface
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Trader Enhancements
|
||||
|
||||
### Status: COMPLETE ✅
|
||||
|
||||
---
|
||||
|
||||
### 2.1 Stock System - COMPLETE ✅
|
||||
|
||||
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 - COMPLETE ✅
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Trader Currency Support - COMPLETE ✅
|
||||
|
||||
Trades can require BOTH items AND currency (additive costs). Uses `PlayerCurrencyData` which auto-delegates to Vault when enabled.
|
||||
|
||||
#### 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
|
||||
|
||||
#### Implementation Details
|
||||
- Currency cost stored as `long[] currencyCost[18]` in RoleTrader
|
||||
- Uses `data.tradeData.getBalance()` and `data.tradeData.withdraw()`
|
||||
- Automatically uses Vault when `ConfigMarket.UseVault` is enabled
|
||||
- NBT serialization for persistence ✅
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Player Trader GUI - COMPLETE ✅
|
||||
|
||||
#### 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 |
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Multi-Player Sync - COMPLETE ✅
|
||||
|
||||
MarketRegistry provides linked market synchronization across all NPCs sharing the same market.
|
||||
|
||||
---
|
||||
|
||||
### 2.5 Scripting API - COMPLETE ✅
|
||||
|
||||
Full API available via `IRoleTrader` interface.
|
||||
|
||||
---
|
||||
|
||||
## 3. Auction House System
|
||||
|
||||
### Status: COMPLETE ✅
|
||||
|
||||
### 3.1 Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AuctionController │
|
||||
│ - Manages auction listings with player indices │
|
||||
│ - Handles listing/bidding/buyout logic │
|
||||
│ - Uses ConcurrentHashMap for thread-safety (no locks) │
|
||||
│ - Async saves on CNPC+ thread every 30 seconds │
|
||||
│ - 30-second update cycle for processing ended auctions │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
Uses PlayerCurrencyData (Vault-aware)
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PlayerTradeData │
|
||||
│ - Combined currency + auction claims (shared across profiles) │
|
||||
│ - Currency: Vault integration with local fallback │
|
||||
│ - Claims: Thread-safe CopyOnWriteArrayList for async additions │
|
||||
│ - Expired claims processed on login │
|
||||
│ - Notifications sent immediately to online players │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Vault Integration
|
||||
|
||||
The Auction system uses `PlayerCurrencyData` for all currency operations:
|
||||
- `AuctionController.createListing()` → `currencyData.withdraw(fee)`
|
||||
- `AuctionController.placeBid()` → `currencyData.withdraw(bidAmount)`
|
||||
- `AuctionController.buyout()` → `currencyData.withdraw(buyoutPrice)`
|
||||
- `AuctionController.claimCurrency()` → `currencyData.deposit(amount)`
|
||||
|
||||
**When Vault is enabled** (`ConfigMarket.UseVault = true`):
|
||||
- All operations automatically use Vault economy
|
||||
- No code changes required - `PlayerCurrencyData` handles delegation
|
||||
|
||||
### 3.3 GUI Structure
|
||||
|
||||
```
|
||||
GuiAuctionInterface (Base GUI - opened via Auctioneer NPC)
|
||||
│
|
||||
├── GuiAuctionListing (Browse auctions - PAGE_LISTINGS)
|
||||
│ - 9x5 grid of auction items
|
||||
│ - Filter button with tooltip showing current settings
|
||||
│ - Click item → Opens GuiAuctionBidding
|
||||
│ - Pagination with up/down arrows
|
||||
│
|
||||
├── GuiAuctionSell (Create new listing - PAGE_SELL)
|
||||
│ - Sell slot for item to auction (cloned from inventory)
|
||||
│ - Click inventory: left=add stack, right=add 1
|
||||
│ - Click sell slot: left=clear, right=remove 1
|
||||
│ - Starting price + buyout price text fields
|
||||
│ - Allow Buyout toggle button
|
||||
│ - Listing fee display
|
||||
│ - Confirm button with coin icon
|
||||
│
|
||||
├── GuiAuctionTrades (My Trades - PAGE_CLAIMS)
|
||||
│ - 9x5 grid showing: active listings, active bids, claims
|
||||
│ - Blue tint: active listings/bids
|
||||
│ - Green tint: claimable items
|
||||
│ - Red tint: refunds
|
||||
│ - Confirmation system for cancel/claim operations
|
||||
│
|
||||
├── GuiAuctionBidding (View listing details)
|
||||
│ - Item display slot
|
||||
│ - Auction info (seller, price, time, bids)
|
||||
│ - Bid button → SubGuiAuctionBid
|
||||
│ - Buy Now button → SubGuiAuctionBuyNow
|
||||
│ - Own listings: buttons hidden, view-only
|
||||
│
|
||||
├── SubGuiAuctionBid (Bid confirmation popup)
|
||||
│ - Shows minimum bid, player balance
|
||||
│ - Bid amount input field
|
||||
│ - Place Bid / Cancel buttons
|
||||
│
|
||||
├── SubGuiAuctionBuyNow (Buyout confirmation popup)
|
||||
│ - Shows buyout price, player balance
|
||||
│ - Yes / Cancel buttons
|
||||
│
|
||||
└── SubGuiAuctionSearch (Filter popup)
|
||||
- Search text input
|
||||
- Apply/Cancel buttons
|
||||
```
|
||||
|
||||
### 3.4 Client Config Sync
|
||||
|
||||
The client uses `AuctionClientConfig` to cache server settings:
|
||||
- Synced via `LoginPacket` on player join
|
||||
- Client never uses `ConfigMarket` directly for display
|
||||
- Ensures server authority over fees, currency name, etc.
|
||||
|
||||
```java
|
||||
// Server sends config in LoginPacket
|
||||
AuctionClientConfig.writeToNBT(compound); // Server side
|
||||
|
||||
// Client receives and caches
|
||||
AuctionClientConfig.readFromNBT(compound); // Client side
|
||||
|
||||
// Client GUI uses cached values
|
||||
AuctionClientConfig.getListingFee();
|
||||
AuctionClientConfig.getCurrencyName();
|
||||
AuctionClientConfig.getMinBidIncrement();
|
||||
```
|
||||
|
||||
### 3.5 Data Structures
|
||||
|
||||
#### AuctionListing
|
||||
```java
|
||||
public class AuctionListing {
|
||||
String id; // Unique listing ID (UUID-based)
|
||||
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 {
|
||||
String id; // Unique claim ID
|
||||
UUID playerUUID; // Player who can claim
|
||||
String 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
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 Security Features
|
||||
|
||||
All actions are validated server-side:
|
||||
|
||||
| Check | Location | Description |
|
||||
|-------|----------|-------------|
|
||||
| Own item bid prevention | `placeBid()` | `listing.isSeller(playerUUID)` check |
|
||||
| Own item buyout prevention | `buyout()` | `listing.isSeller(playerUUID)` check |
|
||||
| Auction expiry check | `placeBid()/buyout()` | Double-check inside write lock |
|
||||
| Currency validation | All actions | Verify balance before operations |
|
||||
| Claim ownership | `claimItem()/claimCurrency()` | `claim.isForPlayer(playerUUID)` check |
|
||||
| Blacklist check | `createListing()` | `AuctionBlacklist.isBlacklistedForPlayer()` check |
|
||||
| Bound item check | `createListing()` | Auto-blocks Soulbound/Profile-Slotbound items |
|
||||
|
||||
### 3.7 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.8 Item Blacklist System
|
||||
|
||||
Prevents specific items from being listed on the auction house.
|
||||
|
||||
#### Blacklist Types
|
||||
|
||||
| Type | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| **Items** | Block specific items by registry name | `minecraft:diamond_sword` |
|
||||
| **Wildcards** | Block items matching pattern | `minecraft:diamond*` |
|
||||
| **Mods** | Block all items from a mod | `customnpcs` |
|
||||
| **NBT Tags** | Block items with specific NBT | `ench` (enchanted items) |
|
||||
|
||||
#### Auto-Blocked Items (Always Blocked)
|
||||
|
||||
These items are **always** blocked regardless of blacklist settings:
|
||||
- **Soulbound Items** - Items with `cnpc_soulbind` requirement
|
||||
- **Profile-Slotbound Items** - Items with `cnpc_profile_slot` requirement
|
||||
|
||||
The system checks for these under `RPGCore/Requirements` in the item's NBT.
|
||||
|
||||
#### Bypass Permission
|
||||
|
||||
Players with OP status or the `customnpcs.auction.blacklist.bypass` permission can list blacklisted items.
|
||||
|
||||
#### Configuration
|
||||
|
||||
```
|
||||
Market.Auction.Blacklist:
|
||||
Enabled: true
|
||||
BlacklistedItems: ["minecraft:barrier", "minecraft:command_block*"]
|
||||
BlacklistedMods: []
|
||||
BlacklistedNBTTags: []
|
||||
```
|
||||
|
||||
#### Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/cnpc auction blacklist add <item\|mod\|nbt> <value>` | Add to blacklist (runtime only) |
|
||||
| `/cnpc auction blacklist remove <item\|mod\|nbt> <value>` | Remove from blacklist (runtime only) |
|
||||
| `/cnpc auction blacklist list [item\|mod\|nbt]` | List blacklisted entries |
|
||||
| `/cnpc auction blacklist reload` | Reload blacklist from config |
|
||||
| `/cnpc auction blacklist check` | Check if held item is blacklisted |
|
||||
|
||||
---
|
||||
|
||||
## 4. AuctionController Performance & Thread Safety
|
||||
|
||||
### 4.1 Threading Model
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AuctionController │
|
||||
│ │
|
||||
│ ┌─────────────────────┐ ┌─────────────────┐ ┌───────────────┐ │
|
||||
│ │ ConcurrentHashMap │ │ AtomicBoolean │ │ CNPC+ Thread │ │
|
||||
│ │ (listings, indices) │ │ dirty, saving │ │ (async saves) │ │
|
||||
│ └─────────────────────┘ └─────────────────┘ └───────────────┘ │
|
||||
│ │
|
||||
│ All operations use ConcurrentHashMap - no explicit locking │
|
||||
│ Minecraft is single-threaded for gameplay - inherent safety │
|
||||
│ Async saves snapshot data before writing to disk │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PlayerTradeData │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Currency: long balance (or Vault delegation) │ │
|
||||
│ │ Claims: CopyOnWriteArrayList<AuctionClaim> │ │
|
||||
│ │ - Thread-safe for concurrent read/write │ │
|
||||
│ │ - Claims can be added from CNPC+ thread for offline players │ │
|
||||
│ │ - Stored in PlayerData (persisted with player, not auctions) │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 Player Indices for O(1) Lookups
|
||||
|
||||
| Index | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| `playerListingIds` | `Map<UUID, Set<String>>` | Active listings by seller |
|
||||
| `playerBidIds` | `Map<UUID, Set<String>>` | Listings where player has active bid |
|
||||
|
||||
**Claims are now stored in PlayerData:**
|
||||
- `PlayerTradeData.claims` - CopyOnWriteArrayList per player
|
||||
- Accessed via `PlayerData.get(player).tradeData`
|
||||
- Shared across all profile slots (combined with currency)
|
||||
|
||||
**Performance Impact:**
|
||||
- `getPlayerListingCount()`: O(n) → O(1)
|
||||
- `getPlayerActiveListings()`: O(n) → O(k) where k = player's listings
|
||||
- `getPlayerActiveBids()`: O(n) → O(k)
|
||||
- `getPlayerClaims()`: Direct access to player's claims list
|
||||
|
||||
### 4.3 Update Intervals
|
||||
|
||||
| Operation | Interval | Rationale |
|
||||
|-----------|----------|-----------|
|
||||
| Process ended auctions | 30 seconds (600 ticks) | Reduces server load |
|
||||
| Save auction data | 30 seconds (600 ticks) | Batches writes, reduces I/O |
|
||||
| Process expired claims | On player login | Per-player, not global tick |
|
||||
|
||||
### 4.4 Data Safety
|
||||
|
||||
1. **Currency Deduction** - Withdraw before updating listing
|
||||
2. **Previous Bidder Refund** - Store info before modification
|
||||
3. **Concurrent Saves** - `AtomicBoolean` prevents duplicates
|
||||
4. **Offline Player Claims** - Loaded async on CNPC+ thread, saved to PlayerData
|
||||
5. **Claim Migration** - Old claims in auction.dat migrated to PlayerData on load
|
||||
|
||||
### 4.5 Server Shutdown Handling
|
||||
|
||||
On `FMLServerStoppedEvent`:
|
||||
1. `AuctionController.save()` called synchronously
|
||||
2. Uses CNPC+ thread (single-threaded executor)
|
||||
3. All pending saves complete before shutdown
|
||||
|
||||
---
|
||||
|
||||
## 5. File Structure
|
||||
|
||||
### New Files Created
|
||||
|
||||
```
|
||||
constants/
|
||||
EnumAuctionStatus.java ✅
|
||||
EnumAuctionSort.java ✅
|
||||
EnumClaimType.java ✅
|
||||
EnumNotificationType.java ✅
|
||||
|
||||
controllers/
|
||||
AuctionController.java ✅ (with threading, indices, Vault support)
|
||||
AuctionConfigSync.java ✅ (server-side config sync utility)
|
||||
|
||||
controllers/data/
|
||||
AuctionListing.java ✅
|
||||
AuctionClaim.java ✅
|
||||
AuctionFilter.java ✅
|
||||
AuctionBlacklist.java ✅ (item blacklist checking + bypass)
|
||||
PlayerTradeData.java ✅ (currency + claims storage per player)
|
||||
|
||||
roles/
|
||||
RoleAuctioneer.java ✅
|
||||
|
||||
containers/
|
||||
ContainerAuction.java ✅
|
||||
ContainerAuctionListing.java ✅
|
||||
ContainerAuctionBidding.java ✅
|
||||
ContainerAuctionTrades.java ✅
|
||||
ContainerAuctionSell.java ✅
|
||||
InventoryAuctionDisplay.java ✅
|
||||
SlotAuctionDisplay.java ✅
|
||||
|
||||
client/
|
||||
AuctionClientConfig.java ✅ (client-side config cache)
|
||||
|
||||
client/gui/player/
|
||||
GuiAuctionInterface.java ✅
|
||||
GuiAuctionListing.java ✅
|
||||
GuiAuctionSell.java ✅
|
||||
GuiAuctionTrades.java ✅
|
||||
GuiAuctionBidding.java ✅
|
||||
SubGuiAuctionSearch.java ✅
|
||||
SubGuiAuctionBid.java ✅
|
||||
SubGuiAuctionBuyNow.java ✅
|
||||
AuctionTooltipHandler.java ✅
|
||||
|
||||
client/gui/util/
|
||||
GuiAuctionNavButton.java ✅
|
||||
|
||||
network/packets/player/
|
||||
AuctionActionPacket.java ✅
|
||||
```
|
||||
|
||||
### New Commands
|
||||
|
||||
```
|
||||
command/
|
||||
AuctionCommand.java ✅ /cnpc auction with list, view, open, blacklist
|
||||
command/auction/
|
||||
BlacklistSubCommand.java ✅ /cnpc auction blacklist subcommands
|
||||
```
|
||||
|
||||
### Files Modified
|
||||
|
||||
```
|
||||
CustomNpcs.java ✅ Initialize AuctionController, save on shutdown
|
||||
EnumRoleType.java ✅ Added Auctioneer
|
||||
EnumGuiType.java ✅ Added PlayerAuction, PlayerAuctionSell, PlayerAuctionTrades, PlayerAuctionBidding
|
||||
EnumPlayerPacket.java ✅ Added AuctionData, AuctionAction
|
||||
CommonProxy.java ✅ Handle auction container types
|
||||
ClientProxy.java ✅ Handle auction GUI types
|
||||
DataAdvanced.java ✅ Handle Auctioneer role
|
||||
ConfigMarket.java ✅ Added Auction section
|
||||
LoginPacket.java ✅ Sync auction config to client
|
||||
PlayerData.java ✅ Added tradeData field (currency + claims, shared across profiles)
|
||||
en_US.lang ✅ Added auction lang keys
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Configuration
|
||||
|
||||
### ConfigMarket.java Structure
|
||||
|
||||
```java
|
||||
// =========================================
|
||||
// Currency (Market.Currency)
|
||||
// =========================================
|
||||
public static boolean UseVault = false; // Use Vault economy
|
||||
public static String CurrencyName = "Coins"; // Display name
|
||||
public static long StartingBalance = 0; // New player balance
|
||||
public static long MaxBalance = Long.MAX_VALUE; // Balance cap
|
||||
|
||||
// =========================================
|
||||
// Auction (Market.Auction)
|
||||
// =========================================
|
||||
public static final int MAX_TRADE_SLOTS = 45; // 9x5 grid maximum
|
||||
public static final int MIN_TRADE_SLOTS = 1; // Minimum allowed
|
||||
|
||||
public static boolean AuctionEnabled = true;
|
||||
public static int AuctionDurationHours = 24; // Auction length (hours)
|
||||
public static long ListingFee = 10; // Fee to create listing
|
||||
public static long MinimumListingPrice = 1; // Minimum starting price
|
||||
public static double SalesTaxPercent = 0.05; // 5% tax on sales
|
||||
public static int DefaultMaxTrades = 8; // Per player (listings + bids + claims)
|
||||
public static int SnipeProtectionMinutes = 2; // Anti-snipe timer
|
||||
public static int ClaimExpirationDays = 20; // Days before unclaimed deleted
|
||||
public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase
|
||||
|
||||
// =========================================
|
||||
// Auction Logging
|
||||
// =========================================
|
||||
public static boolean AuctionLoggingEnabled = false;
|
||||
public static boolean LogAuctionCreated = true;
|
||||
public static boolean LogAuctionBid = true;
|
||||
public static boolean LogAuctionBuyout = true;
|
||||
public static boolean LogAuctionSold = true;
|
||||
public static boolean LogAuctionExpired = true;
|
||||
public static boolean LogAuctionCancelled = true;
|
||||
public static boolean LogAuctionClaimed = true;
|
||||
|
||||
// =========================================
|
||||
// Auction Blacklist (Market.Auction.Blacklist)
|
||||
// =========================================
|
||||
public static boolean BlacklistEnabled = true;
|
||||
public static String[] BlacklistedItems = {}; // Registry names (supports wildcards)
|
||||
public static String[] BlacklistedMods = {}; // Mod IDs to block entirely
|
||||
public static String[] BlacklistedNBTTags = {}; // NBT tag keys to block
|
||||
```
|
||||
|
||||
### Auction Permissions
|
||||
|
||||
| Permission | Description |
|
||||
|------------|-------------|
|
||||
| `customnpcs.auction.trades.*` | Unlimited trade slots (up to 45) |
|
||||
| `customnpcs.auction.trades.X` | Exactly X trade slots (where X is 1-45) |
|
||||
| `customnpcs.auction.blacklist.bypass` | Bypass item blacklist restrictions |
|
||||
|
||||
**Trade Slot Permissions:**
|
||||
- Default is 8 trade slots
|
||||
- Permissions only **increase** the limit (permissions below 8 are ignored)
|
||||
- Maximum is capped at 45 (9x5 grid)
|
||||
- Trade slots = Active Listings + Active Bids + Pending Claims
|
||||
|
||||
**Blacklist Bypass:**
|
||||
- OP players or those with `customnpcs.auction.blacklist.bypass` can list blacklisted items
|
||||
- Soulbound and Profile-Slotbound items are **always** blocked (no bypass)
|
||||
|
||||
---
|
||||
|
||||
## 7. Commands
|
||||
|
||||
### `/cnpc auction` Command
|
||||
|
||||
Allows interaction with the Auction House without needing an Auctioneer NPC.
|
||||
|
||||
| Subcommand | Usage | Permission | Description |
|
||||
|------------|-------|------------|-------------|
|
||||
| `list` | `/cnpc auction list [page]` | OP | View active auctions in chat |
|
||||
| `view` | `/cnpc auction view <player>` | OP | View a player's auction activity |
|
||||
| `open` | `/cnpc auction open` | Any | Opens the Auction House GUI |
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
/cnpc auction list - Show first page of active auctions
|
||||
/cnpc auction list 2 - Show page 2 of auctions
|
||||
/cnpc auction view Steve - Show Steve's listings, bids, and claims
|
||||
/cnpc auction open - Open the full Auction House GUI
|
||||
```
|
||||
|
||||
### `/cnpc auction blacklist` Command
|
||||
|
||||
Manage the auction item blacklist (runtime changes, not persisted to config).
|
||||
|
||||
| Subcommand | Usage | Permission | Description |
|
||||
|------------|-------|------------|-------------|
|
||||
| `add` | `/cnpc auction blacklist add <item\|mod\|nbt> <value>` | OP | Add to blacklist |
|
||||
| `remove` | `/cnpc auction blacklist remove <item\|mod\|nbt> <value>` | OP | Remove from blacklist |
|
||||
| `list` | `/cnpc auction blacklist list [item\|mod\|nbt]` | OP | List blacklisted entries |
|
||||
| `reload` | `/cnpc auction blacklist reload` | OP | Reload from config |
|
||||
| `check` | `/cnpc auction blacklist check` | Any | Check if held item is blacklisted |
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
/cnpc auction blacklist add item minecraft:diamond_sword
|
||||
/cnpc auction blacklist add item minecraft:diamond* (wildcard)
|
||||
/cnpc auction blacklist add mod industrialcraft2
|
||||
/cnpc auction blacklist add nbt ench
|
||||
/cnpc auction blacklist remove item minecraft:diamond_sword
|
||||
/cnpc auction blacklist list item
|
||||
/cnpc auction blacklist check (checks held item)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Status
|
||||
|
||||
### Phase 1: Currency System - COMPLETE ✅
|
||||
### Phase 2: Trader Enhancements - COMPLETE ✅
|
||||
### Phase 3: Auction Core - COMPLETE ✅
|
||||
### Phase 4: Auction GUI - COMPLETE ✅
|
||||
### Phase 5: Auction Network - COMPLETE ✅
|
||||
### Phase 6: Performance & Safety - COMPLETE ✅
|
||||
|
||||
1. ~~Threading model with ReentrantReadWriteLock~~
|
||||
2. ~~Player indices for O(1) lookups~~
|
||||
3. ~~20-second update cycle~~
|
||||
4. ~~Async saves on CNPC+ thread~~
|
||||
5. ~~Race condition prevention~~
|
||||
6. ~~Server shutdown handling~~
|
||||
7. ~~Own listing view (buttons hidden)~~
|
||||
8. ~~Server-side security checks~~
|
||||
|
||||
### Remaining Work
|
||||
|
||||
- [ ] **GuiManageAuction (Admin GUI)** - Admin interface to view all auctions, cancel listings, manage claims
|
||||
- [ ] Additional testing
|
||||
|
||||
### Implemented Since Initial Plan
|
||||
|
||||
- [x] **`/auction` Command** - Console/player command with subcommands:
|
||||
- `/auction list [page]` - View active auctions from command line
|
||||
- `/auction view <player>` - View a player's auction activity (listings, bids, claims)
|
||||
- `/auction open` - Open Auction House GUI without needing an Auctioneer NPC
|
||||
- [x] **Permission system integration** - Per-player trade slot limits via `customnpcs.auction.trades.X` permissions
|
||||
- Default: 8 trade slots
|
||||
- Maximum: 45 (9x5 grid)
|
||||
- Synced to client when viewing My Trades
|
||||
- Cached on login for performance
|
||||
- [x] **Minimum Listing Price** - Configurable minimum starting price (default: 1)
|
||||
- [x] **Buyout enabled by default** - Buyout toggle is ON by default when creating listings
|
||||
- [x] **Search by seller** - Filter can now search by item name OR seller name
|
||||
- [x] **Rebid prevention** - Players cannot rebid on auctions where they're already the highest bidder
|
||||
- [x] **Buyout price difference** - When buying out an auction you're bidding on, only pay the difference
|
||||
- [x] **Immediate claim cleanup** - Claims are removed from storage immediately when claimed
|
||||
- [x] **Listing cleanup** - Ended listings are removed when all related claims are processed
|
||||
- [x] **Live notifications** - Notifications sent immediately to online players instead of queued
|
||||
- [x] **Login claim summary** - Shows detailed claim summary on login (X items and Y currency to claim)
|
||||
- [x] **PlayerTradeData** - Merged currency + claims into single class (shared across profiles)
|
||||
- [x] **No locking system** - Pure ConcurrentHashMap for better performance
|
||||
- [x] **30-second intervals** - Combined process/save cycle for simplicity
|
||||
- [x] **Async offline player claims** - Claims added to offline players via CNPC+ thread
|
||||
- [x] **Claim migration** - Old claims in auction.dat automatically migrated to PlayerData
|
||||
- [x] **Item Blacklist System** - Block items by registry name, mod, NBT tag, or wildcard pattern
|
||||
- Auto-blocks Soulbound and Profile-Slotbound items (checks `RPGCore/Requirements`)
|
||||
- Per-player bypass via `customnpcs.auction.blacklist.bypass` permission
|
||||
- Runtime add/remove commands (not persisted to config)
|
||||
- Check command to test held item
|
||||
|
||||
---
|
||||
|
||||
## 9. Design Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| Claims Storage | In PlayerData, not auction.dat | Claims persist with player, reduces auction.dat size, allows offline claim addition |
|
||||
| Thread Safety | ConcurrentHashMap (no locks) | Minecraft is single-threaded for gameplay; locks add overhead without benefit |
|
||||
| Vault Integration | Via PlayerCurrencyData | Single point of delegation, reused by Trader & Auction |
|
||||
| Update Interval | 30 seconds | Performance vs responsiveness tradeoff |
|
||||
| Player Indices | ConcurrentHashMap + Sets | O(1) lookups, thread-safe |
|
||||
| Async Saves | CNPC+ Thread | Doesn't block server tick |
|
||||
| Offline Claims | Load PlayerData async, add claim, save | Claims can be added to offline players without blocking |
|
||||
| Config Sync | LoginPacket → AuctionClientConfig | Client never accesses server config directly |
|
||||
| Item Blacklist | Static utility class | Simple, fast checks without instantiation; auto-blocks bound items |
|
||||
| Bound Item Detection | Check `RPGCore/Requirements` NBT path | Uses AttributeItemUtil constants for consistency |
|
||||
|
||||
---
|
||||
|
||||
## 10. Future Considerations (Not in 1.11)
|
||||
|
||||
- Category-based filtering (by item type, mod, etc.)
|
||||
- Auction history/statistics tracking
|
||||
- Auction house tax sink (currency removed from economy)
|
||||
- Reserve prices (hidden minimum price)
|
||||
- Private auctions (invite-only)
|
||||
- Watchlist/favorites system
|
||||
- Price history graphs
|
||||
@@ -0,0 +1,144 @@
|
||||
# CNPC+ 1.11 Ability System Beta
|
||||
|
||||
🟡 **In Testing**
|
||||
|
||||
|
||||
|
||||
## Features: ⚒️
|
||||
|
||||
- **Phase-Based Execution:**
|
||||
|
||||
> All abilities follow a Wind-up → Active → Completion flow. Wind-up lets players react, Active deals the effect.
|
||||
|
||||
|
||||
|
||||
- **Melee Abilities:**
|
||||
|
||||
> - **Slam** - Leap and AOE on impact.
|
||||
> - **Heavy Hit** - Single powerful strike with bonus knockback.
|
||||
> - **Cutter** - Slashing cone arc attack.
|
||||
> - **Shockwave** - AOE burst radiating from the caster.
|
||||
|
||||
|
||||
|
||||
- **Energy Orbs:**
|
||||
|
||||
> - **Orb** - Homing energy sphere.
|
||||
> - **Dual Orb** - Two simultaneous homing orbs.
|
||||
> - **Orb Barrage** - Rapid volley of orbs in sequence.
|
||||
|
||||
|
||||
|
||||
- **Energy Discs:**
|
||||
|
||||
> - **Disc** - Spinning flat projectile with optional boomerang.
|
||||
> - **Dual Disc** - Two simultaneous discs.
|
||||
|
||||
|
||||
|
||||
- **Energy Beams:**
|
||||
|
||||
> - **Beam** - Sustained homing beam from caster to target.
|
||||
> - **Dual Beam** - Two simultaneous beams.
|
||||
|
||||
|
||||
|
||||
- **Laser Shot:**
|
||||
|
||||
> Instant-fire projectile with no homing. Pure aim.
|
||||
|
||||
|
||||
|
||||
- **Projectile:**
|
||||
|
||||
> Standard projectile using classic Minecraft physics.
|
||||
|
||||
|
||||
|
||||
- **Movement Abilities:**
|
||||
|
||||
> - **Charge** - Rush forward in a line, damaging everything in path.
|
||||
> - **Dash** - Quick directional movement without damage.
|
||||
> - **Teleport** - Instant relocation to the target.
|
||||
|
||||
|
||||
|
||||
- **AOE Abilities:**
|
||||
|
||||
> - **Sweeper** - Wide rotational sweep around the caster.
|
||||
> - **Vortex** - Pulls entities inward while dealing damage.
|
||||
|
||||
|
||||
|
||||
- **Telegraph System:**
|
||||
|
||||
> Visual ground indicators during wind-up. Supports Circle, Ring, Line, Cone, and Point shapes with color customization and warning phase transitions.
|
||||
|
||||
|
||||
|
||||
- **Energy Projectile Customization:**
|
||||
|
||||
> Modular data system for all energy projectiles: Anchor Point, Display Colors, Homing, Combat Stats, and Lifespan.
|
||||
|
||||
|
||||
|
||||
- **Ability Effects:**
|
||||
|
||||
> Abilities can apply potion effects on hit: Slowness, Weakness, Poison, Wither, Blindness, Nausea, Hunger, Mining Fatigue.
|
||||
|
||||
|
||||
|
||||
- **Ability Conditions:**
|
||||
|
||||
> Condition system controlling when abilities can execute based on target state and custom predicates.
|
||||
|
||||
|
||||
|
||||
- **Interruptible Wind-ups:**
|
||||
|
||||
> Abilities can be interrupted by damage during wind-up, forcing a configurable Dazed recovery state.
|
||||
|
||||
|
||||
|
||||
- **Movement & Rotation Locking:**
|
||||
|
||||
> Lock caster movement, rotation, or both during Wind-up, Active, or both phases.
|
||||
|
||||
|
||||
|
||||
- **Animation & Sound Integration:**
|
||||
|
||||
> Wind-up and Active animations with optional duration sync. Custom sound events per phase.
|
||||
|
||||
|
||||
|
||||
- **Player Abilities:**
|
||||
|
||||
> Players can unlock abilities, select one at a time, and share a universal cooldown.
|
||||
|
||||
|
||||
|
||||
- **Ability Preview:**
|
||||
|
||||
> Live preview in the ability editor GUI. Test telegraphs, animations, and projectiles on a dummy NPC.
|
||||
|
||||
|
||||
|
||||
- **Scripting API:**
|
||||
|
||||
> Full event support: Start, Execute, Interrupt, Complete, Hit (modifiable damage/knockback), and Tick events. Start, Execute, and Hit are cancelable.
|
||||
|
||||
|
||||
|
||||
- **Ability Commands:**
|
||||
|
||||
> `/kam ability list`, `types`, `info`, `delete`, `reload` for managing custom abilities.
|
||||
|
||||
|
||||
|
||||
**Extras**
|
||||
|
||||
- Global ability presets that update everywhere when edited.
|
||||
- Weighted ability selection for NPC random usage.
|
||||
- Configurable min/max range per ability.
|
||||
- Reusable telegraph presets shared across abilities.
|
||||
@@ -0,0 +1,199 @@
|
||||
# ⚔️ CustomNPC+ 1.11 - Abilities ⚔️
|
||||
|
||||
---
|
||||
|
||||
The **Ability Update** introduces a full combat ability system to CNPC+! Give your NPCs powerful attacks with **visual telegraphs**, launch **energy projectiles** that home in on targets, and let players unlock abilities of their own!
|
||||
|
||||
---
|
||||
|
||||
## How Abilities Work 🎯
|
||||
|
||||
Every ability follows a **phase-based execution flow** that gives combat a readable, strategic feel.
|
||||
|
||||
**Phases:**
|
||||
- **Wind-up** - The ability charges. Telegraphs appear, animations play, and observant players can react
|
||||
- **Active** - The ability fires. Damage is dealt, projectiles launch, effects apply
|
||||
- **Dazed** - Optional recovery state if the ability is interrupted during wind-up
|
||||
|
||||
**Core Configuration:**
|
||||
- **Cooldown** - Time before the ability can be used again
|
||||
- **Targeting Mode** - Aggro Target, Self, AOE on Self, or AOE on Target
|
||||
- **Range** - Configurable min and max range for activation
|
||||
- **Lock Movement** - Lock the caster's movement, rotation, or both during any phase
|
||||
- **Interruptible** - Allow damage to interrupt wind-up and force a dazed state
|
||||
- **Weight** - Priority weighting for NPC random ability selection
|
||||
|
||||
> Abilities support both **inline configuration** on individual NPCs and **global presets** that update everywhere when edited.
|
||||
|
||||
---
|
||||
|
||||
## Melee Abilities 🗡️
|
||||
|
||||
Close-range attacks that hit hard and punish positioning.
|
||||
|
||||
- **Slam** - Leap toward the target in an arc, dealing AOE damage on impact. Configurable radius, knockback, and leap height
|
||||
- **Heavy Hit** - A single powerful melee strike with bonus knockback
|
||||
- **Cutter** - A slashing arc attack in a cone. Configurable angle and reach
|
||||
- **Shockwave** - An AOE burst radiating outward from the caster
|
||||
|
||||
> All melee abilities support configurable **damage**, **knockback**, and **potion effects** on hit.
|
||||
|
||||
---
|
||||
|
||||
## Energy Projectiles ✨
|
||||
|
||||
A brand new projectile system built from the ground up with full visual customization.
|
||||
|
||||
### Orbs
|
||||
|
||||
Homing energy spheres that track their target through the air.
|
||||
|
||||
- **Orb** - A single homing projectile
|
||||
- **Dual Orb** - Fires two orbs simultaneously
|
||||
- **Orb Barrage** - Fires a volley of orbs in rapid sequence
|
||||
|
||||
### Discs
|
||||
|
||||
Flat spinning projectiles with optional **boomerang** behavior.
|
||||
|
||||
- **Disc** - A single spinning disc with configurable radius and thickness
|
||||
- **Dual Disc** - Fires two discs simultaneously
|
||||
|
||||
### Beams
|
||||
|
||||
Sustained energy beams that connect the caster to their target.
|
||||
|
||||
- **Beam** - A homing beam with configurable width and head size. Locks the caster in place during channeling
|
||||
- **Dual Beam** - Fires two beams simultaneously
|
||||
|
||||
### Laser Shot
|
||||
|
||||
An **instant-fire** projectile with configurable width and speed. No homing — pure aim.
|
||||
|
||||
### Projectile
|
||||
|
||||
A standard projectile using classic Minecraft physics for a more traditional ranged attack.
|
||||
|
||||
### Energy Configuration
|
||||
|
||||
Every energy projectile shares a **modular data system** for deep customization:
|
||||
|
||||
- **Anchor Point** - Choose where the projectile spawns: Right Hand, Left Hand, Front, Center, Chest, or Above Head
|
||||
- **Display** - Set core color, edge color, glow effect, transparency, and size
|
||||
- **Homing** - Toggle homing, adjust turn rate and max tracking range
|
||||
- **Combat** - Damage, knockback, vertical knockback, hit cooldown, and optional AOE splash
|
||||
- **Lifespan** - Projectile speed, max age, and max travel distance
|
||||
|
||||
---
|
||||
|
||||
## Movement Abilities 💨
|
||||
|
||||
Abilities that reposition the caster in combat.
|
||||
|
||||
- **Charge** - Rush forward in a line, damaging all entities in the path. Configurable speed, hit width, and knockback
|
||||
- **Dash** - A quick directional movement without dealing damage
|
||||
- **Teleport** - Instantly relocate to the target's position
|
||||
|
||||
> **Charge** locks direction during wind-up, so the telegraph accurately shows the attack path.
|
||||
|
||||
---
|
||||
|
||||
## AOE Abilities 🌀
|
||||
|
||||
Area control abilities that dominate space.
|
||||
|
||||
- **Sweeper** - A wide rotational sweep attack around the caster with configurable radius and rotation speed
|
||||
- **Vortex** - Creates a pulling force that drags nearby entities toward the center while dealing damage
|
||||
|
||||
---
|
||||
|
||||
## Telegraph System 📍
|
||||
|
||||
**Telegraphs** are visual ground indicators that warn players before an ability lands. They appear during the wind-up phase and disappear when the ability fires.
|
||||
|
||||
**Shapes:**
|
||||
- **Circle** - Filled circle with configurable radius
|
||||
- **Ring** - Donut shape with inner and outer radius
|
||||
- **Line** - Rectangle along the attack direction
|
||||
- **Cone** - Fan/wedge shape with configurable angle
|
||||
- **Point** - Single point indicator
|
||||
|
||||
**Features:**
|
||||
- **Color Customization** - Set ARGB colors for both the telegraph and its warning phase
|
||||
- **Warning Phase** - Color shifts in the final ticks before the ability fires
|
||||
- **Entity Tracking** - Telegraphs can follow a moving entity or stay at a fixed position
|
||||
- **Smooth Interpolation** - Position updates are interpolated for clean visuals
|
||||
|
||||
> Telegraphs are saved as **reusable presets** and can be shared across multiple abilities.
|
||||
|
||||
---
|
||||
|
||||
## Animations & Sound 🎬
|
||||
|
||||
Abilities integrate directly with the animation and sound systems.
|
||||
|
||||
- **Wind-up Animation** - Plays during the charging phase
|
||||
- **Active Animation** - Plays when the ability fires
|
||||
- **Animation Sync** - Optionally sync wind-up duration to match animation length exactly
|
||||
- **Sound Events** - Assign custom sounds to both wind-up and active phases
|
||||
- **Phase Colors** - Visual color feedback on the caster during wind-up and active
|
||||
|
||||
> Many ability types include **built-in animation names** that work automatically when matching animations exist.
|
||||
|
||||
---
|
||||
|
||||
## Effects & Conditions ⚡
|
||||
|
||||
### Potion Effects
|
||||
|
||||
Abilities can apply **potion effects** to any entity they hit. Each effect has configurable type, duration, and amplifier.
|
||||
|
||||
**Available Effects:**
|
||||
Slowness, Weakness, Poison, Wither, Blindness, Nausea, Hunger, Mining Fatigue
|
||||
|
||||
### Conditions
|
||||
|
||||
Abilities support a **condition system** that controls when they can be used. Conditions are evaluated before execution and can require specific target states, ranges, or custom predicates.
|
||||
|
||||
---
|
||||
|
||||
## Player Abilities 🧑
|
||||
|
||||
Players can unlock and use abilities too!
|
||||
|
||||
- **Unlock System** - Players receive abilities via scripting or commands
|
||||
- **Ability Selection** - Choose one ability at a time from your unlocked list
|
||||
- **Universal Cooldown** - Shared cooldown across all player abilities
|
||||
|
||||
---
|
||||
|
||||
## Scripting API 📜
|
||||
|
||||
Full scripting support for ability lifecycle events:
|
||||
|
||||
- **StartEvent** *(Cancelable)* - Fired when wind-up begins
|
||||
- **ExecuteEvent** *(Cancelable)* - Fired when the ability activates
|
||||
- **InterruptEvent** - Fired when wind-up is interrupted by damage
|
||||
- **CompleteEvent** - Fired when the ability finishes
|
||||
- **HitEvent** *(Cancelable)* - Fired on each entity hit. Damage and knockback values can be modified
|
||||
- **TickEvent** - Fired every tick with current phase and tick count
|
||||
|
||||
> Scripts can cancel, modify damage, and react to every phase of ability execution.
|
||||
|
||||
---
|
||||
|
||||
## Ability Preview 🔎
|
||||
|
||||
The ability editor includes a **live preview system**. Test your telegraph shapes, animations, and projectile visuals directly in the GUI on a dummy NPC — no world effects applied.
|
||||
|
||||
---
|
||||
|
||||
## Ability Commands 💬
|
||||
|
||||
- `/kam ability list` - List all custom abilities
|
||||
- `/kam ability types` - List all registered ability types
|
||||
- `/kam ability info <name|uuid>` - View ability details
|
||||
- `/kam ability delete <uuid>` - Delete a custom ability
|
||||
- `/kam ability reload` - Reload abilities from disk
|
||||
|
||||
---
|
||||
@@ -1,301 +0,0 @@
|
||||
# Ability System Review - CustomNPC+
|
||||
|
||||
A comprehensive technical review of the Ability System identifying bugs, logic issues, incomplete features, and inconsistencies.
|
||||
|
||||
---
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### 1. Cooldown Uses Mixed Time Systems
|
||||
**Status:** FIXED
|
||||
**Files:**
|
||||
- `Ability.java` - Removed cooldown tracking (cooldownEndTime, startCooldown(), isOnCooldown(), canUse())
|
||||
- `DataAbilities.java` - Now sole owner of cooldown management using world time
|
||||
|
||||
**Solution:** Removed all cooldown tracking from `Ability.java`. Cooldowns are now managed exclusively by `DataAbilities` using `world.getTotalWorldTime()` for consistency.
|
||||
|
||||
---
|
||||
|
||||
### 2. AbilityHeal - Heal Over Time Completely Broken
|
||||
**Status:** FIXED
|
||||
**File:** `AbilityHeal.java`
|
||||
|
||||
**Solution:** Refactored to separate ally-finding from healing. The `findAlliesInRadius()` method is now called regardless of `instantHeal` flag, populating the list for HoT to use. Added heal particles during HoT ticks.
|
||||
|
||||
---
|
||||
|
||||
### 3. AbilityGuard - Counter Integration Missing
|
||||
**Status:** FIXED
|
||||
**Files:**
|
||||
- `DataAbilities.java:onDamage()` - Added integration to call `AbilityGuard.onDamageTaken()`
|
||||
- `DataAbilities.java:getGuardDamageReduction()` - Added helper method for damage reduction
|
||||
|
||||
**Solution:** `DataAbilities.onDamage()` now checks if current ability is an `AbilityGuard` and calls `onDamageTaken()` when the NPC is hit while guarding.
|
||||
|
||||
---
|
||||
|
||||
## High Priority Issues
|
||||
|
||||
### 4. knockbackUp Stored But Never Applied
|
||||
**Status:** FIXED
|
||||
**File:** `Ability.java`
|
||||
|
||||
**Solution:** Updated `applyNpcKnockback()` to accept a `knockbackUp` parameter and use it instead of hardcoded `0.1D`. Both `applyAbilityDamage()` and `applyAbilityDamageWithDirection()` now pass the knockbackUp value from the HitEvent.
|
||||
|
||||
---
|
||||
|
||||
### 5. Targeting Modes Not Implemented
|
||||
**Status:** FIXED (Removed)
|
||||
**File:** `TargetingMode.java`
|
||||
|
||||
**Solution:** Removed the unused targeting modes (`RANDOM_ENEMY`, `LOWEST_HP`, `HIGHEST_THREAT`) from the enum. Only the implemented modes remain: `AGGRO_TARGET`, `SELF`, `AOE_SELF`, `AOE_TARGET`.
|
||||
|
||||
---
|
||||
|
||||
### 6. Damage Source Inconsistency
|
||||
**Status:** FIXED
|
||||
**File:** `Ability.java`
|
||||
|
||||
**Solution:** Standardized both `applyAbilityDamage()` and `applyAbilityDamageWithDirection()` to use `NpcDamageSource("mob", npc)` for consistent damage source handling.
|
||||
|
||||
---
|
||||
|
||||
## Incomplete Features
|
||||
|
||||
### 7. AbilityProjectile - No Actual Projectile
|
||||
**Status:** CONFIRMED - INCOMPLETE
|
||||
**File:** `AbilityProjectile.java:88`
|
||||
|
||||
```java
|
||||
// TODO: Use custom EntityAbilityProjectile for actual tracking
|
||||
applyAbilityDamageWithDirection(npc, target, damage, knockback, dx, dz);
|
||||
```
|
||||
|
||||
**Current behavior:**
|
||||
- `speed`, `homing`, `homingStrength` fields exist but are unused
|
||||
- No projectile entity is spawned
|
||||
- Damage is instant, just with particle cosmetics
|
||||
|
||||
---
|
||||
|
||||
### 8. AbilityOrb - No Visual Orb Rendering
|
||||
**Status:** CONFIRMED - INCOMPLETE
|
||||
**File:** `AbilityOrb.java:117-179`
|
||||
|
||||
**Problem:** The orb tracks position (`orbX`, `orbY`, `orbZ`) with runtime getters but nothing renders it client-side. Players only see particle effects on hit, not the flying orb itself.
|
||||
|
||||
---
|
||||
|
||||
### 9. AbilityBeam - No Visual Beam Rendering
|
||||
**Status:** CONFIRMED - INCOMPLETE
|
||||
**File:** `AbilityBeam.java:213-220`
|
||||
|
||||
**Problem:** Only spawns flame particles along the path. No actual beam entity or proper beam visual. The `lockOnTarget` telegraph during active phase isn't visually communicated.
|
||||
|
||||
---
|
||||
|
||||
## Medium Priority Issues
|
||||
|
||||
### 10. Hazard/Trap Telegraph Offset Mismatch
|
||||
**Status:** CONFIRMED
|
||||
**Files:**
|
||||
- `AbilityHazard.java:119-147, 201-248`
|
||||
- `AbilityTrap.java:109-138, 166-205`
|
||||
|
||||
**Problem:** Telegraph is created at target position, but actual effect applies random offset:
|
||||
|
||||
```java
|
||||
// createTelegraph() - No offset applied, shows at target position
|
||||
|
||||
// onExecute() - Random offset applied AFTER telegraph locks
|
||||
double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ());
|
||||
zoneX = pos[0]; // Actual effect position differs from telegraph
|
||||
```
|
||||
|
||||
**Impact:** Players see telegraph at one location, damage happens elsewhere. Misleading visual feedback.
|
||||
|
||||
**Fix:** Either apply offset during telegraph creation, or update telegraph position in `onExecute()`.
|
||||
|
||||
---
|
||||
|
||||
### 11. AbilityCharge Telegraph Direction Timing
|
||||
**Status:** CONFIRMED - MINOR
|
||||
**File:** `AbilityCharge.java:91, 254-286`
|
||||
|
||||
**Problem:** Telegraph created at tick 0, direction locked at tick 1:
|
||||
|
||||
```java
|
||||
// Telegraph created during setup (tick 0) with current target direction
|
||||
// Line 91: Direction locked at tick 1
|
||||
if (tick == 1) {
|
||||
lockChargeDirection(npc, target);
|
||||
}
|
||||
```
|
||||
|
||||
**Impact:** If target moves between tick 0-1, telegraph direction won't match actual charge.
|
||||
|
||||
---
|
||||
|
||||
### 12. AbilitySlam - activeTicks May End Before Landing
|
||||
**Status:** CONFIRMED - MINOR
|
||||
**File:** `AbilitySlam.java:52-53, 230-250`
|
||||
|
||||
**Problem:** `activeTicks = 60` is fixed, but if NPC takes longer to land, ability transitions to RECOVERY without `onLanding()` being called.
|
||||
|
||||
**Secondary issue:** If NPC lands early (tick 15), active phase continues until tick 60 doing nothing - wasted ticks.
|
||||
|
||||
---
|
||||
|
||||
### 13. FOLLOW Mode Telegraph Not Updated
|
||||
**Status:** CONFIRMED
|
||||
**Files:** `AbilityHazard.java:139-146`, `AbilityVortex.java`
|
||||
|
||||
**Problem:** For `FOLLOW_TARGET` mode, telegraph locks position on execute, but the actual hazard zone follows the target. Visual mismatch between warning and danger.
|
||||
|
||||
---
|
||||
|
||||
## Redundancies
|
||||
|
||||
### 14. Duplicate findGroundLevel() Methods
|
||||
**Status:** FIXED
|
||||
**Files:**
|
||||
- `Ability.java` - Made `findGroundLevel()` public static
|
||||
- `TelegraphInstance.java` - Now uses `Ability.findGroundLevel()`
|
||||
|
||||
---
|
||||
|
||||
### 15. Duplicate calculateOffsetPosition() Methods
|
||||
**Status:** FIXED
|
||||
**Files:**
|
||||
- `Ability.java` - Added shared static `calculateOffsetPosition()` method
|
||||
- `AbilityTrap.java` - Removed duplicate, uses `Ability.calculateOffsetPosition()`
|
||||
- `AbilityHazard.java` - Removed duplicate, uses `Ability.calculateOffsetPosition()`
|
||||
|
||||
---
|
||||
|
||||
### 16. Unused applyKnockback in AbilityOrb
|
||||
**Status:** FIXED
|
||||
**File:** `AbilityOrb.java`
|
||||
|
||||
Removed the unused `applyKnockback()` method.
|
||||
|
||||
---
|
||||
|
||||
### 17. Duplicate Reset/Interrupt Cleanup
|
||||
**Status:** NOT FIXED (Low Priority)
|
||||
**Multiple ability files**
|
||||
|
||||
Most abilities override both `reset()` and `onInterrupt()` with duplicate cleanup code. Pattern should have `onInterrupt()` call `reset()` or use shared cleanup method.
|
||||
|
||||
---
|
||||
|
||||
## Minor Issues & Inconsistencies
|
||||
|
||||
### 18. Field Naming Confusion
|
||||
- `AbilityHazard`: `damagePerSecond` with `damageInterval` - name suggests per-second but applied every N ticks
|
||||
- `AbilityCharge`: `hitWidth` stored as `hitRadius` in older NBT (backward compat exists)
|
||||
|
||||
### 19. AbilityVortex Entity Lookup Performance
|
||||
**File:** `AbilityVortex.java:201-212`
|
||||
|
||||
Iterates through ALL loaded entities every tick to find pulled entities by UUID. Should cache entity references or use entity IDs.
|
||||
|
||||
### 20. Telegraph Yaw Rotation
|
||||
**File:** `TelegraphRenderer.java:95`
|
||||
|
||||
Uses `GL11.glRotatef(-instance.getYaw(), 0, 1, 0)` - negative yaw. Should verify this matches Minecraft's yaw conventions.
|
||||
|
||||
### 21. Missing Null Checks
|
||||
Various locations where `target` could be null but is used without defensive checks:
|
||||
- `AbilityProjectile.java:73`
|
||||
- `AbilityOrb.java:138-171`
|
||||
|
||||
---
|
||||
|
||||
## Missing Features (Enhancement Requests)
|
||||
|
||||
These are not bugs but notable missing functionality:
|
||||
|
||||
1. **No Ability Chaining/Combo System** - No way to trigger abilities from other abilities
|
||||
2. **No Cooldown Reduction on Hit** - Can't reduce cooldowns based on successful hits
|
||||
3. **No Multi-Target Hit Limit** - AOE abilities hit unlimited targets (unlike Vortex's `maxTargets`)
|
||||
4. **No Damage Type Configuration** - All abilities use mob/melee damage, no magic/fire/etc options
|
||||
5. **No Target Validation During Execution** - If target dies/despawns mid-ability, behavior varies
|
||||
|
||||
---
|
||||
|
||||
## Priority Summary
|
||||
|
||||
| Priority | Issue | Status |
|
||||
|----------|-------|--------|
|
||||
| **CRITICAL** | Cooldown time system mismatch | FIXED |
|
||||
| **CRITICAL** | AbilityHeal HoT broken | FIXED |
|
||||
| **CRITICAL** | AbilityGuard counter not integrated | FIXED |
|
||||
| **HIGH** | knockbackUp ignored | FIXED |
|
||||
| **HIGH** | Targeting modes unimplemented | FIXED (Removed) |
|
||||
| **HIGH** | Damage source inconsistency | FIXED |
|
||||
| **MEDIUM** | Projectile/Orb/Beam no visuals | Not Fixed |
|
||||
| **MEDIUM** | Hazard/Trap telegraph offset | Not Fixed |
|
||||
| **LOW** | Charge telegraph timing | Not Fixed |
|
||||
| **LOW** | Slam activeTicks timing | Not Fixed |
|
||||
| **LOW** | Code redundancies | FIXED |
|
||||
|
||||
---
|
||||
|
||||
## Remaining Issues
|
||||
|
||||
The following issues were not addressed in this fix round:
|
||||
|
||||
1. **Projectile/Orb/Beam Visual Rendering** - These abilities still only use particles, no actual entity rendering
|
||||
2. **Hazard/Trap Telegraph Offset Mismatch** - Telegraph shows at target, effect spawns with offset
|
||||
3. **Charge Telegraph Direction Timing** - Minor visual mismatch between tick 0-1
|
||||
4. **Slam activeTicks Timing** - Fixed at 60 ticks regardless of landing time
|
||||
|
||||
---
|
||||
|
||||
## Changes Made
|
||||
|
||||
### Ability.java
|
||||
- Removed `cooldownEndTime` transient field
|
||||
- Removed `startCooldown()`, `isOnCooldown()`, `canUse()` methods (cooldown managed by DataAbilities)
|
||||
- Updated `tick()` to not call startCooldown (DataAbilities handles it)
|
||||
- Updated `reset()` to not reset cooldownEndTime
|
||||
- Made `findGroundLevel()` public static for shared use
|
||||
- Added static `calculateOffsetPosition()` utility method
|
||||
- Fixed `applyNpcKnockback()` to accept and use knockbackUp parameter
|
||||
- Standardized damage source to `NpcDamageSource("mob", npc)` in both damage methods
|
||||
- Removed deprecated getters/setters (telegraphColor, castSound, animationId)
|
||||
|
||||
### IAbility.java (API)
|
||||
- Removed `isOnCooldown()` method from interface
|
||||
|
||||
### DataAbilities.java
|
||||
- Added AbilityGuard import
|
||||
- Added guard counter integration in `onDamage()`
|
||||
- Added `getGuardDamageReduction()` helper method
|
||||
- Removed deprecated `isMovementBlocked()` method
|
||||
- Updated knockbackUp javadoc (no longer deprecated)
|
||||
|
||||
### CombatHandler.java
|
||||
- Renamed `isMovementBlocked()` to `isAbilityControllingMovement()`
|
||||
|
||||
### TargetingMode.java
|
||||
- Removed unused modes: `RANDOM_ENEMY`, `LOWEST_HP`, `HIGHEST_THREAT`
|
||||
|
||||
### AbilityHeal.java
|
||||
- Fixed HoT by separating `findAlliesInRadius()` from healing logic
|
||||
- Allies are now found in `onExecute()` regardless of instantHeal flag
|
||||
- Added heal particles during HoT ticks
|
||||
|
||||
### AbilityHazard.java
|
||||
- Removed duplicate `calculateOffsetPosition()`, uses `Ability.calculateOffsetPosition()`
|
||||
- Removed deprecated `getDamagePerTick()`/`setDamagePerTick()` methods
|
||||
|
||||
### AbilityTrap.java
|
||||
- Removed duplicate `calculateOffsetPosition()`, uses `Ability.calculateOffsetPosition()`
|
||||
|
||||
### AbilityOrb.java
|
||||
- Removed unused `applyKnockback()` method
|
||||
|
||||
### TelegraphInstance.java
|
||||
- Removed duplicate `findGroundLevel()`, uses `Ability.findGroundLevel()`
|
||||
@@ -0,0 +1,57 @@
|
||||
# CNPC+ 1.11 Beta - Market Update
|
||||
|
||||
🟡 **In Testing**
|
||||
|
||||
## Features: ⚒️
|
||||
|
||||
- **Built-in Currency System:**
|
||||
> Fully configurable economy currency used across all Market features. Profile persistent across character slots with Vault support for Bukkit-compatible plugins.
|
||||
|
||||
- **Auction House:**
|
||||
> Global player marketplace for listing items, bidding, and instant buyouts. Auctions last 24 hours by default.
|
||||
|
||||
- **Auction Bidding & Snipe Protection:**
|
||||
> Competitive bidding with a 5% minimum bid increment. Snipe Protection extends auctions on last-second bids. Outbid players are automatically refunded.
|
||||
|
||||
- **Buyout System:**
|
||||
> Instant purchase at the seller's Buyout Price. Highest bidder only pays the difference between their bid and the buyout.
|
||||
|
||||
- **My Trades:**
|
||||
> Single screen for managing Active Listings, Active Bids, and Pending Claims. Players can cancel listings with a 10% penalty if bids exist.
|
||||
|
||||
- **Auction Notifications:**
|
||||
> Real-time notifications for sales, outbids, and wins. Offline players receive a claim summary on login.
|
||||
|
||||
- **Trade Slots:**
|
||||
> Limited slots (default 8, max 45) shared across listings, bids, and claims. Configurable via permissions.
|
||||
|
||||
- **Claim Expiration:**
|
||||
> Uncollected claims expire after 20 days.
|
||||
|
||||
- **Item Blacklist:**
|
||||
> Block items by registry name, wildcard pattern, mod, or NBT tag. Soulbound and Profile-Slotbound items are always blocked automatically.
|
||||
|
||||
- **Listing Fee & Sales Tax:**
|
||||
> Configurable listing fee charged on creation and sales tax taken from successful sales.
|
||||
|
||||
- **Search & Sorting:**
|
||||
> Search by item name or seller. Sort by newest, ending soon, price, or most bids.
|
||||
|
||||
- **Trader Stock System:**
|
||||
> Limited stock with Global or Per-Player modes. Restock timers for MC Daily/Weekly, RL Daily/Weekly, or Custom intervals.
|
||||
|
||||
- **Trader Currency Trades:**
|
||||
> Trades can require currency on top of item costs.
|
||||
|
||||
- **Auctioneer NPC Role:**
|
||||
> New role that opens the Auction House on interaction. No configuration needed.
|
||||
|
||||
- **Auction Commands:**
|
||||
> `/kam auction open` for players. Admin commands for listing view and player activity inspection.
|
||||
|
||||
**Extras**
|
||||
|
||||
- Auction logging for server admins (tracks creates, bids, buyouts, sales, expirations, cancellations, claims).
|
||||
- Lifetime currency tracking per player (total earned and spent).
|
||||
- Configurable starting balance and max balance cap.
|
||||
- Blacklist bypass permission for admins.
|
||||
+17
-23
@@ -10,9 +10,10 @@ The **Market Update** brings a complete economy system to CNPC+! Create bustling
|
||||
|
||||
CNPC+ now has a **built-in currency** used across all Market features!
|
||||
|
||||
- **Fully Configurable** - Name it Coins, Gold, Credits, whatever fits your world
|
||||
- **Fully Configurable** - Name it Coins, Gold, Credits, whatever fits your world. Set a starting balance and max balance cap to shape your economy
|
||||
- **Profile Persistent** - Currency carries across all your character slots
|
||||
- **Vault Support** - CNPC+ integrates seamlessly with Bukkit-compatible economy plugins if Vault is installed
|
||||
- **Vault Support** - Integrates seamlessly with Bukkit-compatible economy plugins if Vault is installed. Falls back to built-in currency otherwise
|
||||
- **Lifetime Tracking** - Tracks total earned and spent per player
|
||||
|
||||
> Full Scripting API support for currency operations on players.
|
||||
|
||||
@@ -20,17 +21,17 @@ CNPC+ now has a **built-in currency** used across all Market features!
|
||||
|
||||
## Auction House 🏛️
|
||||
|
||||
A **global player marketplace** where players can list items, bid against each other, and purchase instantly!
|
||||
A **global player marketplace** where players can list items, bid against each other, and purchase instantly! Search for items by name or seller, and sort by newest, ending soon, price, or most bids.
|
||||
|
||||
### Selling
|
||||
|
||||
List any item with a **Starting Price** for competitive bidding. Set an optional **Buyout Price** for players who want to skip the wait and buy immediately.
|
||||
List any item with a **Starting Price** for competitive bidding. Set an optional **Buyout Price** for players who want to skip the wait and buy immediately. Auctions last **24 hours** by default.
|
||||
|
||||
> A configurable **Listing Fee** is charged when creating auctions, and a **Sales Tax** is taken from successful sales.
|
||||
|
||||
### Bidding
|
||||
|
||||
Compete for items by placing bids! **Snipe Protection** extends auctions when last-second bids come in, giving everyone a fair shot. If you get outbid, your currency is **automatically refunded**.
|
||||
Compete for items by placing bids! Each bid must be at least **5%** higher than the current bid. **Snipe Protection** extends auctions when last-second bids come in, giving everyone a fair shot. If you get outbid, your currency is **automatically refunded**.
|
||||
|
||||
### Buyout
|
||||
|
||||
@@ -44,7 +45,9 @@ One screen to manage all your auction activity:
|
||||
- **Active Bids** - Auctions you're competing in
|
||||
- **Ready to Claim** - Won items and currency waiting for pickup
|
||||
|
||||
> Players can **cancel** their own listings. Items are returned as a claim.
|
||||
Players can **cancel** their own listings -- items are returned as a claim. If there are active bids on the listing, a **10% cancellation penalty** is charged.
|
||||
|
||||
> Claims expire after **20 days**. Make sure to collect your items and currency before they're gone!
|
||||
|
||||
### Notifications
|
||||
|
||||
@@ -52,7 +55,7 @@ Get notified immediately when your item sells, when you're outbid, or when you w
|
||||
|
||||
### Trade Slots
|
||||
|
||||
Each player has a limited number of **Trade Slots** (default 8). Trade Slots are shared between Active Listings, Active Bids, and Pending Claims.
|
||||
Every player gets a limited number of **Trade Slots** (default 8). Slots are shared across Active Listings, Active Bids, **and** Pending Claims -- so uncollected claims take up space. Pick up your claims to free slots for new listings and bids!
|
||||
|
||||
> Server owners can grant players more slots via permissions up to a maximum of 45.
|
||||
|
||||
@@ -68,14 +71,9 @@ Server owners have full control over what items can be listed on the Auction Hou
|
||||
- **Mod** - Block all items from a specific mod
|
||||
- **NBT Tag** - Block items containing certain NBT data
|
||||
|
||||
> **Soulbound** and **Profile-Slotbound** items are always blocked automatically. Bound items cannot be traded!
|
||||
> **Soulbound** and **Profile-Slotbound** items are always blocked automatically, regardless of blacklist settings. Bound items cannot be traded!
|
||||
|
||||
**Blacklist Commands:**
|
||||
- `/kam auction blacklist add <item|mod|nbt> <value>` - Add entry
|
||||
- `/kam auction blacklist remove <item|mod|nbt> <value>` - Remove entry
|
||||
- `/kam auction blacklist list` - View all blacklisted entries
|
||||
- `/kam auction blacklist check` - Check if your held item is blacklisted
|
||||
- `/kam auction blacklist reload` - Reload from config
|
||||
Manage the blacklist in-game with `/kam auction blacklist` commands, or configure it directly in the config file.
|
||||
|
||||
---
|
||||
|
||||
@@ -94,14 +92,10 @@ Traders now support **limited stock** with automatic restocking!
|
||||
- **RL Daily / Weekly** - Restock based on real-life time
|
||||
- **Custom** - Set your own interval in MC ticks or real-time
|
||||
|
||||
**Visual Feedback:**
|
||||
- Stock count displayed next to each trade
|
||||
- **Out of Stock** overlay when unavailable
|
||||
- Live **countdown timer** until next restock
|
||||
Stock counts, out-of-stock status, and restock timers are all visible directly in the trade interface.
|
||||
|
||||
> Admins can manually reset stock or cooldowns via the Trader GUI.
|
||||
|
||||
|
||||
### Currency Trades
|
||||
|
||||
Trades can now require **currency on top of items**. Combine material costs with coin costs for balanced economy progression!
|
||||
@@ -110,10 +104,9 @@ Trades can now require **currency on top of items**. Combine material costs with
|
||||
|
||||
## Auctioneer Role 🧑💼
|
||||
|
||||
New NPC role for the Auction House! Set the role on any NPC and players can access the full Auction House by interacting with them.
|
||||
> No configuration needed
|
||||
New NPC role for the Auction House! Set the role on any NPC and players can access the full Auction House by interacting with them -- no extra configuration needed. The role respects the global Auction Enabled setting.
|
||||
|
||||
### Auction Command
|
||||
### Auction Commands
|
||||
|
||||
Players can also access the Auction House via command:
|
||||
|
||||
@@ -123,5 +116,6 @@ Players can also access the Auction House via command:
|
||||
- `/kam auction list [page]` - View active auctions in chat
|
||||
- `/kam auction view <player>` - View a player's auction activity
|
||||
|
||||
---
|
||||
> Server owners can enable **auction logging** to track all auction activity (creates, bids, buyouts, sales, expirations, cancellations, and claims).
|
||||
|
||||
---
|
||||
+1
-1
Submodule gradle-plugins updated: 45dbf494ca...f1c82c10c8.
+1
-1
Submodule src/api updated: 29ad8637d6...f383fc3037.
@@ -13,6 +13,7 @@ import net.minecraft.util.MathHelper;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.WorldServer;
|
||||
import noppes.npcs.controllers.ServerCloneController;
|
||||
import noppes.npcs.controllers.data.CloneFolder;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import java.util.List;
|
||||
@@ -35,20 +36,24 @@ public class CloneCommand extends CommandKamkeelBase {
|
||||
|
||||
@SubCommand(
|
||||
desc = "Add NPC(s) to clone storage",
|
||||
usage = "<npc> <tab> [clonedname]",
|
||||
usage = "<npc> <tab|folder> [clonedname]",
|
||||
permission = 4
|
||||
)
|
||||
public void add(ICommandSender sender, String[] args) {
|
||||
int tab = 0;
|
||||
int tab = -1;
|
||||
String folder = null;
|
||||
try {
|
||||
tab = Integer.parseInt(args[1]);
|
||||
if (tab < 0 || tab > 15) {
|
||||
if (tab < 1 || tab > 15) {
|
||||
sendError(sender, "Tab must be within 1-15");
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
sendError(sender, String.format("Tab is not a number: %s", args[1]));
|
||||
return;
|
||||
folder = args[1];
|
||||
if (ServerCloneController.Instance == null || !ServerCloneController.Instance.hasFolder(folder)) {
|
||||
sendError(sender, String.format("Unknown folder: %s", folder));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int x = sender.getPlayerCoordinates().posX;
|
||||
@@ -64,32 +69,47 @@ public class CloneCommand extends CommandKamkeelBase {
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
if (!npc.writeToNBTOptional(compound))
|
||||
return;
|
||||
ServerCloneController.Instance.addClone(compound, name, tab);
|
||||
sendResult(sender, String.format("Added NPC \u00A7e%s\u00A77 to Tab \u00A7b%d\u00A77", name, tab));
|
||||
if (folder != null) {
|
||||
ServerCloneController.Instance.addClone(compound, name, folder);
|
||||
sendResult(sender, String.format("Added NPC \u00A7e%s\u00A77 to Folder \u00A7b%s\u00A77", name, folder));
|
||||
} else {
|
||||
ServerCloneController.Instance.addClone(compound, name, tab);
|
||||
sendResult(sender, String.format("Added NPC \u00A7e%s\u00A77 to Tab \u00A7b%d\u00A77", name, tab));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "List NPC from clone storage",
|
||||
usage = "<tab>",
|
||||
usage = "<tab|folder>",
|
||||
permission = 2
|
||||
)
|
||||
public void list(ICommandSender sender, String[] args) {
|
||||
sendMessage(sender, "--- Stored NPCs --- (server side)");
|
||||
int tab = 0;
|
||||
int tab = -1;
|
||||
String folder = null;
|
||||
try {
|
||||
tab = Integer.parseInt(args[1]);
|
||||
if (tab < 0 || tab > 15) {
|
||||
if (tab < 1 || tab > 15) {
|
||||
sendError(sender, "Tab must be within 1-15");
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
sendError(sender, String.format("Tab is not a number: %s", args[1]));
|
||||
return;
|
||||
folder = args[1];
|
||||
if (ServerCloneController.Instance == null || !ServerCloneController.Instance.hasFolder(folder)) {
|
||||
sendError(sender, String.format("Unknown folder: %s", folder));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (String name : ServerCloneController.Instance.getClones(tab)) {
|
||||
List<String> clones;
|
||||
if (folder != null) {
|
||||
clones = ServerCloneController.Instance.getClones(folder);
|
||||
} else {
|
||||
clones = ServerCloneController.Instance.getClones(tab);
|
||||
}
|
||||
for (String name : clones) {
|
||||
sendMessage(sender, name);
|
||||
}
|
||||
sendMessage(sender, "------------------------------------");
|
||||
@@ -97,59 +117,75 @@ public class CloneCommand extends CommandKamkeelBase {
|
||||
|
||||
@SubCommand(
|
||||
desc = "Remove NPC from clone storage",
|
||||
usage = "<name> <tab>",
|
||||
usage = "<name> <tab|folder>",
|
||||
permission = 4
|
||||
)
|
||||
public void del(ICommandSender sender, String[] args) throws CommandException {
|
||||
String nametodel = args[0];
|
||||
int tab = 0;
|
||||
int tab = -1;
|
||||
String folder = null;
|
||||
try {
|
||||
tab = Integer.parseInt(args[1]);
|
||||
if (tab < 0 || tab > 15) {
|
||||
sendError(sender, String.format("Tab must be within 1-15"));
|
||||
if (tab < 1 || tab > 15) {
|
||||
sendError(sender, "Tab must be within 1-15");
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
sendError(sender, String.format("Tab is not a number: %s", args[1]));
|
||||
return;
|
||||
}
|
||||
|
||||
boolean deleted = false;
|
||||
for (String name : ServerCloneController.Instance.getClones(tab)) {
|
||||
if (nametodel.equalsIgnoreCase(name)) {
|
||||
ServerCloneController.Instance.removeClone(name, tab);
|
||||
deleted = true;
|
||||
break;
|
||||
folder = args[1];
|
||||
if (ServerCloneController.Instance == null || !ServerCloneController.Instance.hasFolder(folder)) {
|
||||
sendError(sender, String.format("Unknown folder: %s", folder));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!ServerCloneController.Instance.removeClone(nametodel, tab)) {
|
||||
|
||||
boolean success;
|
||||
if (folder != null) {
|
||||
success = ServerCloneController.Instance.removeClone(nametodel, folder);
|
||||
} else {
|
||||
success = ServerCloneController.Instance.removeClone(nametodel, tab);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
sendError(sender, String.format("NPC '%s' was not found", nametodel));
|
||||
} else {
|
||||
sendResult(sender, String.format("Removed NPC \u00A7e%s\u00A77 from Tab \u00A7b%d\u00A77", nametodel, tab));
|
||||
if (folder != null) {
|
||||
sendResult(sender, String.format("Removed NPC \u00A7e%s\u00A77 from Folder \u00A7b%s\u00A77", nametodel, folder));
|
||||
} else {
|
||||
sendResult(sender, String.format("Removed NPC \u00A7e%s\u00A77 from Tab \u00A7b%d\u00A77", nametodel, tab));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "Spawn cloned NPC",
|
||||
usage = "<name> <tab> [[world:]x,y,z]] [newname]",
|
||||
usage = "<name> <tab|folder> [[world:]x,y,z]] [newname]",
|
||||
permission = 2
|
||||
)
|
||||
public void spawn(ICommandSender sender, String[] args) throws CommandException {
|
||||
String name = args[0].replaceAll("%", " "); // if name of npc separed by space, user must use % in place of space
|
||||
int tab = 0;
|
||||
int tab = -1;
|
||||
String folder = null;
|
||||
try {
|
||||
tab = Integer.parseInt(args[1]);
|
||||
if (tab < 0 || tab > 15) {
|
||||
sendError(sender, String.format("Tab must be within 1-15"));
|
||||
if (tab < 1 || tab > 15) {
|
||||
sendError(sender, "Tab must be within 1-15");
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
sendError(sender, String.format("Tab is not a number: %s", args[1]));
|
||||
return;
|
||||
folder = args[1];
|
||||
if (ServerCloneController.Instance == null || !ServerCloneController.Instance.hasFolder(folder)) {
|
||||
sendError(sender, String.format("Unknown folder: %s", folder));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String newname = null;
|
||||
NBTTagCompound compound = ServerCloneController.Instance.getCloneData(sender, name, tab);
|
||||
NBTTagCompound compound;
|
||||
if (folder != null) {
|
||||
compound = ServerCloneController.Instance.getCloneData(sender, name, folder);
|
||||
} else {
|
||||
compound = ServerCloneController.Instance.getCloneData(sender, name, tab);
|
||||
}
|
||||
if (compound == null) {
|
||||
sendError(sender, "Unknown npc");
|
||||
return;
|
||||
@@ -211,22 +247,26 @@ public class CloneCommand extends CommandKamkeelBase {
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "Spawn cloned NPC",
|
||||
usage = "<name> <tab> [[world:]x,y,z]] [newname]",
|
||||
desc = "Spawn cloned NPC in a grid",
|
||||
usage = "<name> <tab|folder> <width> <height> [[world:]x,y,z]] [newname]",
|
||||
permission = 2
|
||||
)
|
||||
public void grid(ICommandSender sender, String[] args) throws CommandException {
|
||||
String name = args[0].replaceAll("%", " "); // if name of npc separed by space, user must use % in place of space
|
||||
int tab = 0;
|
||||
int tab = -1;
|
||||
String folder = null;
|
||||
try {
|
||||
tab = Integer.parseInt(args[1]);
|
||||
if (tab < 0 || tab > 15) {
|
||||
sendError(sender, String.format("Tab must be within 1-15"));
|
||||
if (tab < 1 || tab > 15) {
|
||||
sendError(sender, "Tab must be within 1-15");
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
sendError(sender, String.format("Tab is not a number: %s", args[1]));
|
||||
return;
|
||||
folder = args[1];
|
||||
if (ServerCloneController.Instance == null || !ServerCloneController.Instance.hasFolder(folder)) {
|
||||
sendError(sender, String.format("Unknown folder: %s", folder));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int width, height;
|
||||
@@ -238,9 +278,13 @@ public class CloneCommand extends CommandKamkeelBase {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
String newname = null;
|
||||
NBTTagCompound compound = ServerCloneController.Instance.getCloneData(sender, name, tab);
|
||||
NBTTagCompound compound;
|
||||
if (folder != null) {
|
||||
compound = ServerCloneController.Instance.getCloneData(sender, name, folder);
|
||||
} else {
|
||||
compound = ServerCloneController.Instance.getCloneData(sender, name, tab);
|
||||
}
|
||||
if (compound == null) {
|
||||
sendError(sender, "Unknown npc");
|
||||
return;
|
||||
@@ -317,6 +361,163 @@ public class CloneCommand extends CommandKamkeelBase {
|
||||
return;
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "List all custom clone folders",
|
||||
usage = "",
|
||||
permission = 2
|
||||
)
|
||||
public void listfolders(ICommandSender sender, String[] args) {
|
||||
if (ServerCloneController.Instance == null) {
|
||||
sendError(sender, "Folder system not initialized");
|
||||
return;
|
||||
}
|
||||
sendMessage(sender, "--- Clone Folders ---");
|
||||
for (CloneFolder f : ServerCloneController.Instance.getFolderList()) {
|
||||
sendMessage(sender, f.name);
|
||||
}
|
||||
sendMessage(sender, "------------------------------------");
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "Create a custom clone folder",
|
||||
usage = "<name>",
|
||||
permission = 4
|
||||
)
|
||||
public void createfolder(ICommandSender sender, String[] args) {
|
||||
if (ServerCloneController.Instance == null) {
|
||||
sendError(sender, "Folder system not initialized");
|
||||
return;
|
||||
}
|
||||
String name = args[0];
|
||||
if (!CloneFolder.isValidName(name)) {
|
||||
sendError(sender, String.format("Invalid folder name: %s", name));
|
||||
return;
|
||||
}
|
||||
if (ServerCloneController.Instance.hasFolder(name)) {
|
||||
sendError(sender, String.format("Folder '%s' already exists", name));
|
||||
return;
|
||||
}
|
||||
CloneFolder created = ServerCloneController.Instance.createFolder(name);
|
||||
if (created != null) {
|
||||
sendResult(sender, String.format("Created folder \u00A7b%s", name));
|
||||
} else {
|
||||
sendError(sender, String.format("Failed to create folder '%s'", name));
|
||||
}
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "Rename a custom clone folder",
|
||||
usage = "<oldname> <newname>",
|
||||
permission = 4
|
||||
)
|
||||
public void renamefolder(ICommandSender sender, String[] args) {
|
||||
if (ServerCloneController.Instance == null) {
|
||||
sendError(sender, "Folder system not initialized");
|
||||
return;
|
||||
}
|
||||
String oldName = args[0];
|
||||
String newName = args[1];
|
||||
if (!ServerCloneController.Instance.hasFolder(oldName)) {
|
||||
sendError(sender, String.format("Unknown folder: %s", oldName));
|
||||
return;
|
||||
}
|
||||
if (!CloneFolder.isValidName(newName)) {
|
||||
sendError(sender, String.format("Invalid folder name: %s", newName));
|
||||
return;
|
||||
}
|
||||
if (ServerCloneController.Instance.renameFolder(oldName, newName)) {
|
||||
sendResult(sender, String.format("Renamed folder \u00A7b%s\u00A77 to \u00A7b%s", oldName, newName));
|
||||
} else {
|
||||
sendError(sender, String.format("Failed to rename folder '%s'", oldName));
|
||||
}
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "Delete an empty custom clone folder",
|
||||
usage = "<name>",
|
||||
permission = 4
|
||||
)
|
||||
public void deletefolder(ICommandSender sender, String[] args) {
|
||||
if (ServerCloneController.Instance == null) {
|
||||
sendError(sender, "Folder system not initialized");
|
||||
return;
|
||||
}
|
||||
String name = args[0];
|
||||
if (!ServerCloneController.Instance.hasFolder(name)) {
|
||||
sendError(sender, String.format("Unknown folder: %s", name));
|
||||
return;
|
||||
}
|
||||
if (ServerCloneController.Instance.deleteFolder(name)) {
|
||||
sendResult(sender, String.format("Deleted folder \u00A7b%s", name));
|
||||
} else {
|
||||
sendError(sender, String.format("Failed to delete folder '%s' (folder must be empty)", name));
|
||||
}
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "Move a clone between tabs/folders",
|
||||
usage = "<clonename> <from_tab|folder> <to_tab|folder>",
|
||||
permission = 4
|
||||
)
|
||||
public void move(ICommandSender sender, String[] args) {
|
||||
if (ServerCloneController.Instance == null) {
|
||||
sendError(sender, "Folder system not initialized");
|
||||
return;
|
||||
}
|
||||
String cloneName = args[0];
|
||||
|
||||
int fromTab = -1;
|
||||
String fromFolder = null;
|
||||
try {
|
||||
fromTab = Integer.parseInt(args[1]);
|
||||
if (fromTab < 1 || fromTab > 15) {
|
||||
sendError(sender, "Source tab must be within 1-15");
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
fromFolder = args[1];
|
||||
if (!ServerCloneController.Instance.hasFolder(fromFolder)) {
|
||||
sendError(sender, String.format("Unknown source folder: %s", fromFolder));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int toTab = -1;
|
||||
String toFolder = null;
|
||||
try {
|
||||
toTab = Integer.parseInt(args[2]);
|
||||
if (toTab < 1 || toTab > 15) {
|
||||
sendError(sender, "Destination tab must be within 1-15");
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
toFolder = args[2];
|
||||
if (!ServerCloneController.Instance.hasFolder(toFolder)) {
|
||||
sendError(sender, String.format("Unknown destination folder: %s", toFolder));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
boolean success;
|
||||
if (fromFolder != null && toFolder != null) {
|
||||
success = ServerCloneController.Instance.moveClone(cloneName, fromFolder, toFolder);
|
||||
} else if (fromFolder != null) {
|
||||
success = ServerCloneController.Instance.moveClone(cloneName, fromFolder, toTab);
|
||||
} else if (toFolder != null) {
|
||||
success = ServerCloneController.Instance.moveClone(cloneName, fromTab, toFolder);
|
||||
} else {
|
||||
success = ServerCloneController.Instance.moveClone(cloneName, fromTab, toTab);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
String from = fromFolder != null ? fromFolder : String.valueOf(fromTab);
|
||||
String to = toFolder != null ? toFolder : String.valueOf(toTab);
|
||||
sendResult(sender, String.format("Moved \u00A7e%s\u00A77 from \u00A7b%s\u00A77 to \u00A7b%s", cloneName, from, to));
|
||||
} else {
|
||||
sendError(sender, String.format("Failed to move '%s'", cloneName));
|
||||
}
|
||||
}
|
||||
|
||||
public World getWorld(String t) {
|
||||
WorldServer[] ws = MinecraftServer.getServer().worldServers;
|
||||
for (WorldServer w : ws) {
|
||||
|
||||
@@ -67,6 +67,7 @@ public abstract class Ability implements IAbility {
|
||||
|
||||
// Feedback
|
||||
protected LockMovementType lockMovement = LockMovementType.WINDUP;
|
||||
protected LockType lockType = LockType.BOTH;
|
||||
protected int windUpColor = 0x80FF4400; // Telegraph color during wind up
|
||||
protected int activeColor = 0xC0FF0000; // Telegraph warning/active color
|
||||
|
||||
@@ -137,6 +138,16 @@ public abstract class Ability implements IAbility {
|
||||
// OPTIONAL OVERRIDES
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Returns true if this ability type deals damage.
|
||||
* Override in non-damaging subclasses to return false.
|
||||
* Used by external mods to determine whether to show
|
||||
* damage-related configuration (e.g., DBC stats tab).
|
||||
*/
|
||||
public boolean hasDamage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
}
|
||||
|
||||
@@ -246,12 +257,22 @@ public abstract class Ability implements IAbility {
|
||||
|
||||
// Apply damage
|
||||
if (damage > 0) {
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), damage);
|
||||
} else if (caster instanceof EntityPlayer) {
|
||||
hitEntity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) caster), damage);
|
||||
} else {
|
||||
hitEntity.attackEntityFrom(DamageSource.causeMobDamage(caster), damage);
|
||||
// Check for external damage handler first (e.g., DBC Addon)
|
||||
IAbilityDamageHandler handler = AbilityController.Instance.getDamageHandler();
|
||||
boolean handled = false;
|
||||
if (handler != null) {
|
||||
handled = handler.handleDamage(this, caster, hitEntity, damage, knockback, knockbackUp,
|
||||
knockbackDirX, knockbackDirZ);
|
||||
}
|
||||
if (!handled) {
|
||||
// Default damage path
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), damage);
|
||||
} else if (caster instanceof EntityPlayer) {
|
||||
hitEntity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) caster), damage);
|
||||
} else {
|
||||
hitEntity.attackEntityFrom(DamageSource.causeMobDamage(caster), damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,15 +411,27 @@ public abstract class Ability implements IAbility {
|
||||
defs.add(FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks)
|
||||
.tab("General").range(0, 10000));
|
||||
defs.add(FieldDef.section("ability.section.movement").tab("General"));
|
||||
defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMovementType.getDisplayKeys(),
|
||||
() -> this.getLockMovement().getDisplayKey(),
|
||||
v -> {
|
||||
String[] keys = LockMovementType.getDisplayKeys();
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
if (keys[i].equals(v)) { this.setLockMovement(LockMovementType.fromOrdinal(i)); break; }
|
||||
}
|
||||
})
|
||||
.tab("General").hover("ability.hover.lockMove"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.stringEnumField("ability.lockPosition", LockMovementType.getDisplayKeys(),
|
||||
() -> this.getLockMovement().getDisplayKey(),
|
||||
v -> {
|
||||
String[] keys = LockMovementType.getDisplayKeys();
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
if (keys[i].equals(v)) { this.setLockMovement(LockMovementType.fromOrdinal(i)); break; }
|
||||
}
|
||||
})
|
||||
.hover("ability.hover.lockPosition"),
|
||||
FieldDef.stringEnumField("ability.lockType", LockType.getDisplayKeys(),
|
||||
() -> this.getLockType().getDisplayKey(),
|
||||
v -> {
|
||||
String[] keys = LockType.getDisplayKeys();
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
if (keys[i].equals(v)) { this.setLockType(LockType.fromOrdinal(i)); break; }
|
||||
}
|
||||
})
|
||||
.hover("ability.hover.lockType")
|
||||
.visibleWhen(() -> this.lockMovement != LockMovementType.NO)
|
||||
).tab("General"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible)
|
||||
.hover("ability.hover.interruptible"),
|
||||
@@ -470,6 +503,11 @@ public abstract class Ability implements IAbility {
|
||||
}
|
||||
}
|
||||
|
||||
// External field providers (e.g., DBC Addon injecting a "DBC" tab)
|
||||
for (IAbilityFieldProvider provider : AbilityController.Instance.getFieldProviders()) {
|
||||
provider.addFieldDefinitions(this, defs);
|
||||
}
|
||||
|
||||
return defs;
|
||||
}
|
||||
|
||||
@@ -940,6 +978,7 @@ public abstract class Ability implements IAbility {
|
||||
nbt.setInteger("recovery", dazedTicks);
|
||||
nbt.setBoolean("interruptible", interruptible);
|
||||
nbt.setInteger("lockMovement", lockMovement.ordinal());
|
||||
nbt.setInteger("lockType", lockType.ordinal());
|
||||
nbt.setInteger("windUpColor", windUpColor);
|
||||
nbt.setInteger("activeColor", activeColor);
|
||||
nbt.setString("windUpSound", windUpSound);
|
||||
@@ -990,6 +1029,7 @@ public abstract class Ability implements IAbility {
|
||||
dazedTicks = nbt.getInteger("recovery");
|
||||
interruptible = nbt.getBoolean("interruptible");
|
||||
lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement"));
|
||||
lockType = nbt.hasKey("lockType") ? LockType.fromOrdinal(nbt.getInteger("lockType")) : LockType.BOTH;
|
||||
windUpColor = nbt.getInteger("windUpColor");
|
||||
activeColor = nbt.getInteger("activeColor");
|
||||
windUpSound = nbt.getString("windUpSound");
|
||||
@@ -1154,6 +1194,14 @@ public abstract class Ability implements IAbility {
|
||||
this.lockMovement = lockMovement;
|
||||
}
|
||||
|
||||
public LockType getLockType() {
|
||||
return lockType;
|
||||
}
|
||||
|
||||
public void setLockType(LockType lockType) {
|
||||
this.lockType = lockType;
|
||||
}
|
||||
|
||||
/**
|
||||
* API method: Get lock movement type as integer.
|
||||
* @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE
|
||||
@@ -1173,19 +1221,53 @@ public abstract class Ability implements IAbility {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if movement should be locked during WINDUP phase.
|
||||
* API method: Get lock type as integer.
|
||||
* @return 0=MOVEMENT, 1=ROTATION, 2=BOTH
|
||||
*/
|
||||
@Override
|
||||
public boolean isMovementLockedDuringWindup() {
|
||||
return lockMovement.locksWindup();
|
||||
public int getLockTypeOrdinal() {
|
||||
return lockType.ordinal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if movement should be locked during ACTIVE phase.
|
||||
* API method: Set lock type from integer.
|
||||
* @param type 0=MOVEMENT, 1=ROTATION, 2=BOTH
|
||||
*/
|
||||
@Override
|
||||
public void setLockTypeOrdinal(int type) {
|
||||
this.lockType = LockType.fromOrdinal(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if movement (pathfinding/motion) should be locked during WINDUP phase.
|
||||
*/
|
||||
@Override
|
||||
public boolean isMovementLockedDuringWindup() {
|
||||
return lockMovement.locksWindup() && lockType.locksMovement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if movement (pathfinding/motion) should be locked during ACTIVE phase.
|
||||
*/
|
||||
@Override
|
||||
public boolean isMovementLockedDuringActive() {
|
||||
return lockMovement.locksActive();
|
||||
return lockMovement.locksActive() && lockType.locksMovement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rotation (yaw/pitch) should be locked during WINDUP phase.
|
||||
*/
|
||||
@Override
|
||||
public boolean isRotationLockedDuringWindup() {
|
||||
return lockMovement.locksWindup() && lockType.locksRotation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rotation (yaw/pitch) should be locked during ACTIVE phase.
|
||||
*/
|
||||
@Override
|
||||
public boolean isRotationLockedDuringActive() {
|
||||
return lockMovement.locksActive() && lockType.locksRotation();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1194,9 +1276,23 @@ public abstract class Ability implements IAbility {
|
||||
public boolean isMovementLockedForCurrentPhase() {
|
||||
switch (phase) {
|
||||
case WINDUP:
|
||||
return lockMovement.locksWindup();
|
||||
return isMovementLockedDuringWindup();
|
||||
case ACTIVE:
|
||||
return lockMovement.locksActive();
|
||||
return isMovementLockedDuringActive();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rotation should be locked during the current phase.
|
||||
*/
|
||||
public boolean isRotationLockedForCurrentPhase() {
|
||||
switch (phase) {
|
||||
case WINDUP:
|
||||
return isRotationLockedDuringWindup();
|
||||
case ACTIVE:
|
||||
return isRotationLockedDuringActive();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,12 @@ public class AbilityController implements IAbilityHandler {
|
||||
* Used by AbilitySlot for cache invalidation. */
|
||||
private int version = 0;
|
||||
|
||||
/** External damage handler (e.g., DBC Addon). */
|
||||
private IAbilityDamageHandler damageHandler = null;
|
||||
|
||||
/** External field providers for ability GUI (e.g., DBC Addon). Client-side only. */
|
||||
private final List<IAbilityFieldProvider> fieldProviders = new ArrayList<>();
|
||||
|
||||
public AbilityController() {
|
||||
registerBuiltinTypes();
|
||||
}
|
||||
@@ -386,4 +392,39 @@ public class AbilityController implements IAbilityHandler {
|
||||
registerType("cnpc:hazard", AbilityHazard::new);
|
||||
registerType("cnpc:trap", AbilityTrap::new);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// EXTERNAL DAMAGE HANDLER
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Register an external damage handler for abilities.
|
||||
* When set, the handler is called before default damage application.
|
||||
* If the handler returns true, default damage is skipped.
|
||||
*/
|
||||
public void registerDamageHandler(IAbilityDamageHandler handler) {
|
||||
this.damageHandler = handler;
|
||||
}
|
||||
|
||||
/** Get the registered damage handler, or null if none. */
|
||||
public IAbilityDamageHandler getDamageHandler() {
|
||||
return damageHandler;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// EXTERNAL FIELD PROVIDERS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Register an external field provider for ability GUIs.
|
||||
* Providers are called during getAllDefinitions() to inject custom tabs/fields.
|
||||
*/
|
||||
public void registerFieldProvider(IAbilityFieldProvider provider) {
|
||||
fieldProviders.add(provider);
|
||||
}
|
||||
|
||||
/** Get all registered field providers. */
|
||||
public List<IAbilityFieldProvider> getFieldProviders() {
|
||||
return fieldProviders;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
|
||||
/**
|
||||
* External damage handler for abilities.
|
||||
* When registered, called AFTER script events but BEFORE default attackEntityFrom.
|
||||
* If handleDamage returns true, default damage is skipped.
|
||||
*/
|
||||
public interface IAbilityDamageHandler {
|
||||
/**
|
||||
* Handle damage from an ability.
|
||||
*
|
||||
* @param ability The ability dealing damage
|
||||
* @param caster The entity executing the ability
|
||||
* @param target The entity being hit
|
||||
* @param damage The damage amount (after script event modifications)
|
||||
* @param knockback The horizontal knockback
|
||||
* @param knockbackUp The vertical knockback
|
||||
* @param knockbackDirX The X component of knockback direction
|
||||
* @param knockbackDirZ The Z component of knockback direction
|
||||
* @return true if damage was handled (skip default), false to use default damage
|
||||
*/
|
||||
boolean handleDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target,
|
||||
float damage, float knockback, float knockbackUp,
|
||||
double knockbackDirX, double knockbackDirZ);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* External field definition provider for abilities.
|
||||
* Called during getAllDefinitions() to allow mods to inject custom tabs/fields
|
||||
* into the ability configuration GUI.
|
||||
* <p>
|
||||
* Note: Although this interface references FieldDef (client-only), it is safe to
|
||||
* register on both sides because the generic type is erased at bytecode level.
|
||||
* The addFieldDefinitions method is only ever called from client-side GUI code.
|
||||
*/
|
||||
public interface IAbilityFieldProvider {
|
||||
/**
|
||||
* Add field definitions to the ability's GUI.
|
||||
* Fields added here should use .tab() to assign them to a custom tab.
|
||||
*
|
||||
* @param ability The ability being configured
|
||||
* @param defs The mutable list of field definitions to add to
|
||||
*/
|
||||
void addFieldDefinitions(Ability ability, List<FieldDef> defs);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
/**
|
||||
* Defines what an ability locks when Lock Position is enabled.
|
||||
*/
|
||||
public enum LockType {
|
||||
MOVEMENT,
|
||||
ROTATION,
|
||||
BOTH;
|
||||
|
||||
public boolean locksMovement() {
|
||||
return this == MOVEMENT || this == BOTH;
|
||||
}
|
||||
|
||||
public boolean locksRotation() {
|
||||
return this == ROTATION || this == BOTH;
|
||||
}
|
||||
|
||||
public String getDisplayKey() {
|
||||
switch (this) {
|
||||
case MOVEMENT:
|
||||
return "ability.lockType.movement";
|
||||
case ROTATION:
|
||||
return "ability.lockType.rotation";
|
||||
case BOTH:
|
||||
return "ability.lockType.both";
|
||||
default:
|
||||
return "ability.lockType.both";
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] getDisplayKeys() {
|
||||
return new String[]{
|
||||
"ability.lockType.movement",
|
||||
"ability.lockType.rotation",
|
||||
"ability.lockType.both"
|
||||
};
|
||||
}
|
||||
|
||||
public static LockType fromOrdinal(int ordinal) {
|
||||
LockType[] values = values();
|
||||
if (ordinal >= 0 && ordinal < values.length) {
|
||||
return values[ordinal];
|
||||
}
|
||||
return BOTH;
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,7 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
|
||||
beamEntity.setupCharging(anchorData, windUpTicks, offsetDist);
|
||||
|
||||
beamEntity.setEffects(this.effects);
|
||||
beamEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(beamEntity);
|
||||
}
|
||||
}
|
||||
@@ -126,6 +127,11 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Despawn beam entity if still alive
|
||||
|
||||
@@ -137,6 +137,8 @@ public class AbilityBeamDual extends Ability {
|
||||
beamEntity.setEffects(this.effects);
|
||||
if (dualFire) beamEntity2.setEffects(this.effects);
|
||||
|
||||
beamEntity.setSourceAbility(this);
|
||||
beamEntity2.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(beamEntity);
|
||||
world.spawnEntityInWorld(beamEntity2);
|
||||
}
|
||||
@@ -167,6 +169,11 @@ public class AbilityBeamDual extends Ability {
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Despawn beam entity if still alive
|
||||
|
||||
@@ -111,6 +111,11 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
this.activeSound = "mob.endermen.portal";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDamage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
|
||||
@@ -110,6 +110,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
|
||||
discEntity.setupCharging(anchorData, windUpTicks);
|
||||
|
||||
discEntity.setEffects(this.effects);
|
||||
discEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(discEntity);
|
||||
}
|
||||
}
|
||||
@@ -127,6 +128,11 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Despawn disc entity if still alive
|
||||
|
||||
@@ -146,6 +146,8 @@ public class AbilityDiscDual extends Ability {
|
||||
discEntity2.setEffects(this.effects);
|
||||
}
|
||||
|
||||
discEntity.setSourceAbility(this);
|
||||
discEntity2.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(discEntity);
|
||||
world.spawnEntityInWorld(discEntity2);
|
||||
}
|
||||
@@ -176,6 +178,11 @@ public class AbilityDiscDual extends Ability {
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Despawn disc entity if still alive
|
||||
|
||||
@@ -65,6 +65,11 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
|
||||
this.counterAnimationName = "Ability_Guard_Counter";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDamage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public enum CounterType {
|
||||
FLAT,
|
||||
PERCENT;
|
||||
|
||||
@@ -58,6 +58,11 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
|
||||
this.showTelegraph = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDamage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
|
||||
@@ -99,6 +99,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
laserEntity.setupCharging(anchorData, windUpTicks);
|
||||
|
||||
laserEntity.setEffects(this.effects);
|
||||
laserEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(laserEntity);
|
||||
}
|
||||
}
|
||||
@@ -136,6 +137,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -104,6 +104,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
|
||||
orbEntity.setupCharging(anchorData, windUpTicks);
|
||||
|
||||
orbEntity.setEffects(this.effects);
|
||||
orbEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(orbEntity);
|
||||
}
|
||||
}
|
||||
@@ -122,6 +123,11 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
|
||||
// Nothing to clean up - entity manages itself
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Despawn orb entity if still alive
|
||||
|
||||
@@ -129,6 +129,7 @@ public class AbilityOrbBarrage extends Ability {
|
||||
);
|
||||
|
||||
orbEntity.setupCharging(anchorData[i], windUpTicks);
|
||||
orbEntity.setSourceAbility(this);
|
||||
|
||||
chargingEntities[i] = orbEntity;
|
||||
world.spawnEntityInWorld(orbEntity);
|
||||
@@ -179,6 +180,7 @@ public class AbilityOrbBarrage extends Ability {
|
||||
orbSize, colorData, combatData, homingData, lightningData, lifespanData, trajectoryData
|
||||
);
|
||||
|
||||
orb.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(orb);
|
||||
|
||||
orbEntities.add(orb);
|
||||
@@ -211,6 +213,11 @@ public class AbilityOrbBarrage extends Ability {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
currentOrb = 0;
|
||||
|
||||
@@ -133,6 +133,8 @@ public class AbilityOrbDual extends Ability {
|
||||
orbEntity2.setEffects(this.effects);
|
||||
}
|
||||
|
||||
orbEntity.setSourceAbility(this);
|
||||
orbEntity2.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(orbEntity);
|
||||
world.spawnEntityInWorld(orbEntity2);
|
||||
}
|
||||
@@ -161,7 +163,11 @@ public class AbilityOrbDual extends Ability {
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
// Nothing to clean up - entity manages itself
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -217,9 +217,6 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
double dragPowN = Math.pow(drag, flightTicks);
|
||||
double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN);
|
||||
|
||||
// Speed scales horizontal velocity to cover ground faster
|
||||
vHorizontal *= speedFactor;
|
||||
|
||||
// Adjust vertical velocity for height difference between start and target
|
||||
if (dy > 0) {
|
||||
vy += dy / flightTicks * 1.5; // Going up - need more velocity
|
||||
@@ -438,7 +435,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
int flightTicks = (int) Math.max(10, Math.min(horizontalDist * 1.5 / speedFactor, 40));
|
||||
double drag = 0.91;
|
||||
double dragPowN = Math.pow(drag, flightTicks);
|
||||
double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN) * speedFactor;
|
||||
double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN);
|
||||
|
||||
double dirX = dx / horizontalDist;
|
||||
double dirZ = dz / horizontalDist;
|
||||
|
||||
@@ -100,6 +100,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Clean up entity if still alive
|
||||
|
||||
@@ -84,6 +84,11 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
this.activeSound = "mob.endermen.portal";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDamage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
|
||||
@@ -205,10 +205,19 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
startY = posY;
|
||||
startZ = posZ;
|
||||
|
||||
// Calculate velocity toward target
|
||||
// Always derive direction from the owner's current yaw/pitch.
|
||||
// This ensures the laser fires in the direction the NPC is visually facing,
|
||||
// which matches the telegraph line direction regardless of Lock Movement mode.
|
||||
Entity owner = getOwnerEntity();
|
||||
|
||||
if (target != null) {
|
||||
if (owner != null) {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
this.dirX = -Math.sin(yaw) * Math.cos(pitch);
|
||||
this.dirY = -Math.sin(pitch);
|
||||
this.dirZ = Math.cos(yaw) * Math.cos(pitch);
|
||||
} else if (target != null) {
|
||||
// Fallback: aim at target if owner is somehow unavailable
|
||||
double dx = target.posX - startX;
|
||||
double dy = (target.posY + target.getEyeHeight() - 0.4) - startY;
|
||||
double dz = target.posZ - startZ;
|
||||
@@ -218,13 +227,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
this.dirY = dy / len;
|
||||
this.dirZ = dz / len;
|
||||
}
|
||||
} else if (owner != null) {
|
||||
// Fire in NPC's facing direction
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
this.dirX = -Math.sin(yaw) * Math.cos(pitch);
|
||||
this.dirY = -Math.sin(pitch);
|
||||
this.dirZ = Math.cos(yaw) * Math.cos(pitch);;
|
||||
}
|
||||
|
||||
setPosition(endX, endY, endZ);
|
||||
|
||||
@@ -4,8 +4,11 @@ import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.IAbilityDamageHandler;
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
@@ -77,6 +80,9 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
protected boolean previewMode = false; // Client-side preview mode (no damage/effects)
|
||||
protected EntityLivingBase previewOwner = null; // Direct reference for GUI preview (no world lookup)
|
||||
|
||||
/** The ability that spawned this projectile. Transient, not saved to NBT. */
|
||||
protected transient Ability sourceAbility = null;
|
||||
|
||||
// ==================== ROTATION INTERPOLATION ====================
|
||||
public float prevRotationValX, prevRotationValY, prevRotationValZ;
|
||||
public float rotationValX, rotationValY, rotationValZ;
|
||||
@@ -322,14 +328,30 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
if (previewMode) return; // Skip damage in preview mode
|
||||
Entity owner = getOwnerEntity();
|
||||
|
||||
if (owner instanceof EntityNPCInterface) {
|
||||
target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg);
|
||||
} else if (owner instanceof EntityPlayer) {
|
||||
target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg);
|
||||
} else if (owner instanceof EntityLivingBase) {
|
||||
target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg);
|
||||
} else {
|
||||
target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg);
|
||||
// Check for external damage handler (e.g., DBC Addon)
|
||||
boolean handled = false;
|
||||
if (sourceAbility != null && owner instanceof EntityLivingBase) {
|
||||
IAbilityDamageHandler handler = AbilityController.Instance.getDamageHandler();
|
||||
if (handler != null) {
|
||||
double dx = target.posX - posX;
|
||||
double dz = target.posZ - posZ;
|
||||
float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f;
|
||||
handled = handler.handleDamage(sourceAbility, (EntityLivingBase) owner, target,
|
||||
dmg, kb, kbUp, dx, dz);
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
// Default damage path
|
||||
if (owner instanceof EntityNPCInterface) {
|
||||
target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg);
|
||||
} else if (owner instanceof EntityPlayer) {
|
||||
target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg);
|
||||
} else if (owner instanceof EntityLivingBase) {
|
||||
target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg);
|
||||
} else {
|
||||
target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg);
|
||||
}
|
||||
}
|
||||
|
||||
if (kb > 0) {
|
||||
@@ -361,6 +383,21 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
this.effects = effects != null ? effects : new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the source ability that spawned this projectile.
|
||||
* Used by external damage handlers to access ability-specific data.
|
||||
*/
|
||||
public void setSourceAbility(Ability ability) {
|
||||
this.sourceAbility = ability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source ability that spawned this projectile.
|
||||
*/
|
||||
public Ability getSourceAbility() {
|
||||
return sourceAbility;
|
||||
}
|
||||
|
||||
protected void doExplosion() {
|
||||
if (previewMode) return; // Skip explosion in preview mode
|
||||
worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f);
|
||||
|
||||
@@ -108,6 +108,9 @@ import kamkeel.npcs.network.packets.request.bank.BankSavePacket;
|
||||
import kamkeel.npcs.network.packets.request.bank.BanksGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneAllTagsPacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneAllTagsShortPacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneFolderCrudPacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneFolderListPacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneMovePacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneListPacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.ClonePreSavePacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneRemovePacket;
|
||||
@@ -325,6 +328,9 @@ public class PacketHandler {
|
||||
REQUEST_PACKET.registerPacket(new CloneTagListPacket());
|
||||
REQUEST_PACKET.registerPacket(new CloneAllTagsPacket());
|
||||
REQUEST_PACKET.registerPacket(new CloneAllTagsShortPacket());
|
||||
REQUEST_PACKET.registerPacket(new CloneFolderListPacket());
|
||||
REQUEST_PACKET.registerPacket(new CloneFolderCrudPacket());
|
||||
REQUEST_PACKET.registerPacket(new CloneMovePacket());
|
||||
|
||||
|
||||
// Linked Packets
|
||||
|
||||
@@ -163,6 +163,9 @@ public enum EnumRequestPacket {
|
||||
CloneAllTags,
|
||||
CloneAllTagsShort,
|
||||
ClonePreSave,
|
||||
CloneFolderList,
|
||||
CloneFolderCrud,
|
||||
CloneMove,
|
||||
|
||||
EventScript,
|
||||
PlayerScript,
|
||||
|
||||
@@ -16,7 +16,9 @@ import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.client.AuctionClientConfig;
|
||||
import noppes.npcs.client.ClientCacheHandler;
|
||||
import noppes.npcs.config.ConfigMain;
|
||||
import noppes.npcs.client.ScriptClientConfig;
|
||||
import noppes.npcs.controllers.AuctionConfigSync;
|
||||
import noppes.npcs.controllers.ScriptConfigSync;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumMap;
|
||||
@@ -65,6 +67,11 @@ public final class LoginPacket extends AbstractPacket {
|
||||
NBTTagCompound auctionConfig = new NBTTagCompound();
|
||||
AuctionConfigSync.writeToNBT(auctionConfig);
|
||||
ByteBufUtils.writeNBT(out, auctionConfig);
|
||||
|
||||
// Send script config (server-side sync)
|
||||
NBTTagCompound scriptConfig = new NBTTagCompound();
|
||||
ScriptConfigSync.writeToNBT(scriptConfig);
|
||||
ByteBufUtils.writeNBT(out, scriptConfig);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@@ -88,6 +95,10 @@ public final class LoginPacket extends AbstractPacket {
|
||||
NBTTagCompound auctionConfig = ByteBufUtils.readNBT(in);
|
||||
AuctionClientConfig.readFromNBT(auctionConfig);
|
||||
|
||||
// Read script config
|
||||
NBTTagCompound scriptConfig = ByteBufUtils.readNBT(in);
|
||||
ScriptClientConfig.readFromNBT(scriptConfig);
|
||||
|
||||
PacketClient.sendClient(new SyncRevisionInfoPacket(
|
||||
serverKey,
|
||||
ClientCacheHandler.getLastServerKey(),
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package kamkeel.npcs.network.packets.request.clone;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.network.AbstractPacket;
|
||||
import kamkeel.npcs.network.PacketChannel;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.PacketUtil;
|
||||
import kamkeel.npcs.network.enums.EnumItemPacketType;
|
||||
import kamkeel.npcs.network.enums.EnumRequestPacket;
|
||||
import kamkeel.npcs.network.packets.data.large.GuiDataPacket;
|
||||
import kamkeel.npcs.util.ByteBufUtils;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import noppes.npcs.CustomNpcsPermissions;
|
||||
import noppes.npcs.controllers.ServerCloneController;
|
||||
import noppes.npcs.controllers.data.CloneFolder;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class CloneFolderCrudPacket extends AbstractPacket {
|
||||
public static String packetName = "Request|CloneFolderCrud";
|
||||
|
||||
public static final byte ACTION_CREATE = 0;
|
||||
public static final byte ACTION_RENAME = 1;
|
||||
public static final byte ACTION_DELETE = 2;
|
||||
|
||||
private byte action;
|
||||
private String name;
|
||||
private String newName;
|
||||
|
||||
public CloneFolderCrudPacket() {
|
||||
}
|
||||
|
||||
public CloneFolderCrudPacket(byte action, String name) {
|
||||
this.action = action;
|
||||
this.name = name;
|
||||
this.newName = "";
|
||||
}
|
||||
|
||||
public CloneFolderCrudPacket(byte action, String name, String newName) {
|
||||
this.action = action;
|
||||
this.name = name;
|
||||
this.newName = newName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enum getType() {
|
||||
return EnumRequestPacket.CloneFolderCrud;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketChannel getChannel() {
|
||||
return PacketHandler.REQUEST_PACKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomNpcsPermissions.Permission getPermission() {
|
||||
return CustomNpcsPermissions.NPC_CLONE;
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
out.writeByte(action);
|
||||
ByteBufUtils.writeString(out, name);
|
||||
if (action == ACTION_RENAME) {
|
||||
ByteBufUtils.writeString(out, newName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
|
||||
if (!(player instanceof EntityPlayerMP))
|
||||
return;
|
||||
if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.CLONER))
|
||||
return;
|
||||
|
||||
byte action = in.readByte();
|
||||
String name = ByteBufUtils.readString(in);
|
||||
|
||||
boolean success = false;
|
||||
switch (action) {
|
||||
case ACTION_CREATE:
|
||||
if (CloneFolder.isValidName(name))
|
||||
success = ServerCloneController.Instance.createFolder(name) != null;
|
||||
break;
|
||||
case ACTION_RENAME:
|
||||
String newName = ByteBufUtils.readString(in);
|
||||
if (CloneFolder.isValidName(newName))
|
||||
success = ServerCloneController.Instance.renameFolder(name, newName);
|
||||
break;
|
||||
case ACTION_DELETE:
|
||||
success = ServerCloneController.Instance.deleteFolder(name);
|
||||
break;
|
||||
}
|
||||
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
compound.setBoolean("Success", success);
|
||||
NBTTagList folderList = new NBTTagList();
|
||||
for (CloneFolder folder : ServerCloneController.Instance.getFolderList()) {
|
||||
folderList.appendTag(folder.writeNBT(new NBTTagCompound()));
|
||||
}
|
||||
compound.setTag("CloneFolders", folderList);
|
||||
|
||||
GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package kamkeel.npcs.network.packets.request.clone;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.network.AbstractPacket;
|
||||
import kamkeel.npcs.network.PacketChannel;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.PacketUtil;
|
||||
import kamkeel.npcs.network.enums.EnumItemPacketType;
|
||||
import kamkeel.npcs.network.enums.EnumRequestPacket;
|
||||
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 net.minecraft.nbt.NBTTagList;
|
||||
import noppes.npcs.controllers.ServerCloneController;
|
||||
import noppes.npcs.controllers.data.CloneFolder;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class CloneFolderListPacket extends AbstractPacket {
|
||||
public static String packetName = "Request|CloneFolderList";
|
||||
|
||||
public CloneFolderListPacket() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enum getType() {
|
||||
return EnumRequestPacket.CloneFolderList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketChannel getChannel() {
|
||||
return PacketHandler.REQUEST_PACKET;
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
|
||||
if (!(player instanceof EntityPlayerMP))
|
||||
return;
|
||||
if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.CLONER))
|
||||
return;
|
||||
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
NBTTagList folderList = new NBTTagList();
|
||||
for (CloneFolder folder : ServerCloneController.Instance.getFolderList()) {
|
||||
folderList.appendTag(folder.writeNBT(new NBTTagCompound()));
|
||||
}
|
||||
compound.setTag("CloneFolders", folderList);
|
||||
|
||||
GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound);
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,16 @@ public final class CloneListPacket extends AbstractPacket {
|
||||
public static String packetName = "Request|CloneList";
|
||||
|
||||
private int tab;
|
||||
private String folderName;
|
||||
|
||||
public CloneListPacket(int tab) {
|
||||
this.tab = tab;
|
||||
this.folderName = null;
|
||||
}
|
||||
|
||||
public CloneListPacket(String folderName) {
|
||||
this.tab = -1;
|
||||
this.folderName = folderName;
|
||||
}
|
||||
|
||||
public CloneListPacket() {
|
||||
@@ -45,6 +52,9 @@ public final class CloneListPacket extends AbstractPacket {
|
||||
@Override
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
out.writeInt(tab);
|
||||
if (tab == -1) {
|
||||
kamkeel.npcs.util.ByteBufUtils.writeString(out, folderName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,14 +64,26 @@ public final class CloneListPacket extends AbstractPacket {
|
||||
if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.MOUNTER, EnumItemPacketType.CLONER))
|
||||
return;
|
||||
|
||||
NBTTagList list = new NBTTagList();
|
||||
int tab = in.readInt();
|
||||
for (String name : ServerCloneController.Instance.getClones(tab))
|
||||
list.appendTag(new NBTTagString(name));
|
||||
String folder = null;
|
||||
if (tab == -1) {
|
||||
folder = kamkeel.npcs.util.ByteBufUtils.readString(in);
|
||||
}
|
||||
|
||||
NBTTagList list = new NBTTagList();
|
||||
NBTTagList listDate = new NBTTagList();
|
||||
for (String name : ServerCloneController.Instance.getClonesDate(tab))
|
||||
listDate.appendTag(new NBTTagString(name));
|
||||
|
||||
if (folder != null) {
|
||||
for (String name : ServerCloneController.Instance.getClones(folder))
|
||||
list.appendTag(new NBTTagString(name));
|
||||
for (String name : ServerCloneController.Instance.getClonesDate(folder))
|
||||
listDate.appendTag(new NBTTagString(name));
|
||||
} else {
|
||||
for (String name : ServerCloneController.Instance.getClones(tab))
|
||||
list.appendTag(new NBTTagString(name));
|
||||
for (String name : ServerCloneController.Instance.getClonesDate(tab))
|
||||
listDate.appendTag(new NBTTagString(name));
|
||||
}
|
||||
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
compound.setTag("List", list);
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package kamkeel.npcs.network.packets.request.clone;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.network.AbstractPacket;
|
||||
import kamkeel.npcs.network.PacketChannel;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.PacketUtil;
|
||||
import kamkeel.npcs.network.enums.EnumItemPacketType;
|
||||
import kamkeel.npcs.network.enums.EnumRequestPacket;
|
||||
import kamkeel.npcs.network.packets.data.large.GuiDataPacket;
|
||||
import kamkeel.npcs.util.ByteBufUtils;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.CustomNpcsPermissions;
|
||||
import noppes.npcs.controllers.ServerCloneController;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class CloneMovePacket extends AbstractPacket {
|
||||
public static String packetName = "Request|CloneMove";
|
||||
|
||||
private String cloneName;
|
||||
private int fromTab;
|
||||
private String fromFolder;
|
||||
private int toTab;
|
||||
private String toFolder;
|
||||
|
||||
public CloneMovePacket() {
|
||||
}
|
||||
|
||||
public CloneMovePacket(String cloneName, int fromTab, String fromFolder, int toTab, String toFolder) {
|
||||
this.cloneName = cloneName;
|
||||
this.fromTab = fromTab;
|
||||
this.fromFolder = fromFolder;
|
||||
this.toTab = toTab;
|
||||
this.toFolder = toFolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enum getType() {
|
||||
return EnumRequestPacket.CloneMove;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketChannel getChannel() {
|
||||
return PacketHandler.REQUEST_PACKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomNpcsPermissions.Permission getPermission() {
|
||||
return CustomNpcsPermissions.NPC_CLONE;
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
ByteBufUtils.writeString(out, cloneName);
|
||||
out.writeInt(fromTab);
|
||||
ByteBufUtils.writeString(out, fromFolder != null ? fromFolder : "");
|
||||
out.writeInt(toTab);
|
||||
ByteBufUtils.writeString(out, toFolder != null ? toFolder : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
|
||||
if (!(player instanceof EntityPlayerMP))
|
||||
return;
|
||||
if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.CLONER))
|
||||
return;
|
||||
|
||||
String cloneName = ByteBufUtils.readString(in);
|
||||
int fromTab = in.readInt();
|
||||
String fromFolder = ByteBufUtils.readString(in);
|
||||
if (fromFolder.isEmpty()) fromFolder = null;
|
||||
int toTab = in.readInt();
|
||||
String toFolder = ByteBufUtils.readString(in);
|
||||
if (toFolder.isEmpty()) toFolder = null;
|
||||
|
||||
boolean success = false;
|
||||
if (fromFolder != null && toFolder != null) {
|
||||
success = ServerCloneController.Instance.moveClone(cloneName, fromFolder, toFolder);
|
||||
} else if (fromFolder != null) {
|
||||
success = ServerCloneController.Instance.moveClone(cloneName, fromFolder, toTab);
|
||||
} else if (toFolder != null) {
|
||||
success = ServerCloneController.Instance.moveClone(cloneName, fromTab, toFolder);
|
||||
} else {
|
||||
success = ServerCloneController.Instance.moveClone(cloneName, fromTab, toTab);
|
||||
}
|
||||
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
compound.setBoolean("MoveSuccess", success);
|
||||
GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ public final class ClonePreSavePacket extends AbstractPacket {
|
||||
|
||||
private String name;
|
||||
private int tab;
|
||||
private String folderName;
|
||||
|
||||
public ClonePreSavePacket() {
|
||||
}
|
||||
@@ -31,6 +32,13 @@ public final class ClonePreSavePacket extends AbstractPacket {
|
||||
public ClonePreSavePacket(String name, int tab) {
|
||||
this.name = name;
|
||||
this.tab = tab;
|
||||
this.folderName = null;
|
||||
}
|
||||
|
||||
public ClonePreSavePacket(String name, String folderName) {
|
||||
this.name = name;
|
||||
this.tab = -1;
|
||||
this.folderName = folderName;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,6 +61,9 @@ public final class ClonePreSavePacket extends AbstractPacket {
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
ByteBufUtils.writeString(out, this.name);
|
||||
out.writeInt(tab);
|
||||
if (tab == -1) {
|
||||
ByteBufUtils.writeString(out, this.folderName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,7 +73,15 @@ public final class ClonePreSavePacket extends AbstractPacket {
|
||||
if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.CLONER))
|
||||
return;
|
||||
|
||||
boolean bo = ServerCloneController.Instance.getCloneData(null, ByteBufUtils.readString(in), in.readInt()) != null;
|
||||
String name = ByteBufUtils.readString(in);
|
||||
int tab = in.readInt();
|
||||
boolean bo;
|
||||
if (tab == -1) {
|
||||
String folder = ByteBufUtils.readString(in);
|
||||
bo = ServerCloneController.Instance.getCloneData(null, name, folder) != null;
|
||||
} else {
|
||||
bo = ServerCloneController.Instance.getCloneData(null, name, tab) != null;
|
||||
}
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
compound.setBoolean("NameExists", bo);
|
||||
GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound);
|
||||
|
||||
@@ -25,6 +25,7 @@ public final class CloneRemovePacket extends AbstractPacket {
|
||||
public static String packetName = "Request|CloneRemove";
|
||||
|
||||
private int tab;
|
||||
private String folderName;
|
||||
private String name;
|
||||
|
||||
public CloneRemovePacket() {
|
||||
@@ -32,6 +33,13 @@ public final class CloneRemovePacket extends AbstractPacket {
|
||||
|
||||
public CloneRemovePacket(int tab, String name) {
|
||||
this.tab = tab;
|
||||
this.folderName = null;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public CloneRemovePacket(String folderName, String name) {
|
||||
this.tab = -1;
|
||||
this.folderName = folderName;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@@ -54,6 +62,9 @@ public final class CloneRemovePacket extends AbstractPacket {
|
||||
@Override
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
out.writeInt(tab);
|
||||
if (tab == -1) {
|
||||
ByteBufUtils.writeString(out, this.folderName);
|
||||
}
|
||||
ByteBufUtils.writeString(out, this.name);
|
||||
}
|
||||
|
||||
@@ -65,16 +76,28 @@ public final class CloneRemovePacket extends AbstractPacket {
|
||||
return;
|
||||
|
||||
int tab = in.readInt();
|
||||
ServerCloneController.Instance.removeClone(ByteBufUtils.readString(in), tab);
|
||||
String folder = null;
|
||||
if (tab == -1) {
|
||||
folder = ByteBufUtils.readString(in);
|
||||
}
|
||||
String cloneName = ByteBufUtils.readString(in);
|
||||
|
||||
NBTTagList list = new NBTTagList();
|
||||
|
||||
for (String name : ServerCloneController.Instance.getClones(tab))
|
||||
list.appendTag(new NBTTagString(name));
|
||||
|
||||
NBTTagList listDate = new NBTTagList();
|
||||
for (String name : ServerCloneController.Instance.getClonesDate(tab))
|
||||
listDate.appendTag(new NBTTagString(name));
|
||||
|
||||
if (folder != null) {
|
||||
ServerCloneController.Instance.removeClone(cloneName, folder);
|
||||
for (String name : ServerCloneController.Instance.getClones(folder))
|
||||
list.appendTag(new NBTTagString(name));
|
||||
for (String name : ServerCloneController.Instance.getClonesDate(folder))
|
||||
listDate.appendTag(new NBTTagString(name));
|
||||
} else {
|
||||
ServerCloneController.Instance.removeClone(cloneName, tab);
|
||||
for (String name : ServerCloneController.Instance.getClones(tab))
|
||||
list.appendTag(new NBTTagString(name));
|
||||
for (String name : ServerCloneController.Instance.getClonesDate(tab))
|
||||
listDate.appendTag(new NBTTagString(name));
|
||||
}
|
||||
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
compound.setTag("List", list);
|
||||
|
||||
@@ -25,6 +25,7 @@ public final class CloneSavePacket extends AbstractPacket {
|
||||
|
||||
private String name;
|
||||
private int tab;
|
||||
private String folderName;
|
||||
private NBTTagCompound tagExtra;
|
||||
private NBTTagCompound tagCompound;
|
||||
|
||||
@@ -34,6 +35,15 @@ public final class CloneSavePacket extends AbstractPacket {
|
||||
public CloneSavePacket(String name, int tab, NBTTagCompound tagExtra, NBTTagCompound tagCompound) {
|
||||
this.name = name;
|
||||
this.tab = tab;
|
||||
this.folderName = null;
|
||||
this.tagExtra = tagExtra;
|
||||
this.tagCompound = tagCompound;
|
||||
}
|
||||
|
||||
public CloneSavePacket(String name, String folderName, NBTTagCompound tagExtra, NBTTagCompound tagCompound) {
|
||||
this.name = name;
|
||||
this.tab = -1;
|
||||
this.folderName = folderName;
|
||||
this.tagExtra = tagExtra;
|
||||
this.tagCompound = tagCompound;
|
||||
}
|
||||
@@ -58,6 +68,9 @@ public final class CloneSavePacket extends AbstractPacket {
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
ByteBufUtils.writeString(out, this.name);
|
||||
out.writeInt(tab);
|
||||
if (tab == -1) {
|
||||
ByteBufUtils.writeString(out, this.folderName);
|
||||
}
|
||||
ByteBufUtils.writeNBT(out, this.tagExtra);
|
||||
ByteBufUtils.writeNBT(out, this.tagCompound);
|
||||
}
|
||||
@@ -75,11 +88,20 @@ public final class CloneSavePacket extends AbstractPacket {
|
||||
|
||||
String name = ByteBufUtils.readString(in);
|
||||
int tab = in.readInt();
|
||||
String folder = null;
|
||||
if (tab == -1) {
|
||||
folder = ByteBufUtils.readString(in);
|
||||
}
|
||||
NBTTagCompound tagExtra = ByteBufUtils.readNBT(in);
|
||||
NBTTagCompound tagCompound = ByteBufUtils.readNBT(in);
|
||||
|
||||
NBTTagList tagList = tagCompound.getTagList("TagUUIDs", 8);
|
||||
data.cloned.setTag("TagUUIDs", tagList);
|
||||
ServerCloneController.Instance.addClone(data.cloned, name, tab, tagExtra);
|
||||
|
||||
if (folder != null) {
|
||||
ServerCloneController.Instance.addClone(data.cloned, name, folder, tagExtra);
|
||||
} else {
|
||||
ServerCloneController.Instance.addClone(data.cloned, name, tab, tagExtra);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ 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 kamkeel.npcs.util.ByteBufUtils;
|
||||
import noppes.npcs.controllers.ServerTagMapController;
|
||||
import noppes.npcs.controllers.data.TagMap;
|
||||
|
||||
@@ -22,9 +23,16 @@ public final class CloneTagListPacket extends AbstractPacket {
|
||||
public static String packetName = "Request|CloneTagList";
|
||||
|
||||
private int tab;
|
||||
private String folderName;
|
||||
|
||||
public CloneTagListPacket(int tab) {
|
||||
this.tab = tab;
|
||||
this.folderName = null;
|
||||
}
|
||||
|
||||
public CloneTagListPacket(String folderName) {
|
||||
this.tab = -1;
|
||||
this.folderName = folderName;
|
||||
}
|
||||
|
||||
public CloneTagListPacket() {
|
||||
@@ -44,6 +52,9 @@ public final class CloneTagListPacket extends AbstractPacket {
|
||||
@Override
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
out.writeInt(tab);
|
||||
if (tab == -1) {
|
||||
ByteBufUtils.writeString(out, folderName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,7 +65,13 @@ public final class CloneTagListPacket extends AbstractPacket {
|
||||
return;
|
||||
|
||||
int tab = in.readInt();
|
||||
TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab);
|
||||
TagMap tagMap;
|
||||
if (tab == -1) {
|
||||
String folder = ByteBufUtils.readString(in);
|
||||
tagMap = ServerTagMapController.Instance.getTagMap(folder);
|
||||
} else {
|
||||
tagMap = ServerTagMapController.Instance.getTagMap(tab);
|
||||
}
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
compound.setTag("CloneTags", tagMap.writeNBT());
|
||||
GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound);
|
||||
|
||||
@@ -31,6 +31,7 @@ public class MobSpawnerPacket extends AbstractPacket {
|
||||
|
||||
private String selectedName;
|
||||
private int tab;
|
||||
private String folderName;
|
||||
|
||||
private NBTTagCompound compound;
|
||||
|
||||
@@ -44,6 +45,17 @@ public class MobSpawnerPacket extends AbstractPacket {
|
||||
this.posz = posz;
|
||||
this.selectedName = selectedName;
|
||||
this.tab = tab;
|
||||
this.folderName = null;
|
||||
}
|
||||
|
||||
public MobSpawnerPacket(Action type, int posX, int posY, int posz, String selectedName, String folderName) {
|
||||
this.type = type;
|
||||
this.posX = posX;
|
||||
this.posY = posY;
|
||||
this.posz = posz;
|
||||
this.selectedName = selectedName;
|
||||
this.tab = -1;
|
||||
this.folderName = folderName;
|
||||
}
|
||||
|
||||
public MobSpawnerPacket(Action type, int posX, int posY, int posz, NBTTagCompound compound) {
|
||||
@@ -58,6 +70,10 @@ public class MobSpawnerPacket extends AbstractPacket {
|
||||
PacketClient.sendClient(new MobSpawnerPacket(Action.Server, x, y, z, name, tab));
|
||||
}
|
||||
|
||||
public static void ServerFolder(int x, int y, int z, String name, String folderName) {
|
||||
PacketClient.sendClient(new MobSpawnerPacket(Action.Server, x, y, z, name, folderName));
|
||||
}
|
||||
|
||||
public static void Client(int x, int y, int z, NBTTagCompound compound) {
|
||||
PacketClient.sendClient(new MobSpawnerPacket(Action.Client, x, y, z, compound));
|
||||
}
|
||||
@@ -88,6 +104,9 @@ public class MobSpawnerPacket extends AbstractPacket {
|
||||
if (type == Action.Server) {
|
||||
ByteBufUtils.writeString(out, this.selectedName);
|
||||
out.writeInt(this.tab);
|
||||
if (this.tab == -1) {
|
||||
ByteBufUtils.writeString(out, this.folderName);
|
||||
}
|
||||
} else {
|
||||
ByteBufUtils.writeNBT(out, this.compound);
|
||||
}
|
||||
@@ -107,10 +126,18 @@ public class MobSpawnerPacket extends AbstractPacket {
|
||||
int z = in.readInt();
|
||||
NBTTagCompound compound;
|
||||
|
||||
if (requestedAction == Action.Server)
|
||||
compound = ServerCloneController.Instance.getCloneData(player, ByteBufUtils.readString(in), in.readInt());
|
||||
else
|
||||
if (requestedAction == Action.Server) {
|
||||
String name = ByteBufUtils.readString(in);
|
||||
int tab = in.readInt();
|
||||
if (tab == -1) {
|
||||
String folder = ByteBufUtils.readString(in);
|
||||
compound = ServerCloneController.Instance.getCloneData(player, name, folder);
|
||||
} else {
|
||||
compound = ServerCloneController.Instance.getCloneData(player, name, tab);
|
||||
}
|
||||
} else {
|
||||
compound = ByteBufUtils.readNBT(in);
|
||||
}
|
||||
|
||||
if (!ConfigScript.canScript(player, CustomNpcsPermissions.SCRIPT)) {
|
||||
return;
|
||||
|
||||
@@ -37,6 +37,7 @@ public class SpawnMobPacket extends AbstractPacket {
|
||||
|
||||
private String selectedName;
|
||||
private int tab;
|
||||
private String folderName;
|
||||
|
||||
private NBTTagCompound compound;
|
||||
|
||||
@@ -50,6 +51,17 @@ public class SpawnMobPacket extends AbstractPacket {
|
||||
this.posz = posz;
|
||||
this.selectedName = selectedName;
|
||||
this.tab = tab;
|
||||
this.folderName = null;
|
||||
}
|
||||
|
||||
public SpawnMobPacket(Action type, int posX, int posY, int posz, String selectedName, String folderName) {
|
||||
this.type = type;
|
||||
this.posX = posX;
|
||||
this.posY = posY;
|
||||
this.posz = posz;
|
||||
this.selectedName = selectedName;
|
||||
this.tab = -1;
|
||||
this.folderName = folderName;
|
||||
}
|
||||
|
||||
public SpawnMobPacket(Action type, int posX, int posY, int posz, NBTTagCompound compound) {
|
||||
@@ -64,6 +76,10 @@ public class SpawnMobPacket extends AbstractPacket {
|
||||
PacketClient.sendClient(new SpawnMobPacket(Action.Server, x, y, z, name, tab));
|
||||
}
|
||||
|
||||
public static void ServerFolder(int x, int y, int z, String name, String folderName) {
|
||||
PacketClient.sendClient(new SpawnMobPacket(Action.Server, x, y, z, name, folderName));
|
||||
}
|
||||
|
||||
public static void Client(int x, int y, int z, NBTTagCompound compound) {
|
||||
PacketClient.sendClient(new SpawnMobPacket(Action.Client, x, y, z, compound));
|
||||
}
|
||||
@@ -94,6 +110,9 @@ public class SpawnMobPacket extends AbstractPacket {
|
||||
if (type == Action.Server) {
|
||||
ByteBufUtils.writeString(out, this.selectedName);
|
||||
out.writeInt(this.tab);
|
||||
if (this.tab == -1) {
|
||||
ByteBufUtils.writeString(out, this.folderName);
|
||||
}
|
||||
} else {
|
||||
ByteBufUtils.writeNBT(out, this.compound);
|
||||
}
|
||||
@@ -113,10 +132,18 @@ public class SpawnMobPacket extends AbstractPacket {
|
||||
int z = in.readInt();
|
||||
NBTTagCompound compound;
|
||||
|
||||
if (requestedAction == Action.Server)
|
||||
compound = ServerCloneController.Instance.getCloneData(player, ByteBufUtils.readString(in), in.readInt());
|
||||
else
|
||||
if (requestedAction == Action.Server) {
|
||||
String name = ByteBufUtils.readString(in);
|
||||
int tab = in.readInt();
|
||||
if (tab == -1) {
|
||||
String folder = ByteBufUtils.readString(in);
|
||||
compound = ServerCloneController.Instance.getCloneData(player, name, folder);
|
||||
} else {
|
||||
compound = ServerCloneController.Instance.getCloneData(player, name, tab);
|
||||
}
|
||||
} else {
|
||||
compound = ByteBufUtils.readNBT(in);
|
||||
}
|
||||
|
||||
if (compound == null)
|
||||
return;
|
||||
|
||||
@@ -13,6 +13,7 @@ import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.CustomNpcsPermissions;
|
||||
import noppes.npcs.NBTTags;
|
||||
import noppes.npcs.client.ScriptClientConfig;
|
||||
import noppes.npcs.config.ConfigScript;
|
||||
import noppes.npcs.controllers.ScriptController;
|
||||
|
||||
@@ -69,8 +70,8 @@ public final class ScriptFilesPacket extends AbstractPacket {
|
||||
}
|
||||
|
||||
//Set client side configs
|
||||
ConfigScript.ScriptingEnabled = compound.getBoolean("ScriptingEnabled");
|
||||
ConfigScript.RunLoadedScriptsFirst = compound.getBoolean("LoadedFirst");
|
||||
ScriptClientConfig.setScriptingEnabled(compound.getBoolean("ScriptingEnabled"));
|
||||
ScriptClientConfig.setRunLoadedScriptsFirst(compound.getBoolean("LoadedFirst"));
|
||||
ScriptController.Instance.globalRevision = compound.getInteger("GlobalRevision");
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import noppes.npcs.containers.ContainerNpcQuestReward;
|
||||
import noppes.npcs.containers.ContainerNpcQuestTypeItem;
|
||||
import noppes.npcs.controllers.data.AnimationData;
|
||||
import noppes.npcs.controllers.data.PlayerData;
|
||||
import noppes.npcs.config.ConfigScript;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
public class CommonProxy implements IGuiHandler {
|
||||
@@ -220,4 +221,24 @@ public class CommonProxy implements IGuiHandler {
|
||||
|
||||
public void buildPackageIndex() {
|
||||
}
|
||||
|
||||
public boolean isScriptingEnabled() {
|
||||
return ConfigScript.ScriptingEnabled;
|
||||
}
|
||||
|
||||
public boolean isRunLoadedScriptsFirst() {
|
||||
return ConfigScript.RunLoadedScriptsFirst;
|
||||
}
|
||||
|
||||
public boolean isGlobalPlayerScripts() {
|
||||
return ConfigScript.GlobalPlayerScripts;
|
||||
}
|
||||
|
||||
public boolean isGlobalForgeScripts() {
|
||||
return ConfigScript.GlobalForgeScripts;
|
||||
}
|
||||
|
||||
public boolean isGlobalNPCScripts() {
|
||||
return ConfigScript.GlobalNPCScripts;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import noppes.npcs.entity.EntityNPCInterface;
|
||||
import noppes.npcs.scripted.NpcAPI;
|
||||
import noppes.npcs.scripted.event.AbilityEvent;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
@@ -147,10 +148,15 @@ public class DataAbilities {
|
||||
// Remove telegraph - it has served its purpose
|
||||
removeTelegraph(currentAbility);
|
||||
|
||||
// Lock rotation if movement is locked during ACTIVE phase
|
||||
// Capture current rotation values to force them every tick
|
||||
if (currentAbility.isMovementLockedDuringActive()) {
|
||||
captureLockedRotation();
|
||||
// Handle rotation lock transition from WINDUP to ACTIVE
|
||||
if (currentAbility.isRotationLockedDuringActive()) {
|
||||
// ACTIVE or WINDUP_AND_ACTIVE: capture (or keep) rotation lock
|
||||
if (!rotationLocked) {
|
||||
captureLockedRotation();
|
||||
}
|
||||
} else if (rotationLocked) {
|
||||
// WINDUP only: release the lock now that windup is over
|
||||
releaseLockedRotation();
|
||||
}
|
||||
|
||||
// Play active sound and animation
|
||||
@@ -390,6 +396,9 @@ public class DataAbilities {
|
||||
executeImmediate(ability, target);
|
||||
} else {
|
||||
// Normal windup flow
|
||||
if (ability.isRotationLockedDuringWindup()) {
|
||||
captureLockedRotation();
|
||||
}
|
||||
spawnTelegraph(ability, target);
|
||||
playAbilitySound(ability.getWindUpSound());
|
||||
playAbilityAnimation(ability.getWindUpAnimation());
|
||||
@@ -403,8 +412,8 @@ public class DataAbilities {
|
||||
* Called when windUpTicks is 0.
|
||||
*/
|
||||
private void executeImmediate(Ability ability, EntityLivingBase target) {
|
||||
// Lock rotation if movement is locked during ACTIVE phase
|
||||
if (ability.isMovementLockedDuringActive()) {
|
||||
// Lock rotation if rotation is locked during ACTIVE phase
|
||||
if (ability.isRotationLockedDuringActive()) {
|
||||
captureLockedRotation();
|
||||
}
|
||||
|
||||
@@ -934,9 +943,12 @@ public class DataAbilities {
|
||||
// ROTATION LOCKING
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Bit flag for rotation lock in the existing data watcher slot 15 bitfield */
|
||||
private static final int ROTATION_LOCKED_FLAG = 16;
|
||||
|
||||
/**
|
||||
* Capture current rotation values to lock NPC's look direction.
|
||||
* Called when entering ACTIVE phase with movement locked during active.
|
||||
* Called when entering a locked phase (WINDUP or ACTIVE depending on LockMovementType).
|
||||
*/
|
||||
private void captureLockedRotation() {
|
||||
lockedYaw = npc.rotationYaw;
|
||||
@@ -944,22 +956,53 @@ public class DataAbilities {
|
||||
lockedRenderYawOffset = npc.renderYawOffset;
|
||||
lockedPitch = npc.rotationPitch;
|
||||
rotationLocked = true;
|
||||
npc.setBoolFlag(true, ROTATION_LOCKED_FLAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the rotation lock.
|
||||
* Called when leaving ACTIVE phase or ability completes.
|
||||
* Called when leaving the locked phase or ability completes.
|
||||
*/
|
||||
private void releaseLockedRotation() {
|
||||
rotationLocked = false;
|
||||
npc.setBoolFlag(false, ROTATION_LOCKED_FLAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply locked rotation values to the NPC.
|
||||
* Called AFTER super.onLivingUpdate() in EntityNPCInterface to override
|
||||
* any rotation changes made by the look helper or AI tasks.
|
||||
* any rotation changes made by the look helper, AI tasks, or body smoothing.
|
||||
* Runs on both client and server.
|
||||
*/
|
||||
public void applyLockedRotation() {
|
||||
if (npc.worldObj.isRemote) {
|
||||
// Client-side: check bit 16 in the existing flags data watcher (slot 15)
|
||||
boolean flagActive = npc.getBoolFlag(ROTATION_LOCKED_FLAG);
|
||||
if (!flagActive) {
|
||||
rotationLocked = false;
|
||||
return;
|
||||
}
|
||||
// First tick the flag is active: capture current rotation values
|
||||
if (!rotationLocked) {
|
||||
lockedYaw = npc.rotationYaw;
|
||||
lockedYawHead = npc.rotationYawHead;
|
||||
lockedRenderYawOffset = npc.renderYawOffset;
|
||||
lockedPitch = npc.rotationPitch;
|
||||
rotationLocked = true;
|
||||
}
|
||||
// Force all rotation values back to locked state
|
||||
npc.rotationYaw = lockedYaw;
|
||||
npc.rotationYawHead = lockedYawHead;
|
||||
npc.renderYawOffset = lockedRenderYawOffset;
|
||||
npc.rotationPitch = lockedPitch;
|
||||
npc.prevRotationYaw = lockedYaw;
|
||||
npc.prevRotationYawHead = lockedYawHead;
|
||||
npc.prevRenderYawOffset = lockedRenderYawOffset;
|
||||
npc.prevRotationPitch = lockedPitch;
|
||||
return;
|
||||
}
|
||||
|
||||
// Server-side: use stored locked values
|
||||
if (!rotationLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,9 @@ public class EntityAIAmbushTarget extends EntityAIBase {
|
||||
* Updates the task
|
||||
*/
|
||||
public void updateTask() {
|
||||
this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F);
|
||||
if (!this.theEntity.abilities.isRotationLocked()) {
|
||||
this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F);
|
||||
}
|
||||
}
|
||||
|
||||
private Vec3 findHidingSpot() {
|
||||
|
||||
@@ -118,7 +118,9 @@ public class EntityAIAttackTarget extends EntityAIBase {
|
||||
* Updates the task
|
||||
*/
|
||||
public void updateTask() {
|
||||
this.npc.getLookHelper().setLookPositionWithEntity(this.entityTarget, 30.0F, 30.0F);
|
||||
if (!this.npc.abilities.isRotationLocked()) {
|
||||
this.npc.getLookHelper().setLookPositionWithEntity(this.entityTarget, 30.0F, 30.0F);
|
||||
}
|
||||
|
||||
// Skip pathfinding updates if ability is controlling movement
|
||||
if (!navOverride && !this.npc.abilities.isAbilityControllingMovement() && --this.field_75445_i <= 0) {
|
||||
|
||||
@@ -63,7 +63,7 @@ public class EntityAIDodgeShoot extends EntityAIBase {
|
||||
* Updates the task
|
||||
*/
|
||||
public void updateTask() {
|
||||
if (this.entity.getAttackTarget() != null)
|
||||
if (this.entity.getAttackTarget() != null && !this.entity.abilities.isRotationLocked())
|
||||
this.entity.getLookHelper().setLookPositionWithEntity(this.entity.getAttackTarget(), 30.0F, 30.0F);
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,9 @@ public class EntityAIFollow extends EntityAIBase {
|
||||
if (updateTick < 10)
|
||||
return;
|
||||
updateTick = 0;
|
||||
this.npc.getLookHelper().setLookPositionWithEntity(owner, 10.0F, (float) this.npc.getVerticalFaceSpeed());
|
||||
if (!this.npc.abilities.isRotationLocked()) {
|
||||
this.npc.getLookHelper().setLookPositionWithEntity(owner, 10.0F, (float) this.npc.getVerticalFaceSpeed());
|
||||
}
|
||||
|
||||
double speed = 1 + distance / 150;
|
||||
if (speed > 3)
|
||||
|
||||
@@ -26,7 +26,7 @@ public class EntityAILook extends EntityAIBase {
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldExecute() {
|
||||
return !npc.isAttacking() && npc.getNavigator().noPath() && !npc.isPlayerSleeping() && npc.isEntityAlive();
|
||||
return !npc.abilities.isRotationLocked() && !npc.isAttacking() && npc.getNavigator().noPath() && !npc.isPlayerSleeping() && npc.isEntityAlive();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -91,7 +91,9 @@ public class EntityAIOrbitTarget extends EntityAIBase {
|
||||
* Updates the task
|
||||
*/
|
||||
public void updateTask() {
|
||||
this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F);
|
||||
if (!this.theEntity.abilities.isRotationLocked()) {
|
||||
this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F);
|
||||
}
|
||||
if (this.theEntity.getNavigator().noPath() && this.tick >= 0) {
|
||||
if (this.theEntity.onGround && !this.theEntity.isInWater()) {
|
||||
double d0 = this.targetDistance * (double) (MathHelper.cos(angle / 180.0F * (float) Math.PI));
|
||||
|
||||
@@ -100,7 +100,9 @@ public class EntityAIRangedAttack extends EntityAIBase {
|
||||
* Updates the task
|
||||
*/
|
||||
public void updateTask() {
|
||||
this.entityHost.getLookHelper().setLookPositionWithEntity(this.attackTarget, 30.0F, 30.0F);
|
||||
if (!this.entityHost.abilities.isRotationLocked()) {
|
||||
this.entityHost.getLookHelper().setLookPositionWithEntity(this.attackTarget, 30.0F, 30.0F);
|
||||
}
|
||||
double var1 = this.entityHost.getDistanceSq(this.attackTarget.posX, this.attackTarget.boundingBox.minY, this.attackTarget.posZ);
|
||||
float field_82642_h = this.entityHost.stats.rangedRange * this.entityHost.stats.rangedRange;
|
||||
|
||||
|
||||
@@ -68,7 +68,9 @@ public class EntityAIStalkTarget extends EntityAIBase {
|
||||
* Updates the task
|
||||
*/
|
||||
public void updateTask() {
|
||||
this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F);
|
||||
if (!this.theEntity.abilities.isRotationLocked()) {
|
||||
this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F);
|
||||
}
|
||||
|
||||
if (this.theEntity.getNavigator().noPath() || this.overRide) {
|
||||
if (this.isLookingAway()) {
|
||||
|
||||
@@ -31,7 +31,7 @@ public class EntityAIWatchClosest extends EntityAIBase {
|
||||
*/
|
||||
@Override
|
||||
public boolean shouldExecute() {
|
||||
if (this.theWatcher.getRNG().nextFloat() >= this.field_75331_e || theWatcher.isInteracting()) {
|
||||
if (this.theWatcher.abilities.isRotationLocked() || this.theWatcher.getRNG().nextFloat() >= this.field_75331_e || theWatcher.isInteracting()) {
|
||||
return false;
|
||||
} else {
|
||||
if (this.theWatcher.getAttackTarget() != null) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import net.minecraft.pathfinding.PathPoint;
|
||||
import net.minecraft.util.MathHelper;
|
||||
import net.minecraft.util.Vec3;
|
||||
import noppes.npcs.constants.AiMutex;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
public class EntityAIZigZagTarget extends EntityAIBase {
|
||||
private EntityCreature theEntity;
|
||||
@@ -82,6 +83,8 @@ public class EntityAIZigZagTarget extends EntityAIBase {
|
||||
* Updates the task
|
||||
*/
|
||||
public void updateTask() {
|
||||
this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F);
|
||||
if (!(this.theEntity instanceof EntityNPCInterface) || !((EntityNPCInterface) this.theEntity).abilities.isRotationLocked()) {
|
||||
this.theEntity.getLookHelper().setLookPositionWithEntity(targetEntity, 30.0F, 30.0F);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import net.minecraftforge.client.event.RenderGameOverlayEvent;
|
||||
import net.minecraftforge.client.event.RenderHandEvent;
|
||||
import net.minecraftforge.client.event.RenderLivingEvent;
|
||||
import net.minecraftforge.client.event.RenderPlayerEvent;
|
||||
import net.minecraftforge.event.entity.player.ItemTooltipEvent;
|
||||
import noppes.npcs.client.gui.player.AuctionTooltipHandler;
|
||||
import noppes.npcs.client.gui.customoverlay.OverlayCustom;
|
||||
import noppes.npcs.client.gui.hud.ClientHudManager;
|
||||
import noppes.npcs.client.gui.hud.CompassHudComponent;
|
||||
@@ -75,6 +77,11 @@ public class ClientEventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onItemTooltip(ItemTooltipEvent event) {
|
||||
AuctionTooltipHandler.onItemTooltip(event);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onMouse(MouseEvent event) {
|
||||
if (Minecraft.getMinecraft().currentScreen != null)
|
||||
|
||||
@@ -76,6 +76,7 @@ import noppes.npcs.client.gui.GuiBorderBlock;
|
||||
import noppes.npcs.client.gui.GuiMerchantAdd;
|
||||
import noppes.npcs.client.gui.GuiNpcDimension;
|
||||
import noppes.npcs.client.gui.GuiNpcMobSpawner;
|
||||
import noppes.npcs.client.gui.GuiNpcMobSpawnerFullscreen;
|
||||
import noppes.npcs.client.gui.GuiNpcMobSpawnerMounter;
|
||||
import noppes.npcs.client.gui.GuiNpcPather;
|
||||
import noppes.npcs.client.gui.GuiNpcRedstoneBlock;
|
||||
@@ -191,6 +192,7 @@ import noppes.npcs.client.renderer.items.ItemShortLampRenderer;
|
||||
import noppes.npcs.client.renderer.items.ItemTallLampRenderer;
|
||||
import noppes.npcs.client.renderer.items.ItemToolRenderer;
|
||||
import noppes.npcs.client.renderer.items.ScriptedBlockItemRenderer;
|
||||
import noppes.npcs.client.ScriptClientConfig;
|
||||
import noppes.npcs.config.ConfigClient;
|
||||
import noppes.npcs.config.ConfigItem;
|
||||
import noppes.npcs.config.ConfigMain;
|
||||
@@ -334,7 +336,6 @@ public class ClientProxy extends CommonProxy {
|
||||
|
||||
ClientCloneController.Instance = new ClientCloneController();
|
||||
ClientTagMapController.Instance = new ClientTagMapController();
|
||||
|
||||
MinecraftForge.EVENT_BUS.register(new ClientEventHandler());
|
||||
|
||||
// Telegraph rendering system
|
||||
@@ -603,7 +604,9 @@ public class ClientProxy extends CommonProxy {
|
||||
return new GuiNpcRedstoneBlock(x, y, z);
|
||||
|
||||
else if (gui == EnumGuiType.Cloner)
|
||||
return new GuiNpcMobSpawner(x, y, z);
|
||||
return GuiNpcMobSpawner.isFullscreen
|
||||
? new GuiNpcMobSpawnerFullscreen(x, y, z)
|
||||
: new GuiNpcMobSpawner(x, y, z);
|
||||
|
||||
else if (gui == EnumGuiType.MobSpawnerMounter)
|
||||
return new GuiNpcMobSpawnerMounter(x, y, z);
|
||||
@@ -822,6 +825,31 @@ public class ClientProxy extends CommonProxy {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScriptingEnabled() {
|
||||
return ConfigClient.AllowClientScripts && ScriptClientConfig.isScriptingEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunLoadedScriptsFirst() {
|
||||
return ScriptClientConfig.isRunLoadedScriptsFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlobalPlayerScripts() {
|
||||
return ConfigClient.AllowClientScripts && ScriptClientConfig.isGlobalPlayerScripts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlobalForgeScripts() {
|
||||
return ConfigClient.AllowClientScripts && ScriptClientConfig.isGlobalForgeScripts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGlobalNPCScripts() {
|
||||
return ConfigClient.AllowClientScripts && ScriptClientConfig.isGlobalNPCScripts();
|
||||
}
|
||||
|
||||
public static class FontContainer {
|
||||
public StringCache textFont = null;
|
||||
public boolean useCustomFont = true;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package noppes.npcs.client;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
/**
|
||||
* Client-side cache for script configuration synced from server.
|
||||
* Prevents client from using local ConfigScript which may differ from server settings.
|
||||
*
|
||||
* IMPORTANT: This class is CLIENT-ONLY. Server-side NBT writing is done in
|
||||
* ScriptConfigSync to avoid @SideOnly crashes on dedicated servers.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public class ScriptClientConfig {
|
||||
// Cached values from server
|
||||
private static boolean scriptingEnabled = true;
|
||||
private static boolean runLoadedScriptsFirst = true;
|
||||
private static boolean globalPlayerScripts = true;
|
||||
private static boolean globalForgeScripts = true;
|
||||
private static boolean globalNPCScripts = false;
|
||||
|
||||
/**
|
||||
* Update cached config from server NBT data.
|
||||
* Called when receiving login packet or script sync packets.
|
||||
*/
|
||||
public static void readFromNBT(NBTTagCompound compound) {
|
||||
if (compound == null) return;
|
||||
|
||||
scriptingEnabled = compound.getBoolean("ScriptingEnabled");
|
||||
runLoadedScriptsFirst = compound.getBoolean("RunLoadedScriptsFirst");
|
||||
globalPlayerScripts = compound.getBoolean("GlobalPlayerScripts");
|
||||
globalForgeScripts = compound.getBoolean("GlobalForgeScripts");
|
||||
globalNPCScripts = compound.getBoolean("GlobalNPCScripts");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to defaults (called on disconnect).
|
||||
*/
|
||||
public static void reset() {
|
||||
scriptingEnabled = true;
|
||||
runLoadedScriptsFirst = true;
|
||||
globalPlayerScripts = true;
|
||||
globalForgeScripts = true;
|
||||
globalNPCScripts = false;
|
||||
}
|
||||
|
||||
// Setters for individual packet updates
|
||||
public static void setScriptingEnabled(boolean value) { scriptingEnabled = value; }
|
||||
public static void setRunLoadedScriptsFirst(boolean value) { runLoadedScriptsFirst = value; }
|
||||
public static void setGlobalPlayerScripts(boolean value) { globalPlayerScripts = value; }
|
||||
public static void setGlobalForgeScripts(boolean value) { globalForgeScripts = value; }
|
||||
public static void setGlobalNPCScripts(boolean value) { globalNPCScripts = value; }
|
||||
|
||||
// Getters
|
||||
public static boolean isScriptingEnabled() { return scriptingEnabled; }
|
||||
public static boolean isRunLoadedScriptsFirst() { return runLoadedScriptsFirst; }
|
||||
public static boolean isGlobalPlayerScripts() { return globalPlayerScripts; }
|
||||
public static boolean isGlobalForgeScripts() { return globalForgeScripts; }
|
||||
public static boolean isGlobalNPCScripts() { return globalNPCScripts; }
|
||||
}
|
||||
@@ -41,6 +41,26 @@ public class ClientCloneController extends ServerCloneController {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addToTagMap(NBTTagCompound nbttagcompound, String name, String folderName) {
|
||||
HashSet<UUID> tagUUIDs = new HashSet<>();
|
||||
if (nbttagcompound.hasKey("TagUUIDs")) {
|
||||
NBTTagList nbtTagList = nbttagcompound.getTagList("TagUUIDs", 8);
|
||||
for (int i = 0; i < nbtTagList.tagCount(); i++) {
|
||||
tagUUIDs.add(UUID.fromString(nbtTagList.getStringTagAt(i)));
|
||||
}
|
||||
}
|
||||
|
||||
TagMap tagMap = ClientTagMapController.Instance.getTagMap(folderName);
|
||||
if (!tagUUIDs.isEmpty()) {
|
||||
tagMap.putClone(name, tagUUIDs);
|
||||
} else {
|
||||
tagMap.removeClone(name);
|
||||
}
|
||||
ClientTagMapController.Instance.saveTagMap(tagMap);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeFromTagMap(String name, int tab) {
|
||||
TagMap tagMap = ClientTagMapController.Instance.getTagMap(tab);
|
||||
@@ -51,4 +71,43 @@ public class ClientCloneController extends ServerCloneController {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeFromTagMap(String name, String folderName) {
|
||||
TagMap tagMap = ClientTagMapController.Instance.getTagMap(folderName);
|
||||
if (tagMap.removeClone(name)) {
|
||||
ClientTagMapController.Instance.saveTagMap(tagMap);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HashSet<UUID> getTagUUIDsFromTab(String cloneName, int tab) {
|
||||
TagMap tagMap = ClientTagMapController.Instance.getTagMap(tab);
|
||||
HashSet<UUID> uuids = tagMap.getUUIDs(cloneName);
|
||||
return uuids != null ? new HashSet<>(uuids) : new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HashSet<UUID> getTagUUIDsFromFolder(String cloneName, String folderName) {
|
||||
TagMap tagMap = ClientTagMapController.Instance.getTagMap(folderName);
|
||||
HashSet<UUID> uuids = tagMap.getUUIDs(cloneName);
|
||||
return uuids != null ? new HashSet<>(uuids) : new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setTagUUIDsForTab(String cloneName, int tab, HashSet<UUID> tagUUIDs) {
|
||||
if (tagUUIDs == null || tagUUIDs.isEmpty()) return;
|
||||
TagMap tagMap = ClientTagMapController.Instance.getTagMap(tab);
|
||||
tagMap.putClone(cloneName, tagUUIDs);
|
||||
ClientTagMapController.Instance.saveTagMap(tagMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setTagUUIDsForFolder(String cloneName, String folderName, HashSet<UUID> tagUUIDs) {
|
||||
if (tagUUIDs == null || tagUUIDs.isEmpty()) return;
|
||||
TagMap tagMap = ClientTagMapController.Instance.getTagMap(folderName);
|
||||
tagMap.putClone(cloneName, tagUUIDs);
|
||||
ClientTagMapController.Instance.saveTagMap(tagMap);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package noppes.npcs.client.gui;
|
||||
|
||||
import kamkeel.npcs.network.PacketClient;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneAllTagsPacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneFolderListPacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneListPacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneRemovePacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneTagListPacket;
|
||||
@@ -26,6 +27,8 @@ import noppes.npcs.client.gui.util.GuiNPCInterface;
|
||||
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.IClonerGui;
|
||||
import noppes.npcs.client.gui.util.ICustomScrollListener;
|
||||
import noppes.npcs.client.gui.util.IGuiData;
|
||||
import noppes.npcs.controllers.data.Tag;
|
||||
import noppes.npcs.controllers.data.TagMap;
|
||||
@@ -38,52 +41,84 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData, ICustomScrollListener, IClonerGui {
|
||||
|
||||
// ==================== STATIC STATE (persists across GUI opens, shared with fullscreen) ====================
|
||||
public static boolean isFullscreen = false;
|
||||
public static int activeTab = 1;
|
||||
public static String activeFolder = null;
|
||||
public static int showingClones = 0; // 0=Clones, 1=Entities, 2=Server, 3=Filters
|
||||
public static String search = "";
|
||||
public static String tagSearch = "";
|
||||
|
||||
public static HashMap<UUID, Tag> tags = new HashMap<>();
|
||||
public static HashMap<String, UUID> tagNames = new HashMap<>();
|
||||
public static HashSet<String> tagFilters = new HashSet<>();
|
||||
public static byte displayTags = 0;
|
||||
|
||||
// 0 - Any, 1 - All, 2 - Not Any, 3 - Not All
|
||||
public static byte filterCondition = 0;
|
||||
|
||||
public static byte ascending = 0;
|
||||
|
||||
// 0 - By Name, 1 - By Date
|
||||
public static byte sortType = 0;
|
||||
|
||||
public TagMap tagMap;
|
||||
// Scroll position preservation (shared with fullscreen)
|
||||
public static int savedContentScrollY = 0;
|
||||
public static int savedNavScrollY = 0;
|
||||
|
||||
// ==================== INSTANCE STATE ====================
|
||||
public TagMap tagMap;
|
||||
private final GuiCustomScrollCloner scroll = new GuiCustomScrollCloner(this, 0);
|
||||
private final GuiCustomScroll filterScroll = new GuiCustomScroll(this, 1);
|
||||
|
||||
private int posX, posY, posZ;
|
||||
|
||||
public int posX, posY, posZ;
|
||||
private List<String> list;
|
||||
private List<String> rawList;
|
||||
private List<String> tagList;
|
||||
|
||||
private static int showingClones = 0;
|
||||
|
||||
private static String search = "";
|
||||
private static String tagSearch = "";
|
||||
|
||||
private int activeTab = 1;
|
||||
|
||||
// ==================== CONSTRUCTOR ====================
|
||||
public GuiNpcMobSpawner(int i, int j, int k) {
|
||||
super();
|
||||
xSize = 354;
|
||||
posX = i;
|
||||
posY = j;
|
||||
posZ = k;
|
||||
|
||||
this.closeOnEsc = true;
|
||||
|
||||
closeOnEsc = true;
|
||||
xSize = 354;
|
||||
setBackground("menubg.png");
|
||||
PacketClient.sendClient(new CloneAllTagsPacket());
|
||||
if (showingClones == 2) {
|
||||
PacketClient.sendClient(new CloneFolderListPacket());
|
||||
}
|
||||
|
||||
// Minimized view doesn't support folders — ensure we're on a tab
|
||||
if (activeFolder != null) {
|
||||
if (activeTab < 1 || activeTab > 15) {
|
||||
activeTab = 1;
|
||||
}
|
||||
activeFolder = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== IClonerGui ====================
|
||||
@Override
|
||||
public int getShowingClones() {
|
||||
return showingClones;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<UUID, Tag> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TagMap getTagMap() {
|
||||
return tagMap;
|
||||
}
|
||||
|
||||
// ==================== INIT GUI ====================
|
||||
public void initGui() {
|
||||
// Save scroll position before clear
|
||||
savedContentScrollY = scroll.scrollY;
|
||||
|
||||
super.initGui();
|
||||
guiTop += 10;
|
||||
guiLeft += 30;
|
||||
@@ -92,6 +127,7 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
scroll.setSize(293, 188);
|
||||
scroll.guiLeft = guiLeft + 4;
|
||||
scroll.guiTop = guiTop + 26;
|
||||
|
||||
filterScroll.clear();
|
||||
filterScroll.setSize(140, 166);
|
||||
filterScroll.guiLeft = guiLeft + 4;
|
||||
@@ -99,112 +135,133 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
filterScroll.multipleSelection = true;
|
||||
filterScroll.setSelectedList(tagFilters);
|
||||
|
||||
GuiMenuTopButton button;
|
||||
addTopButton(button = new GuiMenuTopButton(3, guiLeft + 4, guiTop - 17, "spawner.clones"));
|
||||
button.active = showingClones == 0;
|
||||
addTopButton(button = new GuiMenuTopButton(5, button, "gui.server"));
|
||||
button.active = showingClones == 2;
|
||||
addTopButton(button = new GuiMenuTopButton(4, button, "spawner.entities"));
|
||||
button.active = showingClones == 1;
|
||||
addTopButton(button = new GuiMenuTopButton(16, guiLeft + (xSize - 67), guiTop - 17, "gui.filters"));
|
||||
button.active = showingClones == 3;
|
||||
addTopButton(button = new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X"));
|
||||
// Top buttons — build right-to-left to avoid overlap
|
||||
GuiMenuTopButton closeBtn = new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X");
|
||||
addTopButton(closeBtn);
|
||||
|
||||
// Position "+" dynamically to the left of "X"
|
||||
int plusX = closeBtn.xPosition - (fontRendererObj.getStringWidth("+") + 12) - 2;
|
||||
GuiMenuTopButton fullscreenBtn = new GuiMenuTopButton(61, plusX, guiTop - 17, "+");
|
||||
addTopButton(fullscreenBtn);
|
||||
|
||||
// Position "Filters" dynamically to the left of "+"
|
||||
int filterBtnW = fontRendererObj.getStringWidth(StatCollector.translateToLocal("gui.filters")) + 12;
|
||||
int filterX = fullscreenBtn.xPosition - filterBtnW - 2;
|
||||
GuiMenuTopButton filterBtn = new GuiMenuTopButton(16, filterX, guiTop - 17, "gui.filters");
|
||||
filterBtn.active = showingClones == 3;
|
||||
addTopButton(filterBtn);
|
||||
|
||||
// Mode buttons from left
|
||||
GuiMenuTopButton btn;
|
||||
addTopButton(btn = new GuiMenuTopButton(3, guiLeft + 4, guiTop - 17, "spawner.clones"));
|
||||
btn.active = showingClones == 0;
|
||||
addTopButton(btn = new GuiMenuTopButton(5, btn, "gui.server"));
|
||||
btn.active = showingClones == 2;
|
||||
addTopButton(btn = new GuiMenuTopButton(4, btn, "spawner.entities"));
|
||||
btn.active = showingClones == 1;
|
||||
|
||||
if (showingClones < 3) {
|
||||
addScroll(scroll);
|
||||
addTextField(new GuiNpcTextField(1, this, fontRendererObj, guiLeft + 4, guiTop + 4, 293, 20, search));
|
||||
|
||||
addButton(new GuiNpcButton(1, guiLeft + 298, guiTop + 6, 52, 20, "item.monsterPlacer.name"));
|
||||
addButton(new GuiNpcButton(2, guiLeft + 298, guiTop + 140, 52, 20, "spawner.mobspawner"));
|
||||
|
||||
if (showingClones == 0 || showingClones == 2) {
|
||||
|
||||
addSideButton(new GuiMenuSideButton(21, guiLeft - 70, this.guiTop + 2, 70, 22, "1"));
|
||||
addSideButton(new GuiMenuSideButton(22, guiLeft - 70, this.guiTop + 23, 70, 22, "2"));
|
||||
addSideButton(new GuiMenuSideButton(23, guiLeft - 70, this.guiTop + 44, 70, 22, "3"));
|
||||
addSideButton(new GuiMenuSideButton(24, guiLeft - 70, this.guiTop + 65, 70, 22, "4"));
|
||||
addSideButton(new GuiMenuSideButton(25, guiLeft - 70, this.guiTop + 86, 70, 22, "5"));
|
||||
addSideButton(new GuiMenuSideButton(26, guiLeft - 70, this.guiTop + 107, 35, 22, "6"));
|
||||
addSideButton(new GuiMenuSideButton(27, guiLeft - 35, this.guiTop + 107, 35, 22, "7"));
|
||||
addSideButton(new GuiMenuSideButton(28, guiLeft - 70, this.guiTop + 128, 35, 22, "8"));
|
||||
addSideButton(new GuiMenuSideButton(29, guiLeft - 35, this.guiTop + 128, 35, 22, "9"));
|
||||
addSideButton(new GuiMenuSideButton(30, guiLeft - 70, this.guiTop + 149, 35, 22, "10"));
|
||||
addSideButton(new GuiMenuSideButton(31, guiLeft - 35, this.guiTop + 149, 35, 22, "11"));
|
||||
addSideButton(new GuiMenuSideButton(32, guiLeft - 70, this.guiTop + 170, 35, 22, "12"));
|
||||
addSideButton(new GuiMenuSideButton(33, guiLeft - 35, this.guiTop + 170, 35, 22, "13"));
|
||||
addSideButton(new GuiMenuSideButton(34, guiLeft - 70, this.guiTop + 191, 35, 22, "14"));
|
||||
addSideButton(new GuiMenuSideButton(35, guiLeft - 35, this.guiTop + 191, 35, 22, "15"));
|
||||
|
||||
addButton(new GuiNpcButton(6, guiLeft + 298, guiTop + 190, 52, 20, "gui.remove"));
|
||||
|
||||
getSideButton(20 + activeTab).active = true;
|
||||
initTabSideButtons();
|
||||
showClones();
|
||||
|
||||
// Restore scroll position
|
||||
scroll.scrollY = Math.min(savedContentScrollY, Math.max(0, scroll.maxScrollY));
|
||||
} else {
|
||||
showEntities();
|
||||
scroll.scrollY = Math.min(savedContentScrollY, Math.max(0, scroll.maxScrollY));
|
||||
}
|
||||
} else {
|
||||
// Show Filters
|
||||
addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("cloner.tagFilters"), guiLeft + 7, guiTop + 7));
|
||||
addScroll(filterScroll);
|
||||
addTextField(new GuiNpcTextField(2, this, fontRendererObj, guiLeft + 4, guiTop + 190, 140, 20, tagSearch));
|
||||
|
||||
addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("cloner.tagVisibility"), guiLeft + 150, guiTop + 27));
|
||||
addButton(new GuiNpcButton(12, guiLeft + 215, guiTop + 20, new String[]{"display.show", "display.all", "display.hide"}, displayTags));
|
||||
|
||||
addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("filter.contains"), guiLeft + 150, guiTop + 50));
|
||||
addButton(new GuiNpcButton(13, guiLeft + 215, guiTop + 43, new String[]{"filter.any", "filter.all", "filter.notany", "filter.notall"}, filterCondition));
|
||||
|
||||
addButton(new GuiNpcButton(11, guiLeft + 150, guiTop + 66, 130, 20, "gui.deselectAll"));
|
||||
|
||||
|
||||
addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("cloner.order"), guiLeft + 150, guiTop + 112));
|
||||
addButton(new GuiNpcButton(14, guiLeft + 215, guiTop + 105, new String[]{"cloner.ascending", "cloner.descending"}, ascending));
|
||||
|
||||
addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("cloner.type"), guiLeft + 150, guiTop + 135));
|
||||
addButton(new GuiNpcButton(15, guiLeft + 215, guiTop + 128, new String[]{"cloner.name", "cloner.date"}, sortType));
|
||||
|
||||
|
||||
getButton(12).width = 65;
|
||||
getButton(12).height = 20;
|
||||
|
||||
getButton(13).width = 65;
|
||||
getButton(13).height = 20;
|
||||
|
||||
getButton(14).width = 65;
|
||||
getButton(14).height = 20;
|
||||
|
||||
getButton(15).width = 65;
|
||||
getButton(15).height = 20;
|
||||
|
||||
showFilters();
|
||||
showFiltersPage();
|
||||
}
|
||||
}
|
||||
|
||||
public int getShowingClones() {
|
||||
return showingClones;
|
||||
private void initTabSideButtons() {
|
||||
// Tabs 1-5: full width (70px)
|
||||
addSideButton(new GuiMenuSideButton(21, guiLeft - 70, guiTop + 2, 70, 22, "1"));
|
||||
addSideButton(new GuiMenuSideButton(22, guiLeft - 70, guiTop + 23, 70, 22, "2"));
|
||||
addSideButton(new GuiMenuSideButton(23, guiLeft - 70, guiTop + 44, 70, 22, "3"));
|
||||
addSideButton(new GuiMenuSideButton(24, guiLeft - 70, guiTop + 65, 70, 22, "4"));
|
||||
addSideButton(new GuiMenuSideButton(25, guiLeft - 70, guiTop + 86, 70, 22, "5"));
|
||||
// Tabs 6-15: half width (35px), 2-column grid
|
||||
addSideButton(new GuiMenuSideButton(26, guiLeft - 70, guiTop + 107, 35, 22, "6"));
|
||||
addSideButton(new GuiMenuSideButton(27, guiLeft - 35, guiTop + 107, 35, 22, "7"));
|
||||
addSideButton(new GuiMenuSideButton(28, guiLeft - 70, guiTop + 128, 35, 22, "8"));
|
||||
addSideButton(new GuiMenuSideButton(29, guiLeft - 35, guiTop + 128, 35, 22, "9"));
|
||||
addSideButton(new GuiMenuSideButton(30, guiLeft - 70, guiTop + 149, 35, 22, "10"));
|
||||
addSideButton(new GuiMenuSideButton(31, guiLeft - 35, guiTop + 149, 35, 22, "11"));
|
||||
addSideButton(new GuiMenuSideButton(32, guiLeft - 70, guiTop + 170, 35, 22, "12"));
|
||||
addSideButton(new GuiMenuSideButton(33, guiLeft - 35, guiTop + 170, 35, 22, "13"));
|
||||
addSideButton(new GuiMenuSideButton(34, guiLeft - 70, guiTop + 191, 35, 22, "14"));
|
||||
addSideButton(new GuiMenuSideButton(35, guiLeft - 35, guiTop + 191, 35, 22, "15"));
|
||||
|
||||
if (activeTab >= 1 && activeTab <= 15) {
|
||||
GuiMenuSideButton active = getSideButton(20 + activeTab);
|
||||
if (active != null) active.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void showFilters() {
|
||||
// ==================== FILTERS PAGE ====================
|
||||
private void showFiltersPage() {
|
||||
int baseY = guiTop;
|
||||
|
||||
addLabel(new GuiNpcLabel(1, StatCollector.translateToLocal("cloner.tagFilters"),
|
||||
guiLeft + 7, baseY + 7));
|
||||
|
||||
filterScroll.clear();
|
||||
filterScroll.setSize(140, 166);
|
||||
filterScroll.guiLeft = guiLeft + 4;
|
||||
filterScroll.guiTop = baseY + 19;
|
||||
filterScroll.multipleSelection = true;
|
||||
filterScroll.setSelectedList(tagFilters);
|
||||
addScroll(filterScroll);
|
||||
|
||||
addTextField(new GuiNpcTextField(2, this, fontRendererObj,
|
||||
guiLeft + 4, baseY + 190, 140, 20, tagSearch));
|
||||
|
||||
addLabel(new GuiNpcLabel(2, StatCollector.translateToLocal("cloner.tagVisibility"),
|
||||
guiLeft + 150, baseY + 27));
|
||||
addButton(new GuiNpcButton(12, guiLeft + 215, baseY + 20,
|
||||
new String[]{"display.show", "display.all", "display.hide"}, displayTags));
|
||||
|
||||
addLabel(new GuiNpcLabel(3, StatCollector.translateToLocal("filter.contains"),
|
||||
guiLeft + 150, baseY + 50));
|
||||
addButton(new GuiNpcButton(13, guiLeft + 215, baseY + 43,
|
||||
new String[]{"filter.any", "filter.all", "filter.notany", "filter.notall"}, filterCondition));
|
||||
|
||||
addButton(new GuiNpcButton(11, guiLeft + 150, baseY + 66, 130, 20, "gui.deselectAll"));
|
||||
|
||||
addLabel(new GuiNpcLabel(4, StatCollector.translateToLocal("cloner.order"),
|
||||
guiLeft + 150, baseY + 112));
|
||||
addButton(new GuiNpcButton(14, guiLeft + 215, baseY + 105,
|
||||
new String[]{"cloner.ascending", "cloner.descending"}, ascending));
|
||||
|
||||
addLabel(new GuiNpcLabel(5, StatCollector.translateToLocal("cloner.type"),
|
||||
guiLeft + 150, baseY + 135));
|
||||
addButton(new GuiNpcButton(15, guiLeft + 215, baseY + 128,
|
||||
new String[]{"cloner.name", "cloner.date"}, sortType));
|
||||
|
||||
getButton(12).width = 65;
|
||||
getButton(12).height = 20;
|
||||
getButton(13).width = 65;
|
||||
getButton(13).height = 20;
|
||||
getButton(14).width = 65;
|
||||
getButton(14).height = 20;
|
||||
getButton(15).width = 65;
|
||||
getButton(15).height = 20;
|
||||
|
||||
filterScroll.setList(getTagList());
|
||||
}
|
||||
|
||||
private void showEntities() {
|
||||
Map<?, ?> data = EntityList.stringToClassMapping;
|
||||
ArrayList<String> list = new ArrayList<String>();
|
||||
for (Object name : data.keySet()) {
|
||||
Class<?> c = (Class<?>) data.get(name);
|
||||
try {
|
||||
if (EntityLiving.class.isAssignableFrom(c) && c.getConstructor(new Class[]{World.class}) != null && !Modifier.isAbstract(c.getModifiers()))
|
||||
list.add(name.toString());
|
||||
} catch (SecurityException e) {
|
||||
e.printStackTrace();
|
||||
} catch (NoSuchMethodException e) {
|
||||
}
|
||||
}
|
||||
this.list = list;
|
||||
this.rawList = new ArrayList<String>(this.list);
|
||||
scroll.setList(getSearchList(), ascending == 0, sortType == 0);
|
||||
}
|
||||
|
||||
// ==================== CLONE / ENTITY DISPLAY ====================
|
||||
private void showClones() {
|
||||
if (showingClones == 2) {
|
||||
PacketClient.sendClient(new CloneTagListPacket(activeTab));
|
||||
@@ -219,16 +276,41 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
}
|
||||
|
||||
this.tagMap = ClientTagMapController.Instance.getTagMap(activeTab);
|
||||
populateRawListWithTags();
|
||||
scroll.setList(getSearchList(), ascending == 0, sortType == 0);
|
||||
}
|
||||
|
||||
private void showEntities() {
|
||||
Map<?, ?> data = EntityList.stringToClassMapping;
|
||||
ArrayList<String> list = new ArrayList<String>();
|
||||
for (Object name : data.keySet()) {
|
||||
Class<?> c = (Class<?>) data.get(name);
|
||||
try {
|
||||
if (EntityLiving.class.isAssignableFrom(c)
|
||||
&& c.getConstructor(new Class[]{World.class}) != null
|
||||
&& !Modifier.isAbstract(c.getModifiers()))
|
||||
list.add(name.toString());
|
||||
} catch (SecurityException e) {
|
||||
e.printStackTrace();
|
||||
} catch (NoSuchMethodException e) {
|
||||
}
|
||||
}
|
||||
this.list = list;
|
||||
this.rawList = new ArrayList<String>(this.list);
|
||||
scroll.setList(getSearchList(), ascending == 0, sortType == 0);
|
||||
}
|
||||
|
||||
private void populateRawListWithTags() {
|
||||
this.rawList = new ArrayList<>(this.list);
|
||||
if (this.tagMap != null) {
|
||||
if (GuiNpcMobSpawner.displayTags == 0 || GuiNpcMobSpawner.displayTags == 1) {
|
||||
if (displayTags == 0 || displayTags == 1) {
|
||||
for (int i = 0; i < this.list.size(); i++) {
|
||||
StringBuilder npcName = new StringBuilder(list.get(i));
|
||||
if (this.tagMap.hasClone(list.get(i))) {
|
||||
for (UUID tagUUID : this.tagMap.getUUIDsList(list.get(i))) {
|
||||
Tag tag = tags.get(tagUUID);
|
||||
if (tag != null) {
|
||||
if (GuiNpcMobSpawner.displayTags == 1 || !tag.getIsHidden()) {
|
||||
if (displayTags == 1 || !tag.getIsHidden()) {
|
||||
npcName.append(" [" + tag.name + "]");
|
||||
}
|
||||
}
|
||||
@@ -238,27 +320,26 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scroll.setList(getSearchList(), ascending == 0, sortType == 0);
|
||||
}
|
||||
|
||||
public void keyTyped(char c, int i) {
|
||||
super.keyTyped(c, i);
|
||||
if (getTextField(1) != null) {
|
||||
if (search.equals(getTextField(1).getText()))
|
||||
return;
|
||||
search = getTextField(1).getText().toLowerCase();
|
||||
scroll.setList(getSearchList(), ascending == 0, sortType == 0);
|
||||
}
|
||||
if (getTextField(2) != null) {
|
||||
if (tagSearch.equals(getTextField(2).getText()))
|
||||
return;
|
||||
tagSearch = getTextField(2).getText().toLowerCase();
|
||||
filterScroll.setList(getTagList());
|
||||
}
|
||||
private NBTTagCompound getCompound() {
|
||||
String sel = scroll.getSelected();
|
||||
if (sel == null)
|
||||
return null;
|
||||
|
||||
if (showingClones == 0) {
|
||||
return ClientCloneController.Instance.getCloneData(player, sel, activeTab);
|
||||
} else {
|
||||
Entity entity = EntityList.createEntityByName(sel, Minecraft.getMinecraft().theWorld);
|
||||
if (entity == null)
|
||||
return null;
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
entity.writeToNBTOptional(compound);
|
||||
return compound;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SEARCH / FILTER ====================
|
||||
private List<String> getSearchList() {
|
||||
if (this.list == null) {
|
||||
this.list = new ArrayList<String>();
|
||||
@@ -269,10 +350,8 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
|
||||
// No Filters - or - Entities
|
||||
if (tagFilters.size() == 0 || showingClones == 1) {
|
||||
// No Search
|
||||
if (search.isEmpty())
|
||||
return new ArrayList<String>(this.list);
|
||||
// Yes Search
|
||||
else {
|
||||
List<String> list = new ArrayList<String>();
|
||||
for (int i = 0; i < this.list.size(); i++) {
|
||||
@@ -284,10 +363,7 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
//// WITH FILTERS
|
||||
|
||||
// No Search
|
||||
// With Filters
|
||||
if (search.isEmpty()) {
|
||||
List<String> list = new ArrayList<String>();
|
||||
for (String name : this.list) {
|
||||
@@ -300,7 +376,6 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
return list;
|
||||
}
|
||||
|
||||
// Yes Search
|
||||
List<String> list = new ArrayList<String>();
|
||||
for (int i = 0; i < this.list.size(); i++) {
|
||||
if (this.rawList.get(i).toLowerCase().contains(search)) {
|
||||
@@ -311,7 +386,6 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -346,7 +420,6 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return conditionMet;
|
||||
}
|
||||
|
||||
@@ -354,10 +427,8 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
if (this.tagList == null) {
|
||||
this.tagList = new ArrayList<String>();
|
||||
}
|
||||
// No Search // No Filters - or - Entities
|
||||
if (tagSearch.isEmpty())
|
||||
return new ArrayList<String>(tagNames.keySet());
|
||||
// Yes Search // No Filters - or - Entities
|
||||
|
||||
List<String> list = new ArrayList<String>();
|
||||
for (String name : this.tagList) {
|
||||
@@ -367,125 +438,162 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
return list;
|
||||
}
|
||||
|
||||
private NBTTagCompound getCompound() {
|
||||
String sel = scroll.getSelected();
|
||||
if (sel == null)
|
||||
return null;
|
||||
|
||||
if (showingClones == 0) {
|
||||
return ClientCloneController.Instance.getCloneData(player, sel, activeTab);
|
||||
} else {
|
||||
Entity entity = EntityList.createEntityByName(sel, Minecraft.getMinecraft().theWorld);
|
||||
if (entity == null)
|
||||
return null;
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
entity.writeToNBTOptional(compound);
|
||||
return compound;
|
||||
}
|
||||
// ==================== MOUSE / KEYBOARD EVENTS ====================
|
||||
@Override
|
||||
public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customScrollDoubleClicked(String selection, GuiCustomScroll guiCustomScroll) {
|
||||
}
|
||||
|
||||
public void keyTyped(char c, int i) {
|
||||
super.keyTyped(c, i);
|
||||
|
||||
GuiNpcTextField searchField = getTextField(1);
|
||||
if (searchField != null) {
|
||||
String newText = searchField.getText().toLowerCase();
|
||||
if (!search.equals(newText)) {
|
||||
search = newText;
|
||||
scroll.setList(getSearchList(), ascending == 0, sortType == 0);
|
||||
}
|
||||
}
|
||||
|
||||
GuiNpcTextField tagSearchField = getTextField(2);
|
||||
if (tagSearchField != null) {
|
||||
String newText = tagSearchField.getText().toLowerCase();
|
||||
if (!tagSearch.equals(newText)) {
|
||||
tagSearch = newText;
|
||||
filterScroll.setList(getTagList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== BUTTON ACTIONS ====================
|
||||
@Override
|
||||
protected void actionPerformed(GuiButton guibutton) {
|
||||
int id = guibutton.id;
|
||||
if (id == 0) {
|
||||
close();
|
||||
}
|
||||
if (id == 1) {
|
||||
if (showingClones == 2) {
|
||||
String sel = scroll.getSelected();
|
||||
if (sel == null)
|
||||
return;
|
||||
SpawnMobPacket.Server(posX, posY, posZ, sel, activeTab);
|
||||
close();
|
||||
} else {
|
||||
NBTTagCompound compound = getCompound();
|
||||
if (compound == null)
|
||||
return;
|
||||
SpawnMobPacket.Client(posX, posY, posZ, compound);
|
||||
close();
|
||||
}
|
||||
}
|
||||
if (id == 2) {
|
||||
if (showingClones == 2) {
|
||||
String sel = scroll.getSelected();
|
||||
if (sel == null)
|
||||
return;
|
||||
MobSpawnerPacket.Server(posX, posY, posZ, sel, activeTab);
|
||||
close();
|
||||
} else {
|
||||
NBTTagCompound compound = getCompound();
|
||||
if (compound == null)
|
||||
return;
|
||||
MobSpawnerPacket.Client(posX, posY, posZ, compound);
|
||||
close();
|
||||
|
||||
}
|
||||
// Tab side buttons (IDs 21-35)
|
||||
if (id >= 21 && id <= 35) {
|
||||
activeTab = id - 20;
|
||||
activeFolder = null;
|
||||
initGui();
|
||||
return;
|
||||
}
|
||||
|
||||
// Mode buttons
|
||||
if (id == 3) {
|
||||
savedContentScrollY = 0;
|
||||
showingClones = 0;
|
||||
initGui();
|
||||
}
|
||||
if (id == 4) {
|
||||
savedContentScrollY = 0;
|
||||
showingClones = 1;
|
||||
initGui();
|
||||
}
|
||||
if (id == 5) {
|
||||
savedContentScrollY = 0;
|
||||
showingClones = 2;
|
||||
PacketClient.sendClient(new CloneFolderListPacket());
|
||||
initGui();
|
||||
}
|
||||
if (id == 16) {
|
||||
savedContentScrollY = 0;
|
||||
showingClones = 3;
|
||||
initGui();
|
||||
}
|
||||
if (id == 17) {
|
||||
close();
|
||||
}
|
||||
|
||||
// Open fullscreen GUI
|
||||
if (id == 61) {
|
||||
isFullscreen = true;
|
||||
Minecraft.getMinecraft().displayGuiScreen(
|
||||
new GuiNpcMobSpawnerFullscreen(posX, posY, posZ));
|
||||
}
|
||||
|
||||
// Spawn
|
||||
if (id == 1) {
|
||||
if (showingClones == 2) {
|
||||
String sel = scroll.getSelected();
|
||||
if (sel == null) return;
|
||||
SpawnMobPacket.Server(posX, posY, posZ, sel, activeTab);
|
||||
close();
|
||||
} else {
|
||||
NBTTagCompound compound = getCompound();
|
||||
if (compound == null) return;
|
||||
SpawnMobPacket.Client(posX, posY, posZ, compound);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
// Mob Spawner
|
||||
if (id == 2) {
|
||||
if (showingClones == 2) {
|
||||
String sel = scroll.getSelected();
|
||||
if (sel == null) return;
|
||||
MobSpawnerPacket.Server(posX, posY, posZ, sel, activeTab);
|
||||
close();
|
||||
} else {
|
||||
NBTTagCompound compound = getCompound();
|
||||
if (compound == null) return;
|
||||
MobSpawnerPacket.Client(posX, posY, posZ, compound);
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove
|
||||
if (id == 6) {
|
||||
if (scroll.getSelected() != null) {
|
||||
int prevSelected = scroll.selected;
|
||||
if (showingClones == 2) {
|
||||
PacketClient.sendClient(new CloneRemovePacket(activeTab, scroll.getSelected()));
|
||||
return;
|
||||
}
|
||||
ClientCloneController.Instance.removeClone(scroll.getSelected(), activeTab);
|
||||
scroll.selected = -1;
|
||||
initGui();
|
||||
if (scroll.list != null && !scroll.list.isEmpty()) {
|
||||
scroll.selected = Math.min(prevSelected, scroll.list.size() - 1);
|
||||
} else {
|
||||
scroll.selected = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter controls
|
||||
if (id == 11) {
|
||||
tagFilters = new HashSet<>();
|
||||
filterScroll.setSelectedList(tagFilters);
|
||||
initGui();
|
||||
}
|
||||
if (id == 12) {
|
||||
GuiNpcButton button = (GuiNpcButton) guibutton;
|
||||
displayTags = (byte) button.getValue();
|
||||
displayTags = (byte) ((GuiNpcButton) guibutton).getValue();
|
||||
}
|
||||
if (id == 13) {
|
||||
GuiNpcButton button = (GuiNpcButton) guibutton;
|
||||
filterCondition = (byte) button.getValue();
|
||||
filterCondition = (byte) ((GuiNpcButton) guibutton).getValue();
|
||||
}
|
||||
if (id == 14) {
|
||||
GuiNpcButton button = (GuiNpcButton) guibutton;
|
||||
ascending = (byte) button.getValue();
|
||||
ascending = (byte) ((GuiNpcButton) guibutton).getValue();
|
||||
}
|
||||
if (id == 15) {
|
||||
GuiNpcButton button = (GuiNpcButton) guibutton;
|
||||
sortType = (byte) button.getValue();
|
||||
}
|
||||
if (id > 20) {
|
||||
activeTab = id - 20;
|
||||
initGui();
|
||||
sortType = (byte) ((GuiNpcButton) guibutton).getValue();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DATA FROM SERVER ====================
|
||||
@Override
|
||||
public void save() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGuiData(NBTTagCompound compound) {
|
||||
if (compound.hasKey("CloneTags")) {
|
||||
if (compound.hasKey("CloneFolders")) {
|
||||
// Minimized doesn't use folders, but still handle the packet
|
||||
initGui();
|
||||
} else if (compound.hasKey("CloneTags")) {
|
||||
this.tagMap = new TagMap(activeTab);
|
||||
NBTTagCompound cloneTags = compound.getCompoundTag("CloneTags");
|
||||
this.tagMap.readNBT(cloneTags);
|
||||
@@ -521,29 +629,8 @@ public class GuiNpcMobSpawner extends GuiNPCInterface implements IGuiData {
|
||||
}
|
||||
|
||||
this.list = list;
|
||||
|
||||
this.rawList = new ArrayList<String>(this.list);
|
||||
if (this.tagMap != null) {
|
||||
if (GuiNpcMobSpawner.displayTags == 0 || GuiNpcMobSpawner.displayTags == 1) {
|
||||
for (int i = 0; i < this.list.size(); i++) {
|
||||
StringBuilder npcName = new StringBuilder(list.get(i));
|
||||
if (this.tagMap.hasClone(list.get(i))) {
|
||||
for (UUID tagUUID : this.tagMap.getUUIDsList(list.get(i))) {
|
||||
Tag tag = tags.get(tagUUID);
|
||||
if (tag != null) {
|
||||
if (GuiNpcMobSpawner.displayTags == 1 || !tag.getIsHidden()) {
|
||||
npcName.append(" [" + tag.name + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.rawList.set(i, npcName.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
populateRawListWithTags();
|
||||
scroll.setList(getSearchList(), ascending == 0, sortType == 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package noppes.npcs.client.gui;
|
||||
|
||||
import kamkeel.npcs.network.PacketClient;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneAllTagsShortPacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneFolderListPacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.ClonePreSavePacket;
|
||||
import kamkeel.npcs.network.packets.request.clone.CloneSavePacket;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -22,12 +23,14 @@ import noppes.npcs.client.gui.util.GuiNpcTextField;
|
||||
import noppes.npcs.client.gui.util.IGuiData;
|
||||
import noppes.npcs.client.gui.util.ISubGuiListener;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
import noppes.npcs.controllers.data.CloneFolder;
|
||||
import noppes.npcs.controllers.data.Tag;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ISubGuiListener {
|
||||
@@ -35,9 +38,16 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall
|
||||
private Entity toClone;
|
||||
private NBTTagCompound compound;
|
||||
private static boolean serverSide = false;
|
||||
private static int saveMode = 0; // 0=Tab, 1=Folder
|
||||
private static int tab = 1;
|
||||
private static String folder = null;
|
||||
public boolean isNPC = false;
|
||||
|
||||
// Client-side folder names
|
||||
private ArrayList<String> clientFolderNames = new ArrayList<>();
|
||||
// Server-side folder names (populated via CloneFolderListPacket)
|
||||
private static ArrayList<String> serverFolderNames = new ArrayList<>();
|
||||
|
||||
// Selected Tags to Add
|
||||
public static NBTTagList tagsCompound;
|
||||
public static HashSet<String> addTags;
|
||||
@@ -68,6 +78,37 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall
|
||||
if (isNPC) {
|
||||
PacketClient.sendClient(new CloneAllTagsShortPacket());
|
||||
}
|
||||
|
||||
buildClientFolderNames();
|
||||
|
||||
// Request server folders if in server mode
|
||||
if (serverSide) {
|
||||
PacketClient.sendClient(new CloneFolderListPacket());
|
||||
}
|
||||
|
||||
// Validate saved folder selection
|
||||
validateFolder();
|
||||
}
|
||||
|
||||
private void buildClientFolderNames() {
|
||||
clientFolderNames.clear();
|
||||
if (ClientCloneController.Instance != null) {
|
||||
for (CloneFolder f : ClientCloneController.Instance.getFolderList()) {
|
||||
clientFolderNames.add(f.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getActiveFolderList() {
|
||||
return serverSide ? serverFolderNames : clientFolderNames;
|
||||
}
|
||||
|
||||
private void validateFolder() {
|
||||
if (saveMode == 1 && folder != null) {
|
||||
if (!getActiveFolderList().contains(folder)) {
|
||||
folder = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,61 +118,138 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall
|
||||
addLabel(new GuiNpcLabel(0, "Save as", guiLeft + 4, guiTop + 6));
|
||||
addTextField(new GuiNpcTextField(0, this, fontRendererObj, guiLeft + 4, guiTop + 18, 200, 20, name));
|
||||
|
||||
// Tab/Folder toggle
|
||||
addButton(new GuiNpcButton(6, guiLeft + 4, guiTop + 45, 50, 20,
|
||||
new String[]{"Tab", "Folder"}, saveMode));
|
||||
|
||||
addLabel(new GuiNpcLabel(1, "Tab", guiLeft + 10, guiTop + 50));
|
||||
addButton(new GuiButtonBiDirectional(2, guiLeft + 40, guiTop + 45, 60, 20, new String[]{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"}, tab - 1));
|
||||
// Destination selector based on saveMode
|
||||
if (saveMode == 0) {
|
||||
// Tab selector: 1-15
|
||||
String[] tabLabels = new String[15];
|
||||
for (int i = 0; i < 15; i++) tabLabels[i] = String.valueOf(i + 1);
|
||||
int selectedTab = (tab >= 1 && tab <= 15) ? tab - 1 : 0;
|
||||
addButton(new GuiButtonBiDirectional(2, guiLeft + 56, guiTop + 45, 90, 20, tabLabels, selectedTab));
|
||||
} else {
|
||||
// Folder selector button — opens SubGuiFolderSelect
|
||||
String btnText = folder != null ? folder : "Select a Folder";
|
||||
addButton(new GuiNpcButton(2, guiLeft + 56, guiTop + 45, 148, 20, btnText));
|
||||
}
|
||||
|
||||
addButton(new GuiNpcButton(3, guiLeft + 4, guiTop + 95, new String[]{"Client side", "Server side"}, serverSide ? 1 : 0));
|
||||
addLabel(new GuiNpcLabel(1, "Tab", guiLeft + 10, guiTop + 50));
|
||||
|
||||
addButton(new GuiNpcButton(0, guiLeft + 4, guiTop + 70, 80, 20, "gui.save"));
|
||||
// Save / Cancel
|
||||
GuiNpcButton saveBtn = new GuiNpcButton(0, guiLeft + 4, guiTop + 70, 80, 20, "gui.save");
|
||||
if (saveMode == 1 && folder == null) {
|
||||
saveBtn.enabled = false;
|
||||
}
|
||||
addButton(saveBtn);
|
||||
addButton(new GuiNpcButton(1, guiLeft + 86, guiTop + 70, 80, 20, "gui.cancel"));
|
||||
|
||||
// Client / Server toggle
|
||||
addButton(new GuiNpcButton(3, guiLeft + 4, guiTop + 95, new String[]{"Client side", "Server side"}, serverSide ? 1 : 0));
|
||||
|
||||
if (isNPC) {
|
||||
addButton(new GuiNpcButton(4, guiLeft + 4, guiTop + 120, 99, 20, "cloner.wandTags"));
|
||||
addButton(new GuiNpcButton(5, guiLeft + 106, guiTop + 120, 99, 20, "cloner.npcTags"));
|
||||
if (addTags.size() > 0) {
|
||||
addLabel(new GuiNpcLabel(6, "cloner.wandtagsapplied", guiLeft + 10, guiTop + 160));
|
||||
addLabel(new GuiNpcLabel(8, "cloner.wandtagsapplied", guiLeft + 10, guiTop + 160));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDestinationFromSelector() {
|
||||
if (saveMode == 0) {
|
||||
GuiNpcButton selector = getButton(2);
|
||||
if (selector == null) return;
|
||||
tab = selector.getValue() + 1;
|
||||
folder = null;
|
||||
}
|
||||
// In folder mode, folder is already set via SubGuiFolderSelect
|
||||
}
|
||||
|
||||
public void buttonEvent(GuiButton guibutton) {
|
||||
int id = guibutton.id;
|
||||
|
||||
// Save
|
||||
if (id == 0) {
|
||||
updateDestinationFromSelector();
|
||||
String name = getTextField(0).getText();
|
||||
if (name.isEmpty())
|
||||
return;
|
||||
int tab = ((GuiNpcButton) guibutton).getValue() + 1;
|
||||
|
||||
if (saveMode == 1 && folder == null)
|
||||
return;
|
||||
|
||||
if (!serverSide) {
|
||||
if (ClientCloneController.Instance.getCloneData(null, name, tab) != null)
|
||||
boolean exists;
|
||||
if (saveMode == 1 && folder != null && ClientCloneController.Instance != null) {
|
||||
exists = ClientCloneController.Instance.getCloneData(null, name, folder) != null;
|
||||
} else {
|
||||
exists = ClientCloneController.Instance.getCloneData(null, name, tab) != null;
|
||||
}
|
||||
if (exists)
|
||||
displayGuiScreen(new GuiYesNo(this, "Warning", "You are about to overwrite a clone", 1));
|
||||
else
|
||||
confirmClicked(true, 0);
|
||||
} else
|
||||
PacketClient.sendClient(new ClonePreSavePacket(name, tab));
|
||||
} else {
|
||||
if (saveMode == 1 && folder != null) {
|
||||
PacketClient.sendClient(new ClonePreSavePacket(name, folder));
|
||||
} else {
|
||||
PacketClient.sendClient(new ClonePreSavePacket(name, tab));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel
|
||||
if (id == 1) {
|
||||
close();
|
||||
}
|
||||
|
||||
// Destination selector
|
||||
if (id == 2) {
|
||||
tab = ((GuiNpcButton) guibutton).getValue() + 1;
|
||||
if (saveMode == 0) {
|
||||
// Tab mode — update from BiDirectional
|
||||
updateDestinationFromSelector();
|
||||
} else {
|
||||
// Folder mode — open folder selector Sub GUI
|
||||
List<String> folders = getActiveFolderList();
|
||||
setSubGui(new SubGuiFolderSelect(folders, folder));
|
||||
}
|
||||
}
|
||||
|
||||
// Client / Server toggle
|
||||
if (id == 3) {
|
||||
boolean wasServerSide = serverSide;
|
||||
serverSide = ((GuiNpcButton) guibutton).getValue() == 1;
|
||||
|
||||
if (serverSide && !wasServerSide) {
|
||||
// Switching to server mode — request fresh folder list
|
||||
PacketClient.sendClient(new CloneFolderListPacket());
|
||||
}
|
||||
|
||||
// Validate folder exists on the new side
|
||||
validateFolder();
|
||||
initGui();
|
||||
}
|
||||
|
||||
// Wand Tags
|
||||
if (id == 4) {
|
||||
if (isNPC) {
|
||||
this.setSubGui(new SubGuiClonerQuickTags(this));
|
||||
}
|
||||
}
|
||||
|
||||
// NPC Tags
|
||||
if (id == 5) {
|
||||
if (isNPC) {
|
||||
this.setSubGui(new SubGuiClonerNPCTags((EntityNPCInterface) toClone, this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab/Folder mode toggle
|
||||
if (id == 6) {
|
||||
saveMode = ((GuiNpcButton) guibutton).getValue();
|
||||
initGui();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void confirmClicked(boolean confirm, int id) {
|
||||
@@ -145,13 +263,21 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall
|
||||
if (isNPC) {
|
||||
compound.setTag("TagUUIDs", tagsCompound);
|
||||
}
|
||||
ClientCloneController.Instance.addClone(compound, name, tab, extraTags);
|
||||
if (saveMode == 1 && folder != null && ClientCloneController.Instance != null) {
|
||||
ClientCloneController.Instance.addClone(compound, name, folder, extraTags);
|
||||
} else {
|
||||
ClientCloneController.Instance.addClone(compound, name, tab, extraTags);
|
||||
}
|
||||
} else {
|
||||
NBTTagCompound compounder = new NBTTagCompound();
|
||||
if (isNPC) {
|
||||
compounder.setTag("TagUUIDs", tagsCompound);
|
||||
}
|
||||
PacketClient.sendClient(new CloneSavePacket(name, tab, extraTags, compounder));
|
||||
if (saveMode == 1 && folder != null) {
|
||||
PacketClient.sendClient(new CloneSavePacket(name, folder, extraTags, compounder));
|
||||
} else {
|
||||
PacketClient.sendClient(new CloneSavePacket(name, tab, extraTags, compounder));
|
||||
}
|
||||
}
|
||||
|
||||
close();
|
||||
@@ -159,7 +285,6 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall
|
||||
displayGuiScreen(this);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
}
|
||||
@@ -171,6 +296,17 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall
|
||||
displayGuiScreen(new GuiYesNo(this, "Warning", "You are about to overwrite a clone", 1));
|
||||
else
|
||||
confirmClicked(true, 0);
|
||||
} else if (compound.hasKey("CloneFolders")) {
|
||||
// Server folder list arrived
|
||||
NBTTagList folderList = compound.getTagList("CloneFolders", 10);
|
||||
serverFolderNames.clear();
|
||||
for (int i = 0; i < folderList.tagCount(); i++) {
|
||||
CloneFolder f = new CloneFolder();
|
||||
f.readNBT(folderList.getCompoundTagAt(i));
|
||||
serverFolderNames.add(f.name);
|
||||
}
|
||||
validateFolder();
|
||||
initGui();
|
||||
} else if (compound.hasKey("ShortTags")) {
|
||||
NBTTagList validTags = compound.getTagList("ShortTags", 10);
|
||||
tagMap.clear();
|
||||
@@ -193,6 +329,12 @@ public class GuiNpcMobSpawnerAdd extends GuiNPCInterface implements GuiYesNoCall
|
||||
|
||||
@Override
|
||||
public void subGuiClosed(SubGuiInterface subgui) {
|
||||
if (subgui instanceof SubGuiFolderSelect) {
|
||||
SubGuiFolderSelect sel = (SubGuiFolderSelect) subgui;
|
||||
if (sel.selectedFolder != null) {
|
||||
folder = sel.selectedFolder;
|
||||
}
|
||||
}
|
||||
initGui();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,67 @@
|
||||
package noppes.npcs.client.gui;
|
||||
|
||||
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.SubGuiInterface;
|
||||
import noppes.npcs.controllers.data.CloneFolder;
|
||||
|
||||
public class SubGuiCloneFolderName extends SubGuiInterface {
|
||||
private String originalName;
|
||||
public boolean cancelled = true;
|
||||
private String folderName = "";
|
||||
|
||||
public SubGuiCloneFolderName(String existingName) {
|
||||
this.originalName = existingName;
|
||||
this.folderName = existingName;
|
||||
xSize = 200;
|
||||
ySize = 80;
|
||||
this.closeOnEsc = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
String title = isRename() ? "Rename Folder" : "New Folder";
|
||||
addLabel(new GuiNpcLabel(0, title, guiLeft + 5, guiTop + 5));
|
||||
addTextField(new GuiNpcTextField(0, this, fontRendererObj, guiLeft + 5, guiTop + 20, 190, 20, folderName));
|
||||
|
||||
addButton(new GuiNpcButton(0, guiLeft + 5, guiTop + 50, 90, 20, "gui.done"));
|
||||
addButton(new GuiNpcButton(1, guiLeft + 100, guiTop + 50, 90, 20, "gui.cancel"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void actionPerformed(GuiButton guibutton) {
|
||||
if (guibutton.id == 0) {
|
||||
// Done
|
||||
folderName = getTextField(0).getText().trim();
|
||||
if (CloneFolder.isValidName(folderName)) {
|
||||
cancelled = false;
|
||||
close();
|
||||
}
|
||||
}
|
||||
if (guibutton.id == 1) {
|
||||
// Cancel
|
||||
cancelled = true;
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRename() {
|
||||
return originalName != null && !originalName.isEmpty();
|
||||
}
|
||||
|
||||
public String getFolderName() {
|
||||
return folderName;
|
||||
}
|
||||
|
||||
public String getOriginalName() {
|
||||
return originalName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package noppes.npcs.client.gui;
|
||||
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import noppes.npcs.client.gui.util.GuiCustomScroll;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.ICustomScrollListener;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SubGuiFolderSelect extends SubGuiInterface implements ICustomScrollListener {
|
||||
|
||||
public String selectedFolder;
|
||||
private final List<String> folderList;
|
||||
private GuiCustomScroll folderScroll;
|
||||
|
||||
public SubGuiFolderSelect(List<String> folders, String currentSelection) {
|
||||
this.folderList = new ArrayList<>(folders);
|
||||
this.selectedFolder = currentSelection;
|
||||
setBackground("menubg.png");
|
||||
xSize = 200;
|
||||
ySize = 220;
|
||||
closeOnEsc = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
folderScroll = new GuiCustomScroll(this, 0);
|
||||
folderScroll.setSize(xSize - 8, ySize - 50);
|
||||
folderScroll.guiLeft = guiLeft + 4;
|
||||
folderScroll.guiTop = guiTop + 4;
|
||||
folderScroll.setList(new ArrayList<>(folderList));
|
||||
if (selectedFolder != null) {
|
||||
folderScroll.setSelected(selectedFolder);
|
||||
}
|
||||
addScroll(folderScroll);
|
||||
|
||||
int btnW = (xSize - 12) / 2;
|
||||
addButton(new GuiNpcButton(0, guiLeft + 4, guiTop + ySize - 44, btnW, 20, "gui.done"));
|
||||
addButton(new GuiNpcButton(1, guiLeft + 4 + btnW + 4, guiTop + ySize - 44, btnW, 20, "gui.cancel"));
|
||||
|
||||
getButton(0).enabled = folderScroll.hasSelected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) {
|
||||
getButton(0).enabled = folderScroll.hasSelected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customScrollDoubleClicked(String selection, GuiCustomScroll guiCustomScroll) {
|
||||
selectedFolder = selection;
|
||||
close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void actionPerformed(GuiButton guibutton) {
|
||||
if (guibutton.id == 0) {
|
||||
selectedFolder = folderScroll.getSelected();
|
||||
close();
|
||||
}
|
||||
if (guibutton.id == 1) {
|
||||
selectedFolder = null;
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package noppes.npcs.client.gui;
|
||||
|
||||
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;
|
||||
|
||||
public class SubGuiMoveConfirm extends SubGuiInterface {
|
||||
public boolean confirmed = false;
|
||||
|
||||
private final int count;
|
||||
public final int destTab;
|
||||
public final String destFolder;
|
||||
|
||||
public SubGuiMoveConfirm(int count, int destTab, String destFolder) {
|
||||
this.count = count;
|
||||
this.destTab = destTab;
|
||||
this.destFolder = destFolder;
|
||||
xSize = 220;
|
||||
ySize = 90;
|
||||
closeOnEsc = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
String line1 = "Move " + count + " clone" + (count != 1 ? "s" : "") + " to:";
|
||||
addLabel(new GuiNpcLabel(0, line1, guiLeft + 10, guiTop + 10, 0xFFFFFF));
|
||||
|
||||
String dest;
|
||||
if (destFolder != null) {
|
||||
dest = "Folder: \"" + destFolder + "\"";
|
||||
} else {
|
||||
dest = "Tab: " + destTab;
|
||||
}
|
||||
addLabel(new GuiNpcLabel(1, " " + dest, guiLeft + 10, guiTop + 25, 0xFFFFFF));
|
||||
|
||||
addButton(new GuiNpcButton(0, guiLeft + 10, guiTop + 55, 95, 20, "gui.yes"));
|
||||
addButton(new GuiNpcButton(1, guiLeft + 115, guiTop + 55, 95, 20, "gui.cancel"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void actionPerformed(GuiButton guibutton) {
|
||||
if (guibutton.id == 0) {
|
||||
confirmed = true;
|
||||
close();
|
||||
}
|
||||
if (guibutton.id == 1) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,18 @@ import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.gui.inventory.GuiContainer;
|
||||
import net.minecraft.inventory.Slot;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.util.EnumChatFormatting;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import net.minecraftforge.event.entity.player.ItemTooltipEvent;
|
||||
import noppes.npcs.client.AuctionClientConfig;
|
||||
import noppes.npcs.constants.EnumClaimType;
|
||||
import noppes.npcs.containers.ContainerAuctionListing;
|
||||
import noppes.npcs.containers.ContainerAuctionSell;
|
||||
import noppes.npcs.containers.ContainerAuctionTrades;
|
||||
import noppes.npcs.containers.SlotAuctionDisplay;
|
||||
import noppes.npcs.controllers.data.AuctionClaim;
|
||||
import noppes.npcs.controllers.data.AuctionListing;
|
||||
|
||||
@@ -19,45 +23,60 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* Handles adding auction information to item tooltips when viewing auction GUIs.
|
||||
* Called from MixinItem when Item.addInformation is invoked.
|
||||
* Called from ItemTooltipEvent in ClientEventHandler.
|
||||
*
|
||||
* Uses slot-based lookup (slot index) instead of ItemStack comparison to correctly
|
||||
* handle duplicate items with different auction data (e.g., two porkchops at different prices).
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public class AuctionTooltipHandler {
|
||||
|
||||
/**
|
||||
* Add auction information to an item's tooltip if we're in an auction GUI.
|
||||
* Handle ItemTooltipEvent - append auction info to the end of the tooltip.
|
||||
*/
|
||||
public static void addAuctionInfo(ItemStack stack, List<String> tooltip) {
|
||||
if (stack == null || tooltip == null) return;
|
||||
public static void onItemTooltip(ItemTooltipEvent event) {
|
||||
if (event.itemStack == null) return;
|
||||
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
if (mc == null || mc.currentScreen == null) return;
|
||||
|
||||
GuiScreen screen = mc.currentScreen;
|
||||
|
||||
// Check if we're in an auction listing GUI
|
||||
if (screen instanceof GuiAuctionListing) {
|
||||
addListingTooltip((GuiAuctionListing) screen, stack, tooltip);
|
||||
}
|
||||
// Check if we're in the trades GUI
|
||||
else if (screen instanceof GuiAuctionTrades) {
|
||||
addTradesTooltip((GuiAuctionTrades) screen, stack, tooltip);
|
||||
}
|
||||
// Check if we're in the sell GUI
|
||||
else if (screen instanceof GuiAuctionSell) {
|
||||
addSellTooltip((GuiAuctionSell) screen, stack, tooltip);
|
||||
addListingTooltip((GuiAuctionListing) screen, event.itemStack, event.toolTip);
|
||||
} else if (screen instanceof GuiAuctionTrades) {
|
||||
addTradesTooltip((GuiAuctionTrades) screen, event.itemStack, event.toolTip);
|
||||
} else if (screen instanceof GuiAuctionSell) {
|
||||
addSellTooltip((GuiAuctionSell) screen, event.itemStack, event.toolTip);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add auction info when viewing the main listings page.
|
||||
* Find the slot in the container whose getStack() returns the exact same ItemStack reference.
|
||||
* This avoids the duplicate item bug where ItemStack.areItemStacksEqual matches the wrong listing.
|
||||
*/
|
||||
private static Slot findHoveredSlot(GuiContainer gui, ItemStack stack) {
|
||||
if (stack == null) return null;
|
||||
for (Object obj : gui.inventorySlots.inventorySlots) {
|
||||
Slot slot = (Slot) obj;
|
||||
if (slot.getHasStack() && slot.getStack() == stack) {
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========== Listing Page ==========
|
||||
|
||||
private static void addListingTooltip(GuiAuctionListing gui, ItemStack stack, List<String> tooltip) {
|
||||
ContainerAuctionListing container = (ContainerAuctionListing) gui.inventorySlots;
|
||||
Slot slot = findHoveredSlot(gui, stack);
|
||||
if (slot == null || !(slot instanceof SlotAuctionDisplay)) return;
|
||||
|
||||
ContainerAuctionListing container = gui.getListingContainer();
|
||||
if (container == null) return;
|
||||
|
||||
// Find the listing that matches this ItemStack
|
||||
AuctionListing listing = container.getListingForItem(stack);
|
||||
int displayIndex = slot.getSlotIndex();
|
||||
AuctionListing listing = container.getListingAt(displayIndex);
|
||||
if (listing == null) return;
|
||||
|
||||
tooltip.add("");
|
||||
@@ -90,149 +109,153 @@ public class AuctionTooltipHandler {
|
||||
tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add auction info when viewing the trades page.
|
||||
*/
|
||||
// ========== Trades Page ==========
|
||||
|
||||
private static void addTradesTooltip(GuiAuctionTrades gui, ItemStack stack, List<String> tooltip) {
|
||||
ContainerAuctionTrades container = (ContainerAuctionTrades) gui.inventorySlots;
|
||||
Slot slot = findHoveredSlot(gui, stack);
|
||||
if (slot == null || !(slot instanceof SlotAuctionDisplay)) return;
|
||||
|
||||
ContainerAuctionTrades container = gui.getTradesContainer();
|
||||
if (container == null) return;
|
||||
|
||||
// Check if it's an item I'm selling
|
||||
AuctionListing sellingListing = container.getSellingListingForItem(stack);
|
||||
if (sellingListing != null) {
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.selling"));
|
||||
int displayIndex = slot.getSlotIndex();
|
||||
|
||||
if (sellingListing.hasBids()) {
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid")
|
||||
.replace("%s", EnumChatFormatting.GOLD + formatCurrency(sellingListing.currentBid)));
|
||||
tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), sellingListing.bidCount));
|
||||
} else {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids"));
|
||||
// Check listings first (selling or bidding)
|
||||
AuctionListing listing = container.getListingAt(displayIndex);
|
||||
if (listing != null) {
|
||||
if (container.isSellingAt(displayIndex)) {
|
||||
addSellingTooltip(listing, tooltip);
|
||||
} else if (container.isBiddingAt(displayIndex)) {
|
||||
addBiddingTooltip(listing, tooltip);
|
||||
}
|
||||
|
||||
long timeRemaining = sellingListing.getTimeRemaining();
|
||||
String timeText = StatCollector.translateToLocal("auction.timeLeft")
|
||||
.replace("%s", formatTimeRemaining(timeRemaining));
|
||||
tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
|
||||
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickToCancel"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's an item I'm bidding on
|
||||
AuctionListing biddingListing = container.getBiddingListingForItem(stack);
|
||||
if (biddingListing != null) {
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.bidding"));
|
||||
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid")
|
||||
.replace("%s", EnumChatFormatting.GOLD + formatCurrency(biddingListing.currentBid)));
|
||||
tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), biddingListing.bidCount));
|
||||
|
||||
long timeRemaining = biddingListing.getTimeRemaining();
|
||||
String timeText = StatCollector.translateToLocal("auction.timeLeft")
|
||||
.replace("%s", formatTimeRemaining(timeRemaining));
|
||||
tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
|
||||
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.clickToRebid"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a claim
|
||||
AuctionClaim claim = container.getClaimForItem(stack);
|
||||
// Check claims
|
||||
AuctionClaim claim = container.getClaimAt(displayIndex);
|
||||
if (claim != null) {
|
||||
tooltip.add("");
|
||||
|
||||
// Title based on claim type
|
||||
switch (claim.type) {
|
||||
case ITEM:
|
||||
if (claim.isReturned) {
|
||||
// Expired - returned to seller
|
||||
tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.expired"));
|
||||
} else {
|
||||
// Won item
|
||||
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.won"));
|
||||
}
|
||||
break;
|
||||
case CURRENCY:
|
||||
// Sold - money to claim
|
||||
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.sold"));
|
||||
if (claim.itemName != null && !claim.itemName.isEmpty()) {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.soldItem")
|
||||
.replace("%s", EnumChatFormatting.WHITE + claim.itemName));
|
||||
}
|
||||
if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.buyer")
|
||||
.replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName));
|
||||
}
|
||||
break;
|
||||
case REFUND:
|
||||
// Outbid - refund available
|
||||
tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.outbid"));
|
||||
if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.outbidBy")
|
||||
.replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Amount for currency claims (including REFUND)
|
||||
if (claim.type.isCurrency()) {
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount")
|
||||
.replace("%s", EnumChatFormatting.GOLD + formatCurrency(claim.currency)));
|
||||
}
|
||||
|
||||
// Expiration
|
||||
int expirationDays = AuctionClientConfig.getClaimExpirationDays();
|
||||
long daysLeft = claim.getDaysUntilExpiration(expirationDays);
|
||||
if (daysLeft <= 1) {
|
||||
tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.claim.expiresSoon"));
|
||||
} else {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.expires")
|
||||
.replace("%s", "" + daysLeft));
|
||||
}
|
||||
|
||||
tooltip.add("");
|
||||
|
||||
// Show rebid option for REFUND claims with item (active auction)
|
||||
if (claim.type == EnumClaimType.REFUND && claim.item != null) {
|
||||
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.leftClickRebid"));
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickRefund"));
|
||||
} else {
|
||||
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim"));
|
||||
}
|
||||
addClaimTooltip(claim, tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add sell page tooltip info.
|
||||
*/
|
||||
private static void addSellingTooltip(AuctionListing listing, List<String> tooltip) {
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.selling"));
|
||||
|
||||
if (listing.hasBids()) {
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid")
|
||||
.replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid)));
|
||||
tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount));
|
||||
} else {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids"));
|
||||
}
|
||||
|
||||
long timeRemaining = listing.getTimeRemaining();
|
||||
String timeText = StatCollector.translateToLocal("auction.timeLeft")
|
||||
.replace("%s", formatTimeRemaining(timeRemaining));
|
||||
tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
|
||||
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickToCancel"));
|
||||
}
|
||||
|
||||
private static void addBiddingTooltip(AuctionListing listing, List<String> tooltip) {
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.bidding"));
|
||||
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid")
|
||||
.replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid)));
|
||||
tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount));
|
||||
|
||||
long timeRemaining = listing.getTimeRemaining();
|
||||
String timeText = StatCollector.translateToLocal("auction.timeLeft")
|
||||
.replace("%s", formatTimeRemaining(timeRemaining));
|
||||
tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
|
||||
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.clickToRebid"));
|
||||
}
|
||||
|
||||
private static void addClaimTooltip(AuctionClaim claim, List<String> tooltip) {
|
||||
tooltip.add("");
|
||||
|
||||
switch (claim.type) {
|
||||
case ITEM:
|
||||
if (claim.isReturned) {
|
||||
tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.expired"));
|
||||
} else {
|
||||
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.won"));
|
||||
}
|
||||
break;
|
||||
case CURRENCY:
|
||||
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.sold"));
|
||||
if (claim.itemName != null && !claim.itemName.isEmpty()) {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.soldItem")
|
||||
.replace("%s", EnumChatFormatting.WHITE + claim.itemName));
|
||||
}
|
||||
if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.buyer")
|
||||
.replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName));
|
||||
}
|
||||
break;
|
||||
case REFUND:
|
||||
tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.outbid"));
|
||||
if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.outbidBy")
|
||||
.replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Amount for currency claims
|
||||
if (claim.type.isCurrency()) {
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount")
|
||||
.replace("%s", EnumChatFormatting.GOLD + formatCurrency(claim.currency)));
|
||||
}
|
||||
|
||||
// Expiration
|
||||
int expirationDays = AuctionClientConfig.getClaimExpirationDays();
|
||||
long daysLeft = claim.getDaysUntilExpiration(expirationDays);
|
||||
if (daysLeft <= 1) {
|
||||
tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.claim.expiresSoon"));
|
||||
} else {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.expires")
|
||||
.replace("%s", "" + daysLeft));
|
||||
}
|
||||
|
||||
tooltip.add("");
|
||||
|
||||
// Rebid option for REFUND claims with item
|
||||
if (claim.type == EnumClaimType.REFUND && claim.item != null) {
|
||||
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.leftClickRebid"));
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickRefund"));
|
||||
} else {
|
||||
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim"));
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Sell Page ==========
|
||||
|
||||
private static void addSellTooltip(GuiAuctionSell gui, ItemStack stack, List<String> tooltip) {
|
||||
Slot slot = findHoveredSlot(gui, stack);
|
||||
if (slot == null) return;
|
||||
|
||||
ContainerAuctionSell container = (ContainerAuctionSell) gui.inventorySlots;
|
||||
if (container == null) return;
|
||||
|
||||
// Check if this is the sell slot item
|
||||
ItemStack sellItem = container.getItemToSell();
|
||||
if (sellItem != null && ItemStack.areItemStacksEqual(stack, sellItem)) {
|
||||
// This is the item in the sell slot
|
||||
if (container.isSellSlot(slot.slotNumber)) {
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.leftClear"));
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.rightRemove"));
|
||||
return;
|
||||
} else if (container.isPlayerInventorySlot(slot.slotNumber)) {
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.leftAdd"));
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.rightAddOne"));
|
||||
}
|
||||
|
||||
// This is an inventory item - show add controls
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.leftAdd"));
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.rightAddOne"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency with commas.
|
||||
*/
|
||||
// ========== Formatting Utilities ==========
|
||||
|
||||
private static String formatCurrency(long amount) {
|
||||
if (amount < 1000) {
|
||||
return amount + " " + AuctionClientConfig.getCurrencyName();
|
||||
@@ -250,9 +273,6 @@ public class AuctionTooltipHandler {
|
||||
return sb.toString() + " " + AuctionClientConfig.getCurrencyName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time remaining.
|
||||
*/
|
||||
private static String formatTimeRemaining(long milliseconds) {
|
||||
if (milliseconds <= 0) {
|
||||
return StatCollector.translateToLocal("auction.ended");
|
||||
|
||||
@@ -112,16 +112,16 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface {
|
||||
mc.renderEngine.bindTexture(AUCTION_BACKGROUND);
|
||||
drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize);
|
||||
|
||||
GL11.glDisable(GL11.GL_LIGHTING);
|
||||
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
|
||||
drawAuctionContent(partialTicks, mouseX, mouseY);
|
||||
|
||||
RenderHelper.enableGUIStandardItemLighting();
|
||||
GL11.glEnable(GL12.GL_RESCALE_NORMAL);
|
||||
GL11.glEnable(GL11.GL_COLOR_MATERIAL);
|
||||
GL11.glEnable(GL11.GL_LIGHTING);
|
||||
|
||||
drawAuctionContent(partialTicks, mouseX, mouseY);
|
||||
|
||||
GL11.glEnable(GL11.GL_LIGHTING);
|
||||
GL11.glEnable(GL11.GL_DEPTH_TEST);
|
||||
RenderHelper.enableStandardItemLighting();
|
||||
|
||||
super.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY);
|
||||
}
|
||||
@@ -167,12 +167,12 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface {
|
||||
Gui.drawRect(x + 1, y + 1, x + 17, y + 17, color);
|
||||
GL11.glDisable(GL11.GL_BLEND);
|
||||
GL11.glEnable(GL11.GL_DEPTH_TEST);
|
||||
GL11.glEnable(GL11.GL_LIGHTING);
|
||||
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
}
|
||||
|
||||
/** Draw dark overlay for unavailable slots */
|
||||
protected void drawDarkenedOverlay(int x, int y) {
|
||||
drawColoredOverlay(x, y, 0x80000000);
|
||||
drawColoredOverlay(x, y, 0xC0000000);
|
||||
}
|
||||
|
||||
/** Draw icon texture on slot (16x16) */
|
||||
|
||||
@@ -38,10 +38,10 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData {
|
||||
private static final int TOTAL_SLOTS = COLS * ROWS;
|
||||
|
||||
// Slot tint colors (ARGB)
|
||||
private static final int TINT_BLUE = 0x303060FF; // Active listing/bid (selling)
|
||||
private static final int TINT_GREEN = 0x3030FF30; // Sold (currency claim) or Won (item claim)
|
||||
private static final int TINT_YELLOW = 0x30FFFF30; // Outbid (refund claim)
|
||||
private static final int TINT_RED = 0x30FF3030; // Expired/Returned (item returned to seller)
|
||||
private static final int TINT_BLUE = 0x605080FF; // Active listing/bid (selling)
|
||||
private static final int TINT_GREEN = 0x6050FF50; // Sold (currency claim) or Won (item claim)
|
||||
private static final int TINT_YELLOW = 0x60FFFF50; // Outbid (refund claim)
|
||||
private static final int TINT_RED = 0x60FF5050; // Expired/Returned (item returned to seller)
|
||||
|
||||
private final ContainerAuctionTrades tradesContainer;
|
||||
private int maxTradeSlots;
|
||||
|
||||
@@ -3,6 +3,7 @@ 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.inventory.Slot;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
@@ -332,6 +333,22 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleMouseClick(Slot slot, int slotId, int mouseButton, int modifier) {
|
||||
// Client-side balance prediction: immediately subtract currency cost on click
|
||||
// so the displayed balance updates instantly without waiting for server response.
|
||||
if (slotId >= 0 && slotId < 18 && mouseButton == 0) {
|
||||
long cost = currencyCost[slotId];
|
||||
if (cost > 0 && playerBalance >= cost) {
|
||||
ItemStack sold = role.inventorySold.items.get(slotId);
|
||||
if (sold != null && container.canBuy(slotId, player)) {
|
||||
playerBalance -= cost;
|
||||
}
|
||||
}
|
||||
}
|
||||
super.handleMouseClick(slot, slotId, mouseButton, modifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
}
|
||||
|
||||
@@ -367,6 +367,13 @@ public class GuiCustomScroll extends GuiScreen {
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setSelectable(boolean selectable) {
|
||||
this.selectable = selectable;
|
||||
if (!selectable) {
|
||||
hover = -1;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMouseOver(int x, int y) {
|
||||
return x >= this.guiLeft && x <= this.guiLeft + this.xSize && y >= this.guiTop && y <= this.guiTop + this.ySize;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package noppes.npcs.client.gui.util;
|
||||
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import noppes.npcs.client.gui.GuiNpcMobSpawner;
|
||||
import noppes.npcs.controllers.data.Tag;
|
||||
@@ -7,10 +8,10 @@ import noppes.npcs.controllers.data.Tag;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GuiCustomScrollCloner extends GuiCustomScroll {
|
||||
private final GuiNpcMobSpawner parent;
|
||||
private final IClonerGui parent;
|
||||
|
||||
public GuiCustomScrollCloner(GuiNpcMobSpawner parent, int id) {
|
||||
super(parent, id);
|
||||
public GuiCustomScrollCloner(IClonerGui parent, int id) {
|
||||
super((GuiScreen) parent, id);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@@ -38,24 +39,25 @@ public class GuiCustomScrollCloner extends GuiCustomScroll {
|
||||
text += "...";
|
||||
} else
|
||||
text = displayString;
|
||||
if ((multipleSelection && selectedList.contains(text)) || (!multipleSelection && selected == i)) {
|
||||
int itemColor = this.colors.getOrDefault(list.get(i), 0xffffff);
|
||||
if ((multipleSelection && selectedList.contains(list.get(i))) || (!multipleSelection && selected == i)) {
|
||||
drawVerticalLine(j - 2, k - 4, k + 10, 0xffffffff);
|
||||
drawVerticalLine(j + xSize - 18 + xOffset, k - 4, k + 10, 0xffffffff);
|
||||
drawHorizontalLine(j - 2, j + xSize - 18 + xOffset, k - 3, 0xffffffff);
|
||||
drawHorizontalLine(j - 2, j + xSize - 18 + xOffset, k + 10, 0xffffffff);
|
||||
fontRendererObj.drawString(text, j, k, this.colors.getOrDefault(text, 0xffffff));
|
||||
fontRendererObj.drawString(text, j, k, itemColor);
|
||||
} else if (i == hover) {
|
||||
fontRendererObj.drawString(text, j, k, 0x00ff00);
|
||||
} else {
|
||||
fontRendererObj.drawString(text, j, k, this.colors.getOrDefault(text, 0xffffff));
|
||||
fontRendererObj.drawString(text, j, k, itemColor);
|
||||
}
|
||||
|
||||
if ((this.parent.getShowingClones() == 0 || this.parent.getShowingClones() == 2) && this.parent.tagMap != null) {
|
||||
if ((parent.getShowingClones() == 0 || parent.getShowingClones() == 2) && parent.getTagMap() != null) {
|
||||
int tagStartX = j + fontRendererObj.getStringWidth(displayString) + 3;
|
||||
if (GuiNpcMobSpawner.displayTags == 0 || GuiNpcMobSpawner.displayTags == 1) {
|
||||
if (this.parent.tagMap.hasClone(list.get(i))) {
|
||||
for (UUID tagUUID : this.parent.tagMap.getUUIDsList(list.get(i))) {
|
||||
Tag tag = this.parent.tags.get(tagUUID);
|
||||
if (parent.getTagMap().hasClone(list.get(i))) {
|
||||
for (UUID tagUUID : parent.getTagMap().getUUIDsList(list.get(i))) {
|
||||
Tag tag = parent.getTags().get(tagUUID);
|
||||
if (tag != null) {
|
||||
if (GuiNpcMobSpawner.displayTags == 1 || !tag.getIsHidden()) {
|
||||
fontRendererObj.drawString("[" + tag.name + "]", tagStartX, k, tag.color);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package noppes.npcs.client.gui.util;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiScreen;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GuiCustomScrollIcons extends GuiCustomScroll {
|
||||
public static final int ICON_NONE = 0;
|
||||
public static final int ICON_TAB = 1;
|
||||
public static final int ICON_FOLDER = 2;
|
||||
|
||||
private static final ResourceLocation TAB_TEXTURE = new ResourceLocation("customnpcs", "textures/gui/cloner/tab.png");
|
||||
private static final ResourceLocation FOLDER_TEXTURE = new ResourceLocation("customnpcs", "textures/gui/cloner/folder.png");
|
||||
|
||||
private static final int ICON_RENDER_SIZE = 10;
|
||||
private static final int ICON_PADDING = 14;
|
||||
|
||||
private List<Integer> iconTypes = new ArrayList<>();
|
||||
|
||||
public GuiCustomScrollIcons(GuiScreen parent, int id) {
|
||||
super(parent, id);
|
||||
}
|
||||
|
||||
public void setListWithIcons(List<String> names, List<Integer> icons) {
|
||||
this.setUnsortedList(names);
|
||||
this.iconTypes = new ArrayList<>(icons);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawItems() {
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
int j = 4;
|
||||
int k = (14 * i + 4) - scrollY;
|
||||
if (k >= 4 && k + 12 < ySize) {
|
||||
int xOffset = scrollHeight < ySize - 8 ? 0 : 10;
|
||||
|
||||
int iconType = (i < iconTypes.size()) ? iconTypes.get(i) : ICON_NONE;
|
||||
int textX = j;
|
||||
if (iconType != ICON_NONE) {
|
||||
textX = j + ICON_PADDING;
|
||||
}
|
||||
|
||||
// Draw icon texture
|
||||
if (iconType == ICON_FOLDER || iconType == ICON_TAB) {
|
||||
ResourceLocation tex = (iconType == ICON_FOLDER) ? FOLDER_TEXTURE : TAB_TEXTURE;
|
||||
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
mc.getTextureManager().bindTexture(tex);
|
||||
GL11.glEnable(GL11.GL_BLEND);
|
||||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
drawFullTexture(j, k - 1, ICON_RENDER_SIZE, ICON_RENDER_SIZE);
|
||||
GL11.glDisable(GL11.GL_BLEND);
|
||||
}
|
||||
|
||||
// Draw text
|
||||
String displayString = StatCollector.translateToLocal(list.get(i));
|
||||
String text = "";
|
||||
float maxWidth = (xSize + xOffset - 8 - (iconType != ICON_NONE ? ICON_PADDING : 0)) * 0.8f;
|
||||
if (fontRendererObj.getStringWidth(displayString) > maxWidth) {
|
||||
for (int h = 0; h < displayString.length(); h++) {
|
||||
char c = displayString.charAt(h);
|
||||
text += c;
|
||||
if (fontRendererObj.getStringWidth(text) > maxWidth)
|
||||
break;
|
||||
}
|
||||
if (displayString.length() > text.length())
|
||||
text += "...";
|
||||
} else {
|
||||
text = displayString;
|
||||
}
|
||||
|
||||
int itemColor = this.colors.getOrDefault(list.get(i), 0xffffff);
|
||||
|
||||
if ((multipleSelection && selectedList.contains(list.get(i))) || (!multipleSelection && selected == i)) {
|
||||
drawVerticalLine(j - 2, k - 4, k + 10, 0xffffffff);
|
||||
drawVerticalLine(j + xSize - 18 + xOffset, k - 4, k + 10, 0xffffffff);
|
||||
drawHorizontalLine(j - 2, j + xSize - 18 + xOffset, k - 3, 0xffffffff);
|
||||
drawHorizontalLine(j - 2, j + xSize - 18 + xOffset, k + 10, 0xffffffff);
|
||||
fontRendererObj.drawString(text, textX, k, itemColor);
|
||||
} else if (i == hover) {
|
||||
fontRendererObj.drawString(text, textX, k, 0x00ff00);
|
||||
} else {
|
||||
fontRendererObj.drawString(text, textX, k, itemColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a full texture (UV 0,0 to 1,1) scaled to the given width/height.
|
||||
* Works with any texture size (not just 256x256).
|
||||
*/
|
||||
private void drawFullTexture(int x, int y, int w, int h) {
|
||||
Tessellator tessellator = Tessellator.instance;
|
||||
tessellator.startDrawingQuads();
|
||||
tessellator.addVertexWithUV(x, y + h, 0, 0, 1);
|
||||
tessellator.addVertexWithUV(x + w, y + h, 0, 1, 1);
|
||||
tessellator.addVertexWithUV(x + w, y, 0, 1, 0);
|
||||
tessellator.addVertexWithUV(x, y, 0, 0, 0);
|
||||
tessellator.draw();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package noppes.npcs.client.gui.util;
|
||||
|
||||
/**
|
||||
* Abstract base class for fullscreen directory-browsing GUIs.
|
||||
* Provides a configurable three-panel layout (left nav, center content, right actions)
|
||||
* with percentage-based sizing, panel backgrounds/borders, and an optional resizable divider.
|
||||
*
|
||||
* Subclasses override abstract hooks to populate each panel region.
|
||||
* Designed for reuse across Cloner, Quest Manager, Dialog Manager, etc.
|
||||
*/
|
||||
public abstract class GuiDirectory extends GuiNPCInterface implements ICustomScrollListener, ISubGuiListener {
|
||||
|
||||
// ===== LAYOUT CONFIG (subclasses may override in constructor) =====
|
||||
protected float leftPanelPercent = 0.20f;
|
||||
protected float rightPanelPercent = 0.10f;
|
||||
protected int minLeftPanelW = 80;
|
||||
protected int minRightPanelW = 40;
|
||||
protected int minCenterW = 100;
|
||||
|
||||
// ===== LAYOUT CONSTANTS =====
|
||||
protected int pad = 10;
|
||||
protected int topBarH = 24;
|
||||
protected int gap = 4;
|
||||
protected int btnH = 20;
|
||||
|
||||
// ===== PANEL COLORS =====
|
||||
protected int panelBg = 0xC0101010;
|
||||
protected int panelBorder = 0xFF333333;
|
||||
protected int topBarBg = 0xC0181818;
|
||||
|
||||
// ===== COMPUTED LAYOUT (populated by computeLayout) =====
|
||||
protected int originX, originY, usableW, usableH;
|
||||
protected int leftPanelW, rightPanelW;
|
||||
protected int contentX, contentY, contentW, contentH;
|
||||
protected int rightX;
|
||||
|
||||
// ===== OPTIONAL RESIZABLE DIVIDER (between left and center) =====
|
||||
protected boolean enableDivider = false;
|
||||
protected int dividerWidth = 5;
|
||||
protected int dividerLineHeight = 20;
|
||||
protected int minDividerPanelW = 50;
|
||||
private int dividerOffset;
|
||||
private boolean isDragging = false;
|
||||
private int dragStartX;
|
||||
|
||||
// ===== CONSTRUCTOR =====
|
||||
public GuiDirectory() {
|
||||
super();
|
||||
closeOnEsc = true;
|
||||
drawDefaultBackground = true;
|
||||
}
|
||||
|
||||
// ===== LAYOUT COMPUTATION =====
|
||||
protected void computeLayout() {
|
||||
originX = pad;
|
||||
originY = pad;
|
||||
usableW = width - 2 * pad;
|
||||
usableH = height - 2 * pad;
|
||||
|
||||
leftPanelW = Math.max(minLeftPanelW, (int)(usableW * leftPanelPercent));
|
||||
rightPanelW = Math.max(minRightPanelW, (int)(usableW * rightPanelPercent));
|
||||
|
||||
if (rightPanelPercent <= 0) {
|
||||
rightPanelW = 0;
|
||||
}
|
||||
|
||||
int gapCount = rightPanelW > 0 ? 3 : 2;
|
||||
contentW = usableW - leftPanelW - rightPanelW - gapCount * gap;
|
||||
if (contentW < minCenterW) {
|
||||
contentW = minCenterW;
|
||||
}
|
||||
|
||||
contentX = originX + leftPanelW + gap;
|
||||
contentY = originY + topBarH + gap;
|
||||
contentH = usableH - topBarH - gap;
|
||||
rightX = contentX + contentW + gap;
|
||||
|
||||
if (enableDivider && dividerOffset == 0) {
|
||||
dividerOffset = leftPanelW;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== INIT GUI =====
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
computeLayout();
|
||||
initTopBar(originY + 2);
|
||||
initLeftPanel();
|
||||
initCenterPanel();
|
||||
if (rightPanelW > 0) {
|
||||
initRightPanel(contentY + 4);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== DRAWING =====
|
||||
@Override
|
||||
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
|
||||
drawTopBar();
|
||||
drawPanels();
|
||||
if (enableDivider) {
|
||||
drawDivider(mouseX, mouseY);
|
||||
}
|
||||
super.drawScreen(mouseX, mouseY, partialTicks);
|
||||
drawOverlay(mouseX, mouseY, partialTicks);
|
||||
}
|
||||
|
||||
protected void drawTopBar() {
|
||||
GuiUtil.drawRectD(originX, originY, originX + usableW, originY + topBarH, topBarBg);
|
||||
}
|
||||
|
||||
protected void drawPanels() {
|
||||
// Left nav panel border
|
||||
GuiUtil.drawRectD(originX - 1, contentY - 1, originX + leftPanelW + 1, originY + usableH + 1, panelBorder);
|
||||
|
||||
// Right action panel border (if present)
|
||||
if (rightPanelW > 0) {
|
||||
GuiUtil.drawRectD(rightX - 1, contentY - 1, rightX + rightPanelW + 1, originY + usableH + 1, panelBorder);
|
||||
}
|
||||
}
|
||||
|
||||
protected void drawDivider(int mouseX, int mouseY) {
|
||||
if (!enableDivider) return;
|
||||
int divX = originX + dividerOffset;
|
||||
int regionTop = contentY;
|
||||
int regionHeight = contentH;
|
||||
int handleTop = regionTop + (regionHeight - dividerLineHeight) / 2;
|
||||
drawRect(divX + 1, handleTop, divX + dividerWidth - 1, handleTop + dividerLineHeight, 0xFF707070);
|
||||
}
|
||||
|
||||
/** Override for extra drawing after super.drawScreen (e.g. status text) */
|
||||
protected void drawOverlay(int mouseX, int mouseY, float partialTicks) {
|
||||
}
|
||||
|
||||
// ===== DIVIDER MOUSE HANDLING =====
|
||||
@Override
|
||||
public void mouseClicked(int mouseX, int mouseY, int mouseButton) {
|
||||
if (enableDivider && !hasSubGui()) {
|
||||
int divX = originX + dividerOffset;
|
||||
int regionTop = contentY;
|
||||
int handleTop = regionTop + (contentH - dividerLineHeight) / 2;
|
||||
int handleBottom = handleTop + dividerLineHeight;
|
||||
if (mouseX >= divX && mouseX <= divX + dividerWidth &&
|
||||
mouseY >= handleTop && mouseY <= handleBottom) {
|
||||
isDragging = true;
|
||||
resizingActive = true;
|
||||
dragStartX = mouseX;
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.mouseClicked(mouseX, mouseY, mouseButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) {
|
||||
if (isDragging) {
|
||||
int dx = mouseX - dragStartX;
|
||||
dragStartX = mouseX;
|
||||
dividerOffset += dx;
|
||||
|
||||
int maxOffset = (usableW - (rightPanelW > 0 ? rightPanelW + gap : 0)) - dividerWidth - minDividerPanelW;
|
||||
dividerOffset = Math.max(minDividerPanelW, Math.min(dividerOffset, maxOffset));
|
||||
|
||||
onDividerMoved(dividerOffset);
|
||||
return;
|
||||
}
|
||||
super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void mouseMovedOrUp(int mouseX, int mouseY, int state) {
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
resizingActive = false;
|
||||
return;
|
||||
}
|
||||
super.mouseMovedOrUp(mouseX, mouseY, state);
|
||||
}
|
||||
|
||||
/** Called when divider is dragged. Override to update scroll sizes. */
|
||||
protected void onDividerMoved(int newOffset) {
|
||||
}
|
||||
|
||||
// Accessors for divider offset
|
||||
protected int getDividerOffset() {
|
||||
return dividerOffset;
|
||||
}
|
||||
|
||||
protected void setDividerOffset(int offset) {
|
||||
this.dividerOffset = offset;
|
||||
}
|
||||
|
||||
// ===== ABSTRACT HOOKS (subclass must implement) =====
|
||||
|
||||
/** Populate top bar buttons. topBtnY is the y-coordinate for the button row. */
|
||||
protected abstract void initTopBar(int topBtnY);
|
||||
|
||||
/** Populate the left navigation panel (scrolls, search fields, buttons). */
|
||||
protected abstract void initLeftPanel();
|
||||
|
||||
/** Populate the center content panel (main scroll list). */
|
||||
protected abstract void initCenterPanel();
|
||||
|
||||
/** Populate the right action panel. startY is the first button y-coordinate. */
|
||||
protected abstract void initRightPanel(int startY);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public class GuiMenuSideButton extends GuiNpcButton {
|
||||
|
||||
public boolean active;
|
||||
public boolean rightSided;
|
||||
public int textColor = -1;
|
||||
public ItemStack renderStack;
|
||||
public ResourceLocation renderResource;
|
||||
public int renderIconPosX = 0;
|
||||
@@ -96,13 +97,15 @@ public class GuiMenuSideButton extends GuiNpcButton {
|
||||
text += "...";
|
||||
} else
|
||||
text = displayString;
|
||||
if (active) {
|
||||
drawCenteredString(fontrenderer, text, xPosition + width / 2, yPosition + (height - 8) / 2, 0xffffa0);
|
||||
} else if (field_146123_n) {
|
||||
drawCenteredString(fontrenderer, text, xPosition + width / 2, yPosition + (height - 8) / 2, 0xffffa0);
|
||||
int color;
|
||||
if (textColor != -1) {
|
||||
color = textColor;
|
||||
} else if (active || field_146123_n) {
|
||||
color = 0xffffa0;
|
||||
} else {
|
||||
drawCenteredString(fontrenderer, text, xPosition + width / 2, yPosition + (height - 8) / 2, 0xe0e0e0);
|
||||
color = 0xe0e0e0;
|
||||
}
|
||||
drawCenteredString(fontrenderer, text, xPosition + width / 2, yPosition + (height - 8) / 2, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package noppes.npcs.client.gui.util;
|
||||
|
||||
import noppes.npcs.controllers.data.Tag;
|
||||
import noppes.npcs.controllers.data.TagMap;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface IClonerGui {
|
||||
int getShowingClones();
|
||||
HashMap<UUID, Tag> getTags();
|
||||
TagMap getTagMap();
|
||||
}
|
||||
@@ -22,6 +22,9 @@ public class ConfigClient {
|
||||
/**
|
||||
* General Properties
|
||||
**/
|
||||
public static Property AllowClientScriptsProperty;
|
||||
public static boolean AllowClientScripts = true;
|
||||
|
||||
public static Property CacheLifeProperty;
|
||||
public static int CacheLife = 10;
|
||||
|
||||
@@ -185,6 +188,9 @@ public class ConfigClient {
|
||||
|
||||
|
||||
// General
|
||||
AllowClientScriptsProperty = config.get(GENERAL, "Allow Client Scripts", true, "Allow the server to run scripts on the client. If disabled, no server scripts will execute client-side.");
|
||||
AllowClientScripts = AllowClientScriptsProperty.getBoolean(true);
|
||||
|
||||
CacheLifeProperty = config.get(GENERAL, "Cache Life", 10, "How long should downloaded imagery data be saved client side? (In minutes)");
|
||||
CacheLife = CacheLifeProperty.getInt(10);
|
||||
|
||||
|
||||
@@ -127,15 +127,11 @@ public class ContainerNPCTrader extends ContainerNpcInterface {
|
||||
givePlayer(soldItem, entityplayer);
|
||||
role.addPurchase(i, entityplayer.getDisplayName());
|
||||
|
||||
// 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)
|
||||
// Always sync the purchasing player's data (balance + stock) after trade.
|
||||
// For shared stocks, consumeStock() syncs stock to other viewers,
|
||||
// but the purchasing player's balance still needs an explicit sync.
|
||||
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()
|
||||
role.syncToPlayer((EntityPlayerMP) entityplayer);
|
||||
}
|
||||
|
||||
return soldItem;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package noppes.npcs.controllers;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.config.ConfigScript;
|
||||
|
||||
/**
|
||||
* Server-side utility for syncing script configuration to clients.
|
||||
*
|
||||
* This class is SERVER-SIDE ONLY. Client reads via ScriptClientConfig.
|
||||
*/
|
||||
public class ScriptConfigSync {
|
||||
|
||||
/**
|
||||
* Write script config to NBT for sending to client.
|
||||
* Called on server side when building login packet.
|
||||
*/
|
||||
public static NBTTagCompound writeToNBT(NBTTagCompound compound) {
|
||||
compound.setBoolean("ScriptingEnabled", ConfigScript.ScriptingEnabled);
|
||||
compound.setBoolean("RunLoadedScriptsFirst", ConfigScript.RunLoadedScriptsFirst);
|
||||
compound.setBoolean("GlobalPlayerScripts", ConfigScript.GlobalPlayerScripts);
|
||||
compound.setBoolean("GlobalForgeScripts", ConfigScript.GlobalForgeScripts);
|
||||
compound.setBoolean("GlobalNPCScripts", ConfigScript.GlobalNPCScripts);
|
||||
return compound;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package noppes.npcs.controllers;
|
||||
import cpw.mods.fml.common.eventhandler.Event;
|
||||
import jdk.nashorn.api.scripting.ScriptObjectMirror;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.NBTTags;
|
||||
import noppes.npcs.config.ConfigScript;
|
||||
import noppes.npcs.constants.EnumScriptType;
|
||||
@@ -104,7 +105,7 @@ public class ScriptContainer implements IScriptUnit {
|
||||
if (!this.evaluated) {
|
||||
// build includes first depending on config setting
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (ConfigScript.RunLoadedScriptsFirst) {
|
||||
if (CustomNpcs.proxy.isRunLoadedScriptsFirst()) {
|
||||
this.appendExternalScripts(sb);
|
||||
}
|
||||
|
||||
@@ -114,7 +115,7 @@ public class ScriptContainer implements IScriptUnit {
|
||||
}
|
||||
|
||||
// build includes last if not built first
|
||||
if (!ConfigScript.RunLoadedScriptsFirst) {
|
||||
if (!CustomNpcs.proxy.isRunLoadedScriptsFirst()) {
|
||||
this.appendExternalScripts(sb);
|
||||
}
|
||||
this.fullscript = sb.toString();
|
||||
@@ -158,14 +159,14 @@ public class ScriptContainer implements IScriptUnit {
|
||||
}
|
||||
|
||||
public void run(EnumScriptType type, Event event) {
|
||||
if (!ConfigScript.ScriptingEnabled)
|
||||
if (!CustomNpcs.proxy.isScriptingEnabled())
|
||||
return;
|
||||
|
||||
this.run((String) type.function, (Object) event);
|
||||
}
|
||||
|
||||
public void run(String type, Object event) {
|
||||
if (!ConfigScript.ScriptingEnabled || errored || !hasCode() || unknownFunctions.contains(type))
|
||||
if (!CustomNpcs.proxy.isScriptingEnabled() || errored || !hasCode() || unknownFunctions.contains(type))
|
||||
return;
|
||||
|
||||
this.setEngine(handler.getLanguage());
|
||||
|
||||
@@ -73,7 +73,7 @@ public class ScriptController {
|
||||
public ScriptController() {
|
||||
Instance = this;
|
||||
manager = new ScriptEngineManager();
|
||||
if (!ConfigScript.ScriptingEnabled)
|
||||
if (!CustomNpcs.proxy.isScriptingEnabled())
|
||||
return;
|
||||
LogWriter.info("Script Engines Available:");
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import noppes.npcs.api.IPos;
|
||||
import noppes.npcs.api.IWorld;
|
||||
import noppes.npcs.api.entity.IEntity;
|
||||
import noppes.npcs.api.handler.ICloneHandler;
|
||||
import noppes.npcs.controllers.data.CloneFolder;
|
||||
import noppes.npcs.controllers.data.TagMap;
|
||||
import noppes.npcs.scripted.CustomNPCsException;
|
||||
import noppes.npcs.scripted.NpcAPI;
|
||||
@@ -27,6 +28,7 @@ import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
@@ -34,8 +36,11 @@ import java.util.UUID;
|
||||
public class ServerCloneController implements ICloneHandler {
|
||||
public static ServerCloneController Instance;
|
||||
|
||||
protected Map<String, CloneFolder> folders = new LinkedHashMap<>();
|
||||
|
||||
public ServerCloneController() {
|
||||
loadClones();
|
||||
loadFolders();
|
||||
}
|
||||
|
||||
private void loadClones() {
|
||||
@@ -103,6 +108,8 @@ public class ServerCloneController implements ICloneHandler {
|
||||
return clones;
|
||||
}
|
||||
|
||||
// ==================== Tab-Based Clone Operations ====================
|
||||
|
||||
public NBTTagCompound getCloneData(ICommandSender player, String name, int tab) {
|
||||
File file = new File(new File(getDir(), tab + ""), name + ".json");
|
||||
if (!file.exists()) {
|
||||
@@ -194,6 +201,250 @@ public class ServerCloneController implements ICloneHandler {
|
||||
return name;
|
||||
}
|
||||
|
||||
// ==================== Folder Registry ====================
|
||||
|
||||
public void loadFolders() {
|
||||
folders.clear();
|
||||
File dir = getDir();
|
||||
File[] files = dir.listFiles();
|
||||
if (files != null) {
|
||||
List<File> folderDirs = new ArrayList<>();
|
||||
for (File f : files) {
|
||||
if (!f.isDirectory()) continue;
|
||||
String name = f.getName();
|
||||
if (name.startsWith("___")) continue;
|
||||
try {
|
||||
Integer.parseInt(name);
|
||||
continue;
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
folderDirs.add(f);
|
||||
}
|
||||
folderDirs.sort(Comparator.comparing(File::getName));
|
||||
for (File f : folderDirs) {
|
||||
CloneFolder folder = new CloneFolder(f.getName());
|
||||
folder.createdDate = f.lastModified();
|
||||
folders.put(f.getName(), folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<CloneFolder> getFolderList() {
|
||||
return new ArrayList<>(folders.values());
|
||||
}
|
||||
|
||||
public List<String> getFolderNames() {
|
||||
return new ArrayList<>(folders.keySet());
|
||||
}
|
||||
|
||||
public boolean hasFolder(String name) {
|
||||
return folders.containsKey(name);
|
||||
}
|
||||
|
||||
// ==================== Folder CRUD ====================
|
||||
|
||||
public CloneFolder createFolder(String name) {
|
||||
if (!CloneFolder.isValidName(name) || folders.containsKey(name)) {
|
||||
return null;
|
||||
}
|
||||
CloneFolder folder = new CloneFolder(name);
|
||||
File dir = getFolderDir(name);
|
||||
if (!dir.exists()) {
|
||||
dir.mkdir();
|
||||
}
|
||||
folders.put(name, folder);
|
||||
return folder;
|
||||
}
|
||||
|
||||
public boolean renameFolder(String oldName, String newName) {
|
||||
if (!folders.containsKey(oldName) || !CloneFolder.isValidName(newName) || folders.containsKey(newName)) {
|
||||
return false;
|
||||
}
|
||||
CloneFolder folder = folders.get(oldName);
|
||||
File oldDir = getFolderDir(oldName);
|
||||
File newDir = new File(getDir(), newName);
|
||||
if (!oldDir.renameTo(newDir)) {
|
||||
return false;
|
||||
}
|
||||
folders.remove(oldName);
|
||||
folder.name = newName;
|
||||
folders.put(newName, folder);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean deleteFolder(String name) {
|
||||
if (!folders.containsKey(name)) {
|
||||
return false;
|
||||
}
|
||||
List<String> clones = getClones(name);
|
||||
if (!clones.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
File dir = getFolderDir(name);
|
||||
if (dir.exists()) {
|
||||
File tagMapFile = new File(dir, "___tagmap.dat");
|
||||
if (tagMapFile.exists()) tagMapFile.delete();
|
||||
File tagMapOld = new File(dir, "___tagmap.dat_old");
|
||||
if (tagMapOld.exists()) tagMapOld.delete();
|
||||
File tagMapNew = new File(dir, "___tagmap.dat_new");
|
||||
if (tagMapNew.exists()) tagMapNew.delete();
|
||||
dir.delete();
|
||||
}
|
||||
folders.remove(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== Folder-Based Clone Operations ====================
|
||||
|
||||
public File getFolderDir(String folderName) {
|
||||
return new File(getDir(), folderName);
|
||||
}
|
||||
|
||||
public List<String> getClones(String folderName) {
|
||||
List<String> list = new ArrayList<>();
|
||||
File dir = getFolderDir(folderName);
|
||||
if (!dir.exists() || !dir.isDirectory())
|
||||
return list;
|
||||
for (String file : dir.list()) {
|
||||
if (file.endsWith(".json"))
|
||||
list.add(file.substring(0, file.length() - 5));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<String> getClonesDate(String folderName) {
|
||||
List<String> list = new ArrayList<>();
|
||||
File dir = getFolderDir(folderName);
|
||||
if (!dir.exists() || !dir.isDirectory())
|
||||
return list;
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) return list;
|
||||
Arrays.sort(files, new Comparator<File>() {
|
||||
public int compare(File f1, File f2) {
|
||||
return Long.compare(f1.lastModified(), f2.lastModified());
|
||||
}
|
||||
});
|
||||
for (File file : files) {
|
||||
String fileName = file.getName();
|
||||
if (fileName.endsWith(".json"))
|
||||
list.add(fileName.substring(0, fileName.length() - 5));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public NBTTagCompound getCloneData(ICommandSender player, String name, String folderName) {
|
||||
File file = new File(getFolderDir(folderName), name + ".json");
|
||||
if (!file.exists()) {
|
||||
if (player != null)
|
||||
player.addChatMessage(new ChatComponentText("Could not find clone file"));
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return NBTJsonUtil.LoadFile(file);
|
||||
} catch (Exception e) {
|
||||
LogWriter.error("Error loading: " + file.getAbsolutePath(), e);
|
||||
if (player != null)
|
||||
player.addChatMessage(new ChatComponentText(e.getMessage()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void saveClone(String folderName, String name, NBTTagCompound compound) {
|
||||
try {
|
||||
File dir = getFolderDir(folderName);
|
||||
if (!dir.exists()) dir.mkdir();
|
||||
String filename = name + ".json";
|
||||
File file = new File(dir, filename + "_new");
|
||||
File file2 = new File(dir, filename);
|
||||
NBTJsonUtil.SaveFile(file, compound);
|
||||
addToTagMap(compound, name, folderName);
|
||||
if (file2.exists()) {
|
||||
file2.delete();
|
||||
}
|
||||
file.renameTo(file2);
|
||||
} catch (Exception e) {
|
||||
LogWriter.except(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removeClone(String name, String folderName) {
|
||||
File file = new File(getFolderDir(folderName), name + ".json");
|
||||
if (!file.exists())
|
||||
return false;
|
||||
file.delete();
|
||||
removeFromTagMap(name, folderName);
|
||||
return true;
|
||||
}
|
||||
|
||||
public String addClone(NBTTagCompound nbttagcompound, String name, String folderName) {
|
||||
cleanTags(nbttagcompound);
|
||||
saveClone(folderName, name, nbttagcompound);
|
||||
return name;
|
||||
}
|
||||
|
||||
public String addClone(NBTTagCompound nbttagcompound, String name, String folderName, NBTTagCompound tempTags) {
|
||||
cleanTagList(nbttagcompound, tempTags);
|
||||
cleanTags(nbttagcompound);
|
||||
saveClone(folderName, name, nbttagcompound);
|
||||
return name;
|
||||
}
|
||||
|
||||
// ==================== Move Operations ====================
|
||||
|
||||
public boolean moveClone(String cloneName, int fromTab, String toFolder) {
|
||||
NBTTagCompound data = getCloneData(null, cloneName, fromTab);
|
||||
if (data == null) return false;
|
||||
|
||||
HashSet<UUID> tagUUIDs = getTagUUIDsFromTab(cloneName, fromTab);
|
||||
|
||||
saveClone(toFolder, cloneName, data);
|
||||
setTagUUIDsForFolder(cloneName, toFolder, tagUUIDs);
|
||||
|
||||
removeClone(cloneName, fromTab);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean moveClone(String cloneName, String fromFolder, int toTab) {
|
||||
NBTTagCompound data = getCloneData(null, cloneName, fromFolder);
|
||||
if (data == null) return false;
|
||||
|
||||
HashSet<UUID> tagUUIDs = getTagUUIDsFromFolder(cloneName, fromFolder);
|
||||
|
||||
saveClone(toTab, cloneName, data);
|
||||
setTagUUIDsForTab(cloneName, toTab, tagUUIDs);
|
||||
|
||||
removeClone(cloneName, fromFolder);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean moveClone(String cloneName, String fromFolder, String toFolder) {
|
||||
NBTTagCompound data = getCloneData(null, cloneName, fromFolder);
|
||||
if (data == null) return false;
|
||||
|
||||
HashSet<UUID> tagUUIDs = getTagUUIDsFromFolder(cloneName, fromFolder);
|
||||
|
||||
saveClone(toFolder, cloneName, data);
|
||||
setTagUUIDsForFolder(cloneName, toFolder, tagUUIDs);
|
||||
|
||||
removeClone(cloneName, fromFolder);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean moveClone(String cloneName, int fromTab, int toTab) {
|
||||
NBTTagCompound data = getCloneData(null, cloneName, fromTab);
|
||||
if (data == null) return false;
|
||||
|
||||
HashSet<UUID> tagUUIDs = getTagUUIDsFromTab(cloneName, fromTab);
|
||||
|
||||
saveClone(toTab, cloneName, data);
|
||||
setTagUUIDsForTab(cloneName, toTab, tagUUIDs);
|
||||
|
||||
removeClone(cloneName, fromTab);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== Tag Utilities ====================
|
||||
|
||||
public NBTTagCompound cleanTagList(NBTTagCompound nbttagcompound, NBTTagCompound tempTags) {
|
||||
HashSet<UUID> tagUUIDs = new HashSet<UUID>();
|
||||
if (nbttagcompound.hasKey("TagUUIDs")) {
|
||||
@@ -243,6 +494,25 @@ public class ServerCloneController implements ICloneHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean addToTagMap(NBTTagCompound nbttagcompound, String name, String folderName) {
|
||||
HashSet<UUID> tagUUIDs = new HashSet<>();
|
||||
if (nbttagcompound.hasKey("TagUUIDs")) {
|
||||
NBTTagList nbtTagList = nbttagcompound.getTagList("TagUUIDs", 8);
|
||||
for (int i = 0; i < nbtTagList.tagCount(); i++) {
|
||||
tagUUIDs.add(UUID.fromString(nbtTagList.getStringTagAt(i)));
|
||||
}
|
||||
}
|
||||
|
||||
TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName);
|
||||
if (!tagUUIDs.isEmpty()) {
|
||||
tagMap.putClone(name, tagUUIDs);
|
||||
} else {
|
||||
tagMap.removeClone(name);
|
||||
}
|
||||
ServerTagMapController.Instance.saveTagMap(tagMap);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean removeFromTagMap(String name, int tab) {
|
||||
TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab);
|
||||
if (tagMap.removeClone(name)) {
|
||||
@@ -252,6 +522,41 @@ public class ServerCloneController implements ICloneHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean removeFromTagMap(String name, String folderName) {
|
||||
TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName);
|
||||
if (tagMap.removeClone(name)) {
|
||||
ServerTagMapController.Instance.saveTagMap(tagMap);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected HashSet<UUID> getTagUUIDsFromTab(String cloneName, int tab) {
|
||||
TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab);
|
||||
HashSet<UUID> uuids = tagMap.getUUIDs(cloneName);
|
||||
return uuids != null ? new HashSet<>(uuids) : new HashSet<>();
|
||||
}
|
||||
|
||||
protected HashSet<UUID> getTagUUIDsFromFolder(String cloneName, String folderName) {
|
||||
TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName);
|
||||
HashSet<UUID> uuids = tagMap.getUUIDs(cloneName);
|
||||
return uuids != null ? new HashSet<>(uuids) : new HashSet<>();
|
||||
}
|
||||
|
||||
protected void setTagUUIDsForTab(String cloneName, int tab, HashSet<UUID> tagUUIDs) {
|
||||
if (tagUUIDs == null || tagUUIDs.isEmpty()) return;
|
||||
TagMap tagMap = ServerTagMapController.Instance.getTagMap(tab);
|
||||
tagMap.putClone(cloneName, tagUUIDs);
|
||||
ServerTagMapController.Instance.saveTagMap(tagMap);
|
||||
}
|
||||
|
||||
protected void setTagUUIDsForFolder(String cloneName, String folderName, HashSet<UUID> tagUUIDs) {
|
||||
if (tagUUIDs == null || tagUUIDs.isEmpty()) return;
|
||||
TagMap tagMap = ServerTagMapController.Instance.getTagMap(folderName);
|
||||
tagMap.putClone(cloneName, tagUUIDs);
|
||||
ServerTagMapController.Instance.saveTagMap(tagMap);
|
||||
}
|
||||
|
||||
public void cleanTags(NBTTagCompound nbttagcompound) {
|
||||
if (nbttagcompound.hasKey("ItemGiverId"))
|
||||
nbttagcompound.setInteger("ItemGiverId", 0);
|
||||
@@ -288,6 +593,8 @@ public class ServerCloneController implements ICloneHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== API Methods (ICloneHandler) ====================
|
||||
|
||||
public IEntity spawn(double x, double y, double z, int tab, String name, IWorld world, boolean ignoreProtection) {
|
||||
NBTTagCompound compound = this.getCloneData((ICommandSender) null, name, tab);
|
||||
if (compound == null) {
|
||||
@@ -327,7 +634,7 @@ public class ServerCloneController implements ICloneHandler {
|
||||
for (File file : dir.listFiles()) {
|
||||
if (file.getName().endsWith(".json")) {
|
||||
NBTTagCompound compound = NBTJsonUtil.LoadFile(file);
|
||||
Instance.cleanTags(compound);
|
||||
cleanTags(compound);
|
||||
Entity entity = EntityList.createEntityFromNBT(compound, world.getMCWorld());
|
||||
arrayList.add(entity == null ? null : NpcAPI.Instance().getIEntity(entity));
|
||||
}
|
||||
@@ -343,7 +650,7 @@ public class ServerCloneController implements ICloneHandler {
|
||||
if (compound == null) {
|
||||
throw new CustomNPCsException("Unknown clone tab:" + tab + " name:" + name, new Object[0]);
|
||||
} else {
|
||||
Instance.cleanTags(compound);
|
||||
cleanTags(compound);
|
||||
Entity entity = EntityList.createEntityFromNBT(compound, world.getMCWorld());
|
||||
return entity == null ? null : NpcAPI.Instance().getIEntity(entity);
|
||||
}
|
||||
@@ -354,7 +661,6 @@ public class ServerCloneController implements ICloneHandler {
|
||||
return compound != null;
|
||||
}
|
||||
|
||||
|
||||
public void set(int tab, String name, IEntity entity) {
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
if (!entity.getMCEntity().writeMountToNBT(compound))
|
||||
@@ -367,4 +673,85 @@ public class ServerCloneController implements ICloneHandler {
|
||||
public void remove(int tab, String name) {
|
||||
this.removeClone(name, tab);
|
||||
}
|
||||
|
||||
// --- Folder API Methods ---
|
||||
|
||||
public String[] getFolders() {
|
||||
return getFolderNames().toArray(new String[0]);
|
||||
}
|
||||
|
||||
public IEntity spawn(double x, double y, double z, String folderName, String name, IWorld world, boolean ignoreProtection) {
|
||||
NBTTagCompound compound = getCloneData(null, name, folderName);
|
||||
if (compound == null) {
|
||||
throw new CustomNPCsException("Unknown clone folder:" + folderName + " name:" + name, new Object[0]);
|
||||
}
|
||||
Entity entity;
|
||||
if (!ignoreProtection) {
|
||||
entity = NoppesUtilServer.spawnCloneWithProtection(compound, (int) x, (int) y, (int) z, world.getMCWorld());
|
||||
} else {
|
||||
entity = NoppesUtilServer.spawnClone(compound, (int) x, (int) y, (int) z, world.getMCWorld());
|
||||
}
|
||||
return entity == null ? null : NpcAPI.Instance().getIEntity(entity);
|
||||
}
|
||||
|
||||
public IEntity spawn(IPos pos, String folderName, String name, IWorld world, boolean ignoreProtection) {
|
||||
return this.spawn(pos.getX(), pos.getY(), pos.getZ(), folderName, name, world, ignoreProtection);
|
||||
}
|
||||
|
||||
public IEntity spawn(double x, double y, double z, String folderName, String name, IWorld world) {
|
||||
return spawn(x, y, z, folderName, name, world, true);
|
||||
}
|
||||
|
||||
public IEntity spawn(IPos pos, String folderName, String name, IWorld world) {
|
||||
return this.spawn(pos.getX(), pos.getY(), pos.getZ(), folderName, name, world);
|
||||
}
|
||||
|
||||
public IEntity[] getFolder(String folderName, IWorld world) {
|
||||
File dir = getFolderDir(folderName);
|
||||
if (!dir.exists() || !dir.isDirectory() || dir.listFiles() == null) {
|
||||
return new IEntity[]{};
|
||||
}
|
||||
|
||||
ArrayList<IEntity> arrayList = new ArrayList<>();
|
||||
try {
|
||||
for (File file : dir.listFiles()) {
|
||||
if (file.getName().endsWith(".json")) {
|
||||
NBTTagCompound compound = NBTJsonUtil.LoadFile(file);
|
||||
cleanTags(compound);
|
||||
Entity entity = EntityList.createEntityFromNBT(compound, world.getMCWorld());
|
||||
arrayList.add(entity == null ? null : NpcAPI.Instance().getIEntity(entity));
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return arrayList.toArray(new IEntity[]{});
|
||||
}
|
||||
|
||||
public IEntity get(String folderName, String name, IWorld world) {
|
||||
NBTTagCompound compound = getCloneData(null, name, folderName);
|
||||
if (compound == null) {
|
||||
throw new CustomNPCsException("Unknown clone folder:" + folderName + " name:" + name, new Object[0]);
|
||||
}
|
||||
cleanTags(compound);
|
||||
Entity entity = EntityList.createEntityFromNBT(compound, world.getMCWorld());
|
||||
return entity == null ? null : NpcAPI.Instance().getIEntity(entity);
|
||||
}
|
||||
|
||||
public boolean has(String folderName, String name) {
|
||||
NBTTagCompound compound = getCloneData(null, name, folderName);
|
||||
return compound != null;
|
||||
}
|
||||
|
||||
public void set(String folderName, String name, IEntity entity) {
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
if (!entity.getMCEntity().writeMountToNBT(compound))
|
||||
throw new CustomNPCsException("Cannot save dead entities", new Object[0]);
|
||||
|
||||
this.cleanTags(compound);
|
||||
this.saveClone(folderName, name, compound);
|
||||
}
|
||||
|
||||
public void remove(String folderName, String name) {
|
||||
this.removeClone(name, folderName);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,13 @@ public class ServerTagMapController {
|
||||
return dir;
|
||||
}
|
||||
|
||||
public File getCloneFolderDir(String folderName) {
|
||||
File dir = new File(getDir(), folderName);
|
||||
if (!dir.exists())
|
||||
dir.mkdir();
|
||||
return dir;
|
||||
}
|
||||
|
||||
public TagMap getTagMap(int tab) {
|
||||
this.tagMap = new TagMap(tab);
|
||||
try {
|
||||
@@ -65,9 +72,33 @@ public class ServerTagMapController {
|
||||
this.tagMap.readNBT(nbtCompound);
|
||||
}
|
||||
|
||||
public TagMap getTagMap(String folderName) {
|
||||
this.tagMap = new TagMap(folderName);
|
||||
try {
|
||||
File file = new File(getCloneFolderDir(folderName), "___tagmap.dat");
|
||||
if (file.exists()) {
|
||||
loadTagMapFile(file);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
File file = new File(getCloneFolderDir(folderName), "___tagmap.dat_old");
|
||||
if (file.exists()) {
|
||||
loadTagMapFile(file);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return this.tagMap;
|
||||
}
|
||||
|
||||
public void saveTagMap(TagMap tagMap) {
|
||||
try {
|
||||
File saveDir = getCloneTabDir(tagMap.cloneTab);
|
||||
File saveDir;
|
||||
if (tagMap.cloneFolder != null) {
|
||||
saveDir = getCloneFolderDir(tagMap.cloneFolder);
|
||||
} else {
|
||||
saveDir = getCloneTabDir(tagMap.cloneTab);
|
||||
}
|
||||
File file = new File(saveDir, "___tagmap.dat_new");
|
||||
File file1 = new File(saveDir, "___tagmap.dat_old");
|
||||
File file2 = new File(saveDir, "___tagmap.dat");
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package noppes.npcs.controllers.data;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
public class CloneFolder {
|
||||
public String name;
|
||||
public long createdDate;
|
||||
|
||||
public CloneFolder() {
|
||||
this.name = "";
|
||||
this.createdDate = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public CloneFolder(String name) {
|
||||
this.name = name;
|
||||
this.createdDate = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public void readNBT(NBTTagCompound compound) {
|
||||
name = compound.getString("Name");
|
||||
createdDate = compound.getLong("Created");
|
||||
}
|
||||
|
||||
public NBTTagCompound writeNBT(NBTTagCompound compound) {
|
||||
compound.setString("Name", name);
|
||||
compound.setLong("Created", createdDate);
|
||||
return compound;
|
||||
}
|
||||
|
||||
public static boolean isValidName(String name) {
|
||||
if (name == null || name.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (name.length() > 32) {
|
||||
return false;
|
||||
}
|
||||
if (!name.equals(name.trim())) {
|
||||
return false;
|
||||
}
|
||||
if (name.equals(".") || name.equals("..")) {
|
||||
return false;
|
||||
}
|
||||
if (name.startsWith("___")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Integer.parseInt(name);
|
||||
return false;
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
for (char c : name.toCharArray()) {
|
||||
if (c == '/' || c == '\\' || c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|') {
|
||||
return false;
|
||||
}
|
||||
if (c < 32) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,13 @@ import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.WorldServer;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.EventHooks;
|
||||
import noppes.npcs.LogWriter;
|
||||
import noppes.npcs.NBTTags;
|
||||
import noppes.npcs.api.IWorld;
|
||||
import noppes.npcs.api.entity.ICustomNpc;
|
||||
import noppes.npcs.config.ConfigDebug;
|
||||
import noppes.npcs.config.ConfigScript;
|
||||
import noppes.npcs.constants.EnumScriptType;
|
||||
import noppes.npcs.constants.ScriptContext;
|
||||
import noppes.npcs.controllers.ScriptContainer;
|
||||
@@ -74,7 +74,7 @@ public class DataScript implements IScriptHandlerPacket {
|
||||
public void readFromNBT(NBTTagCompound compound) {
|
||||
scripts = readScript(compound.getTagList("ScriptsContainers", 10));
|
||||
this.scriptLanguage = compound.getString("ScriptLanguage");
|
||||
if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) {
|
||||
if (scriptLanguage == null || scriptLanguage.isEmpty()) {
|
||||
if (!ScriptController.Instance.languages.isEmpty()) {
|
||||
this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0];
|
||||
} else {
|
||||
@@ -209,7 +209,7 @@ public class DataScript implements IScriptHandlerPacket {
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled && ScriptController.HasStart && !npc.worldObj.isRemote && !scripts.isEmpty() && ConfigScript.ScriptingEnabled;
|
||||
return enabled && ScriptController.HasStart && !npc.worldObj.isRemote && !scripts.isEmpty() && CustomNpcs.proxy.isScriptingEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,8 +12,8 @@ import net.minecraftforge.event.world.ChunkDataEvent;
|
||||
import net.minecraftforge.event.world.ChunkEvent;
|
||||
import net.minecraftforge.event.world.ChunkWatchEvent;
|
||||
import net.minecraftforge.event.world.WorldEvent;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.EventHooks;
|
||||
import noppes.npcs.config.ConfigScript;
|
||||
import noppes.npcs.constants.ScriptContext;
|
||||
import noppes.npcs.controllers.ScriptContainer;
|
||||
import noppes.npcs.controllers.ScriptController;
|
||||
@@ -36,7 +36,7 @@ public class ForgeDataScript extends MultiScriptHandler {
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled && ConfigScript.GlobalForgeScripts && ScriptController.HasStart && this.scripts.size() > 0;
|
||||
return this.enabled && CustomNpcs.proxy.isGlobalForgeScripts() && ScriptController.HasStart && this.scripts.size() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,8 +3,8 @@ package noppes.npcs.controllers.data;
|
||||
import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.api.entity.ICustomNpc;
|
||||
import noppes.npcs.config.ConfigScript;
|
||||
import noppes.npcs.constants.ScriptContext;
|
||||
import noppes.npcs.controllers.ScriptController;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
@@ -28,7 +28,7 @@ public class GlobalNPCDataScript extends MultiScriptHandler {
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return ConfigScript.GlobalNPCScripts && this.enabled && ScriptController.HasStart && this.scripts.size() > 0;
|
||||
return CustomNpcs.proxy.isGlobalNPCScripts() && this.enabled && ScriptController.HasStart && this.scripts.size() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -55,7 +55,7 @@ public abstract class MultiScriptHandler extends ScriptHandler implements IScrip
|
||||
* Normalize the language to a valid one if the current language is not available.
|
||||
*/
|
||||
protected void normalizeLanguage() {
|
||||
if (!ScriptController.Instance.languages.containsKey(this.scriptLanguage)) {
|
||||
if (this.scriptLanguage == null || this.scriptLanguage.isEmpty()) {
|
||||
if (!ScriptController.Instance.languages.isEmpty()) {
|
||||
this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0];
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@ import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.EventHooks;
|
||||
import noppes.npcs.NBTTags;
|
||||
import noppes.npcs.api.entity.IPlayer;
|
||||
@@ -60,7 +61,7 @@ public class PlayerDataScript extends MultiScriptHandler {
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return ConfigScript.GlobalPlayerScripts && ScriptController.Instance.playerScripts.enabled && ScriptController.HasStart && (this.player == null || !this.player.worldObj.isRemote);
|
||||
return CustomNpcs.proxy.isGlobalPlayerScripts() && ScriptController.Instance.playerScripts.enabled && ScriptController.HasStart && (this.player == null || !this.player.worldObj.isRemote);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package noppes.npcs.controllers.data;
|
||||
|
||||
import cpw.mods.fml.common.eventhandler.Event;
|
||||
import noppes.npcs.config.ConfigScript;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.controllers.ScriptController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -14,7 +14,7 @@ public abstract class ScriptHandler implements IScriptHandler {
|
||||
protected long lastInited = -1;
|
||||
|
||||
protected boolean canRunScripts() {
|
||||
return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && scripts != null && !scripts.isEmpty();
|
||||
return enabled && ScriptController.HasStart && CustomNpcs.proxy.isScriptingEnabled() && scripts != null && !scripts.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,7 +5,7 @@ import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.util.ByteBufUtils;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraftforge.common.util.Constants;
|
||||
import noppes.npcs.config.ConfigScript;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.controllers.ScriptController;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -32,7 +32,7 @@ public abstract class SingleScriptHandler extends ScriptHandler {
|
||||
}
|
||||
|
||||
protected boolean canRunScripts() {
|
||||
return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && container != null;
|
||||
return enabled && ScriptController.HasStart && CustomNpcs.proxy.isScriptingEnabled() && container != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -142,7 +142,7 @@ public abstract class SingleScriptHandler extends ScriptHandler {
|
||||
* Normalize the language to a valid one if the current language is not available.
|
||||
*/
|
||||
protected void normalizeLanguage() {
|
||||
if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) {
|
||||
if (this.getLanguage() == null || this.getLanguage().isEmpty()) {
|
||||
if (!ScriptController.Instance.languages.isEmpty()) {
|
||||
this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]);
|
||||
} else {
|
||||
|
||||
@@ -13,10 +13,18 @@ import java.util.UUID;
|
||||
|
||||
public class TagMap {
|
||||
public int cloneTab;
|
||||
public String cloneFolder;
|
||||
public HashMap<String, HashSet<UUID>> tagMap;
|
||||
|
||||
public TagMap(int tab) {
|
||||
this.cloneTab = tab;
|
||||
this.cloneFolder = null;
|
||||
this.tagMap = new HashMap<String, HashSet<UUID>>();
|
||||
}
|
||||
|
||||
public TagMap(String folder) {
|
||||
this.cloneTab = -1;
|
||||
this.cloneFolder = folder;
|
||||
this.tagMap = new HashMap<String, HashSet<UUID>>();
|
||||
}
|
||||
|
||||
|
||||
@@ -1693,7 +1693,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
}
|
||||
|
||||
/**
|
||||
* 1: walking, 2:interacting, 4:attacking, 8:killed
|
||||
* 1: walking, 2:interacting, 4:attacking, 8:killed, 16:rotationLocked
|
||||
*/
|
||||
public boolean getBoolFlag(int id) {
|
||||
return (dataWatcher.getWatchableObjectInt(15) & id) != 0;
|
||||
|
||||
@@ -7,7 +7,6 @@ import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.NBTTags;
|
||||
import noppes.npcs.api.handler.IHookDefinition;
|
||||
import noppes.npcs.config.ConfigScript;
|
||||
import noppes.npcs.constants.EnumScriptType;
|
||||
import noppes.npcs.constants.ScriptContext;
|
||||
import noppes.npcs.controllers.ScriptController;
|
||||
@@ -160,9 +159,9 @@ public abstract class JaninoScript<T> implements IScriptUnit {
|
||||
|
||||
private String getFullCode() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (ConfigScript.RunLoadedScriptsFirst) appendExternalScripts(sb);
|
||||
if (CustomNpcs.proxy.isRunLoadedScriptsFirst()) appendExternalScripts(sb);
|
||||
if (script != null && !script.isEmpty()) sb.append(script).append("\n");
|
||||
if (!ConfigScript.RunLoadedScriptsFirst) appendExternalScripts(sb);
|
||||
if (!CustomNpcs.proxy.isRunLoadedScriptsFirst()) appendExternalScripts(sb);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ public class CustomNPCsMixinPlugin implements IMixinConfigPlugin {
|
||||
}
|
||||
|
||||
mixins.add("MixinItemStack");
|
||||
mixins.add("MixinItem");
|
||||
}
|
||||
return mixins;
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package noppes.npcs.mixin;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.item.Item;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import noppes.npcs.client.gui.player.AuctionTooltipHandler;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mixin to inject auction information into item tooltips when viewing auction GUIs.
|
||||
*/
|
||||
@Mixin(Item.class)
|
||||
public abstract class MixinItem {
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Inject(method = "addInformation", at = @At("HEAD"))
|
||||
public void onAddInformation(ItemStack stack, EntityPlayer player, List tooltip, boolean advanced, CallbackInfo ci) {
|
||||
AuctionTooltipHandler.addAuctionInfo(stack, tooltip);
|
||||
}
|
||||
}
|
||||
@@ -1631,7 +1631,11 @@ ability.cnpc.sweeper=Sweeper
|
||||
ability.weight=Weight
|
||||
ability.windUpTicks=Wind Up
|
||||
ability.cooldownTicks=Cooldown
|
||||
ability.lockMovement=Lock Movement
|
||||
ability.lockPosition=Lock Position
|
||||
ability.lockType=Lock Type
|
||||
ability.lockType.movement=Movement
|
||||
ability.lockType.rotation=Rotation
|
||||
ability.lockType.both=Both
|
||||
ability.interruptible=Interruptible
|
||||
ability.dazedTicks=Dazed Ticks
|
||||
ability.targetingMode=Targeting
|
||||
@@ -1684,6 +1688,7 @@ ability.section.healing=Healing
|
||||
ability.section.trigger=Trigger
|
||||
ability.section.aoe=AOE
|
||||
ability.section.pullDamage=Pull Damage
|
||||
ability.section.hitZone=Hit Zone
|
||||
|
||||
# Shared Sub-Section Labels (used under section headers)
|
||||
gui.radius=Radius
|
||||
@@ -1713,6 +1718,7 @@ ability.maxDistance=Max Distance
|
||||
ability.lifetime=Lifetime
|
||||
ability.stunTicks=Stun Ticks
|
||||
ability.hitWidth=Hit Width
|
||||
ability.hitLength=Hit Length
|
||||
ability.effects=Effects
|
||||
ability.mode=Mode
|
||||
ability.duration=Duration
|
||||
@@ -1856,7 +1862,8 @@ ability.noTypeSettings=No type-specific settings
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
ability.hover.weight=Higher weight = more likely to be chosen when multiple abilities available
|
||||
ability.hover.lockMove=Prevents NPC from moving while ability is charging or active
|
||||
ability.hover.lockPosition=Prevents NPC from moving or rotating while ability is charging or active
|
||||
ability.hover.lockType=What to lock: Movement (pathfinding), Rotation (facing direction), or Both
|
||||
ability.hover.interruptible=If enabled, ability can be interrupted by taking damage
|
||||
ability.hover.dazed=Stun duration in ticks if ability is interrupted (20 ticks = 1 second)
|
||||
ability.hover.windup=Charge-up time before ability activates (20 ticks = 1 second)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 924 B |
Binary file not shown.
|
After Width: | Height: | Size: 932 B |
Reference in new issue
Block a user