diff --git a/.gitignore b/.gitignore index 23d9047f1..908eae39d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ .project /bin/ /config/ +/copilot/ +/buildSrc/build/ /crash-reports/ /logs/ options.txt @@ -39,3 +41,6 @@ addon.late.local.gradle.kts layout.json out run +src/main/resources/assets/customnpcs/api +df/ +tmp/ diff --git a/.gitmodules b/.gitmodules index d8252cb6a..d3428cb3d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = src/api url = https://github.com/KAMKEEL/CustomNPC-Plus-API.git branch = dev +[submodule "gradle-plugins"] + path = gradle-plugins + url = https://github.com/bigguy345/dts-gradle-plugin diff --git a/1.11_Market_Update.md b/1.11_Market_Update.md index 304965950..4236e0e63 100644 --- a/1.11_Market_Update.md +++ b/1.11_Market_Update.md @@ -12,7 +12,7 @@ The 1.11 Market Update introduces a comprehensive economy system for CustomNPC+: ## 1. Currency System -### Status: IMPLEMENTED +### Status: COMPLETE ✅ The currency system provides a unified economy for CNPC+ features. @@ -20,12 +20,32 @@ The currency system provides a unified economy for CNPC+ 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 | |------|---------| -| `PlayerCurrencyData.java` | Per-player currency storage, Vault delegation | -| `IPlayerCurrencyData.java` | API interface for currency operations | +| `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 | @@ -47,17 +67,20 @@ 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 +### Status: COMPLETE ✅ --- -### 2.1 Stock System - IMPLEMENTED +### 2.1 Stock System - COMPLETE ✅ Traders can have limited stock that resets on configurable schedules. @@ -78,7 +101,7 @@ Traders can have limited stock that resets on configurable schedules. | `RLWEEKLY` | Resets every 7 real-life days | ✅ Working | | `RLCUSTOM` | Custom real-time interval | ✅ Working | -#### Stock UI Features - IMPLEMENTED +#### Stock UI Features - COMPLETE ✅ | Feature | Status | Notes | |---------|--------|-------| | Stock count per item | ✅ | Shows to right of output slot | @@ -90,22 +113,11 @@ Traders can have limited stock that resets on configurable schedules. | Reset stock button | ✅ | With confirmation dialog | | Reset cooldown button | ✅ | With confirmation dialog | -#### Key Files -| File | Purpose | -|------|---------| -| `TraderStock.java` | Core stock data class with per-player/global modes | -| `TraderStock.PlayerTraderStock` | Inner class for per-player tracking | -| `EnumStockReset.java` | Reset type enum with intervals | -| `SubGuiNpcTraderStock.java` | Main stock configuration GUI | -| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limit editor | -| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation | -| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation | - --- -### 2.2 Trader Currency Support - IMPLEMENTED +### 2.2 Trader Currency Support - COMPLETE ✅ -Trades can require BOTH items AND currency (additive costs). +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: @@ -114,30 +126,15 @@ Each trade slot can have: - **Currency 2** - Second item cost (existing) ✅ - **Currency Cost** - CNPC+ Currency / Vault cost ✅ NEW -#### Example Trades -| Sold Item | Item Cost | Currency Cost | -|-----------|-----------|---------------| -| Diamond | 2 Emeralds | $0 | -| Diamond Sword | 5 Diamonds | $100 | -| Special Armor | None | $500 | -| Enchanted Book | 1 Book + 10 Lapis | $50 | - #### Implementation Details - Currency cost stored as `long[] currencyCost[18]` in RoleTrader -- Default to 0 (no currency cost) for legacy compatibility ✅ -- Check both item AND currency requirements before purchase ✅ -- Withdraw currency on successful purchase ✅ +- 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 - IMPLEMENTED - -#### Layout (256px wide) -- 3 columns × 6 rows = 18 trade slots -- Column width: 80px -- Per slot: [Currency1][Currency2] = [Output] -- Player inventory positioned below +### 2.3 Player Trader GUI - COMPLETE ✅ #### Display Features | Element | Position | Status | @@ -148,237 +145,134 @@ Each trade slot can have: | Player balance | Bottom-right | ✅ | | Status message | Bottom-left | ✅ On hover | -#### Status Messages (on hover) -| Condition | Message | Color | -|-----------|---------|-------| -| Out of stock | "Out of Stock ($COST)" | Red | -| Can't afford balance | "Insufficient Balance ($COST)" | Red | -| Missing item currency | "Unavailable ($COST)" | Red | -| Slot disabled | "Slot Disabled ($COST)" | Orange | -| Can purchase | "Available ($COST)" | Green | +--- -Cost suffix only appears if trade has currency cost > 0. +### 2.4 Multi-Player Sync - COMPLETE ✅ + +MarketRegistry provides linked market synchronization across all NPCs sharing the same market. --- -### 2.4 Multi-Player Sync - IMPLEMENTED +### 2.5 Scripting API - COMPLETE ✅ -#### MarketRegistry System -Central registry for linked market synchronization across all NPCs sharing the same market. - -| Component | Purpose | -|-----------|---------| -| `MarketRegistry.java` | Central registry tracking traders and viewers per market | -| `marketTraders` | `Map>` - All traders using each market | -| `marketViewers` | `Map>` - All players viewing each market | - -#### Viewer Tracking System -| Feature | Status | Notes | -|---------|--------|-------| -| Register viewer on GUI open | ✅ | For shared stock mode (linked or local) | -| Unregister on GUI close | ✅ | Via onContainerClosed | -| Sync after purchase | ✅ | All viewers updated market-wide | -| Per-player mode optimization | ✅ | Only sync to purchasing player | -| Disconnected player cleanup | ✅ | Removed before sync | -| NPC unload cleanup | ✅ | Via delete() → onUnload() | - -#### Sync Behavior by Mode -| Mode | Linked Market | Behavior | -|------|---------------|----------| -| Shared Stock | Yes | All viewers across ALL NPCs with same market see update immediately | -| Shared Stock | No | All viewers of THIS NPC see update immediately | -| Per-Player Stock | Yes/No | Only purchasing player receives update | - -#### Data Flow -``` -Purchase on NPC1 (market="shop1") - ↓ -consumeStock() → Market.save() → MarketRegistry.syncMarket("shop1") - ↓ -┌──────────────────────────────────────────────┐ -│ 1. Update ALL traders' stock data │ -│ (NPC1, NPC2, NPC3... all with "shop1") │ -│ 2. Sync ALL viewers across ALL those NPCs │ -└──────────────────────────────────────────────┘ -``` - ---- - -### 2.5 Key Files Modified/Created - -| File | Changes | -|------|---------| -| `MarketRegistry.java` | NEW - Central registry for linked market sync | -| `RoleTrader.java` | currencyCost[], viewer tracking (local + registry), syncAllViewers(), resetCooldown(), delete() cleanup | -| `ContainerNPCTrader.java` | Layout constants, currency/stock validation, viewer registration | -| `GuiNPCTrader.java` | Client-side countdown, stock display, status messages | -| `GetTraderData.java` | Network packet for requesting trader data (IGuiData pattern) | -| `TraderStock.java` | Stock management with per-player/global modes, thread-safe methods | -| `EnumStockReset.java` | Reset type definitions with lang key support | -| `SubGuiNpcTraderStock.java` | Stock configuration dialog, custom time validation | -| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limits | -| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation | -| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation | -| `GuiNPCInterface.java` | Fixed SubGui background tiling for smaller widths | -| `Market.java` | Fixed save logic for linked markets | -| `IRoleTrader.java` | Extended API with stock manipulation methods | -| `ScriptRoleTrader.java` | Implemented new API methods | - ---- - -### 2.6 Resolved Issues - -All previously identified issues have been fixed: - -#### Critical Issues - FIXED -| Issue | Fix | -|-------|-----| -| Market load overwrites reset | Reordered `interact()` to load market BEFORE stock reset check | -| recordHistory flag inverted | Removed inverted early return in `Market.save()` | -| API returns wrong item | Fixed `getSellOption()` to return `inventorySold` instead of `inventoryCurrency` | - -#### Medium Priority - FIXED -| Issue | Fix | -|-------|-----| -| Thread safety | Added `synchronized` to `getAvailableStock()`, `consumeStock()`, `resetStock()` | -| Per-player cleanup | `playerStock.clear()` called on stock reset | -| Linked market sync | Implemented `MarketRegistry` for market-wide sync | -| Custom time validation | Added min validation (1 tick MC, 1000ms RL) in `SubGuiNpcTraderStock` | - -#### Low Priority - FIXED -| Issue | Fix | -|-------|-----| -| lastPurchaseTime unused | Removed from `PlayerTraderStock` | -| Currency format | Verified working correctly for all ranges | -| Hardcoded reset types | Now uses `EnumStockReset.getDisplayNames()` with lang keys | - ---- - -### 2.7 Scripting API - IMPLEMENTED - -Full API available via `IRoleTrader` interface: - -```javascript -// Stock Configuration -trader.isStockEnabled() -trader.setStockEnabled(boolean) -trader.isPerPlayerStock() -trader.setPerPlayerStock(boolean) -trader.getStockResetType() // Returns int (0-6 for EnumStockReset) -trader.setStockResetType(int) -trader.getCustomResetTime() -trader.setCustomResetTime(long) - -// Stock Limits -trader.getMaxStock(slot) // Max stock for slot (-1 = unlimited) -trader.setMaxStock(slot, amount) - -// Stock Queries -trader.getAvailableStock(slot) // Global mode -trader.getAvailableStock(slot, player) // Per-player mode -trader.getCurrentStock(slot) // Current stock (global) -trader.getPlayerPurchased(slot, player) // Amount purchased by player - -// Stock Manipulation -trader.setCurrentStock(slot, amount) // Set current stock (global) -trader.setPlayerPurchased(slot, player, amount) // Set player's purchased amount -trader.resetStock() // Reset all stock to max -trader.resetCooldown() // Reset cooldown timer - -// Time Queries -trader.getLastResetTime() // Timestamp of last reset -trader.getTimeUntilReset() // Time remaining until next reset - -// Currency Cost System -trader.getCurrencyCost(slot) -trader.setCurrencyCost(slot, amount) -trader.hasCurrencyCost(slot) -``` +Full API available via `IRoleTrader` interface. --- ## 3. Auction House System -### Status: NOT STARTED (Fresh implementation) +### Status: COMPLETE ✅ ### 3.1 Architecture Overview ``` -┌─────────────────────────────────────────────────────────────┐ -│ AuctionController │ -│ - Manages all auction data │ -│ - Handles listing/bidding/buyout logic │ -│ - Processes claims │ -│ - Saves/loads auction data │ -└─────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────┐ +│ 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 │ +└─────────────────────────────────────────────────────────────────┘ │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ AuctionNotification │ -│ - Tracks pending notifications per player │ -│ - Sends notifications on login │ -│ - Types: Won, Outbid, Sold, Expired, Claimed │ -└─────────────────────────────────────────────────────────────┘ + 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 GUI Structure +### 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 ``` -GuiAuctionHouse (Base GUI - opened via Auctioneer NPC) +GuiAuctionInterface (Base GUI - opened via Auctioneer NPC) │ - ├── GuiAuction (Browse/Search auctions) - │ - Paginated item display (ContainerAuction) - │ - Next/Back page buttons - │ - Filter button → opens GuiAuctionFilter - │ - My Auctions button → opens GuiAuctionClaims - │ - Sell button → opens GuiAuctionSell - │ - Click item → SubGuiAuctionDetails + ├── 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) - │ - Item slot for item to sell - │ - Starting price input - │ - Buyout price input (0 = no buyout) + ├── 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 - │ - Create button + │ - Confirm button with coin icon │ - ├── GuiAuctionClaims (My Auctions / Claims) - │ - Active listings tab - │ - Pending claims tab (items + currency to collect) - │ - Cancel listing button - │ - Claim all button + ├── 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 │ - └── GuiAuctionFilter (Filter settings) - - Search text - - Sort options (EnumAuctionSort) - - Price range (min/max) - - Show my listings only toggle + ├── 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.3 Admin GUI +### 3.4 Client Config Sync -``` -GuiManageAuction (Admin tool - like ManageMagic, ManageQuests) - │ - ├── Active Auctions Tab - │ - View all auctions - │ - Cancel any auction (with reason) - │ - View auction details - │ - ├── Blacklist Tab (Stub for future) - │ - Add/remove blacklisted items - │ - Add/remove blacklisted mods - │ - └── Settings Tab - - View current config values - - Quick reference for config file +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.4 Data Structures +### 3.5 Data Structures #### AuctionListing ```java public class AuctionListing { - int id; // Unique listing ID + String id; // Unique listing ID (UUID-based) UUID sellerUUID; // Seller's UUID String sellerName; // Seller's display name ItemStack item; // Item being sold @@ -397,9 +291,9 @@ public class AuctionListing { #### AuctionClaim ```java public class AuctionClaim { - int id; // Unique claim ID + String id; // Unique claim ID UUID playerUUID; // Player who can claim - int listingId; // Related auction listing + 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) @@ -408,113 +302,21 @@ public class AuctionClaim { } ``` -#### AuctionFilter -```java -public class AuctionFilter { - String searchText; // Item name search - EnumAuctionSort sortBy; // Sort order - long minPrice; // Minimum price filter - long maxPrice; // Maximum price filter (0 = no max) - boolean myListingsOnly; // Show only my listings -} -``` +### 3.6 Security Features -#### Enums -```java -public enum EnumAuctionStatus { - ACTIVE, // Auction is live - ENDED, // Auction ended (winner determined) - CANCELLED, // Cancelled by seller or admin - CLAIMED // All claims processed -} +All actions are validated server-side: -public enum EnumAuctionSort { - NEWEST, // Most recently listed - ENDING_SOON, // Ending soonest - PRICE_LOW, // Lowest current price - PRICE_HIGH, // Highest current price - MOST_BIDS // Most bid activity -} +| 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 | -public enum EnumClaimType { - ITEM, // Winner claims item - CURRENCY, // Seller claims sale currency - REFUND // Outbid player claims refund -} -``` - -### 3.5 AuctionController - -Main controller handling all auction logic. - -#### Core Methods -```java -// Listing Management -int createListing(EntityPlayer seller, ItemStack item, long startingPrice, long buyoutPrice) -void cancelListing(int listingId, EntityPlayer player, boolean isAdmin) -List getActiveListings(AuctionFilter filter, int page, int pageSize) -AuctionListing getListing(int listingId) -int getPlayerListingCount(UUID playerUUID) - -// Bidding -boolean placeBid(int listingId, EntityPlayer bidder, long bidAmount) -boolean buyout(int listingId, EntityPlayer buyer) - -// Claims -List getPlayerClaims(UUID playerUUID) -boolean claimItem(int claimId, EntityPlayer player) -boolean claimCurrency(int claimId, EntityPlayer player) -void claimAll(EntityPlayer player) - -// Lifecycle -void processEndedAuctions() // Called on tick, creates claims for ended auctions -void onPlayerLogin(EntityPlayer player) // Send pending notifications - -// Persistence -void save() -void load() -``` - -#### Auction Flow - -**Creating a Listing:** -1. Validate item not blacklisted -2. Check player has listing slots available -3. Deduct listing fee from player -4. Remove item from player inventory -5. Create AuctionListing with ACTIVE status -6. Save - -**Placing a Bid:** -1. Validate auction is ACTIVE -2. Validate bid > current bid + minimum increment -3. Validate bidder has enough currency -4. Deduct bid amount from bidder -5. If previous bidder exists, create REFUND claim for them -6. Update listing with new bid/bidder -7. Apply snipe protection if needed -8. Save - -**Buyout:** -1. Validate auction is ACTIVE and has buyout price -2. Validate buyer has enough currency -3. Deduct buyout amount from buyer -4. If previous bidder exists, create REFUND claim -5. Create ITEM claim for buyer -6. Create CURRENCY claim for seller (minus sales tax) -7. Set status to ENDED -8. Save - -**Auction Ends (no buyout):** -1. If bids exist: - - Create ITEM claim for high bidder - - Create CURRENCY claim for seller (minus sales tax) -2. If no bids: - - Create ITEM claim for seller (item returned) -3. Set status to ENDED -4. Save - -### 3.6 Snipe Protection +### 3.7 Snipe Protection When a bid is placed with less than `SnipeProtectionMinutes` remaining: - Extend the auction end time TO `SnipeProtectionMinutes` from now @@ -523,288 +325,379 @@ When a bid is placed with less than `SnipeProtectionMinutes` remaining: Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 -### 3.7 Notifications +--- -#### AuctionNotification Class -```java -public class AuctionNotification { - UUID playerUUID; - EnumNotificationType type; - int listingId; - String message; - long timestamp; -} +### 3.8 Item Blacklist System -public enum EnumNotificationType { - AUCTION_WON, // You won an auction - AUCTION_OUTBID, // You were outbid - AUCTION_SOLD, // Your item sold - AUCTION_EXPIRED, // Your auction expired (no bids) - CLAIM_READY // You have items/currency to claim -} +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: [] ``` -Notifications are queued and sent when player logs in. +#### Commands -### 3.8 Permissions - -| Permission | Default | Description | -|------------|---------|-------------| -| `customnpcs.auction.use` | true | Can use auction house | -| `customnpcs.auction.sell` | true | Can create listings | -| `customnpcs.auction.slots.X` | 5 | Max listing slots (X = number) | -| `customnpcs.auction.admin` | op | Admin auction management | +| Command | Description | +|---------|-------------| +| `/cnpc auction blacklist add ` | Add to blacklist (runtime only) | +| `/cnpc auction blacklist remove ` | 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. Configuration +## 4. AuctionController Performance & Thread Safety -### ConfigMarket.java Structure +### 4.1 Threading Model -```java -// ========================================= -// Market General -// ========================================= -public static boolean MarketEnabled = true; - -// ========================================= -// Currency (Market.Currency) -// ========================================= -public static boolean UseVault = false; -public static String CurrencyName = "Coins"; -public static long StartingBalance = 0; -public static long MaxBalance = Long.MAX_VALUE; - -// ========================================= -// Trader (Market.Trader) -// ========================================= -public static boolean EnableStockByDefault = false; -public static String DefaultResetType = "NONE"; - -// ========================================= -// Auction (Market.Auction) -// ========================================= -public static boolean AuctionEnabled = true; -public static int AuctionDuration = 24; // Hours -public static long ListingFee = 10; // Flat fee to create listing -public static double SalesTaxPercent = 0.05; // 5% tax on sales -public static int DefaultMaxListings = 5; // Per player -public static int SnipeProtectionMinutes = 2; // Snipe protection timer -public static int ClaimExpirationDays = 30; // Days before unclaimed items deleted -public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase ``` +┌─────────────────────────────────────────────────────────────────┐ +│ 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 │ │ +│ │ - 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>` | Active listings by seller | +| `playerBidIds` | `Map>` | 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 to Create +### New Files Created ``` constants/ - EnumAuctionStatus.java - EnumAuctionSort.java - EnumClaimType.java - EnumNotificationType.java + EnumAuctionStatus.java ✅ + EnumAuctionSort.java ✅ + EnumClaimType.java ✅ + EnumNotificationType.java ✅ controllers/ - AuctionController.java - AuctionNotification.java + AuctionController.java ✅ (with threading, indices, Vault support) + AuctionConfigSync.java ✅ (server-side config sync utility) controllers/data/ - AuctionListing.java - AuctionClaim.java - AuctionFilter.java + AuctionListing.java ✅ + AuctionClaim.java ✅ + AuctionFilter.java ✅ + AuctionBlacklist.java ✅ (item blacklist checking + bypass) + PlayerTradeData.java ✅ (currency + claims storage per player) roles/ - RoleAuctioneer.java + RoleAuctioneer.java ✅ containers/ - ContainerAuction.java + ContainerAuction.java ✅ + ContainerAuctionListing.java ✅ + ContainerAuctionBidding.java ✅ + ContainerAuctionTrades.java ✅ + ContainerAuctionSell.java ✅ + InventoryAuctionDisplay.java ✅ + SlotAuctionDisplay.java ✅ + +client/ + AuctionClientConfig.java ✅ (client-side config cache) client/gui/player/ - GuiAuctionHouse.java (Base GUI) - GuiAuction.java (Browse auctions) - GuiAuctionSell.java (Create listing) - GuiAuctionClaims.java (My auctions/claims) - GuiAuctionFilter.java (Filter settings) - SubGuiAuctionDetails.java (View listing details) + GuiAuctionInterface.java ✅ + GuiAuctionListing.java ✅ + GuiAuctionSell.java ✅ + GuiAuctionTrades.java ✅ + GuiAuctionBidding.java ✅ + SubGuiAuctionSearch.java ✅ + SubGuiAuctionBid.java ✅ + SubGuiAuctionBuyNow.java ✅ + AuctionTooltipHandler.java ✅ -client/gui/global/ - GuiManageAuction.java (Admin management) +client/gui/util/ + GuiAuctionNavButton.java ✅ -network/packets/ - AuctionDataPacket.java (Server → Client data sync) - AuctionActionPacket.java (Client → Server actions) +network/packets/player/ + AuctionActionPacket.java ✅ ``` -### Files to Modify +### New Commands ``` -CustomNpcs.java - Initialize AuctionController -ServerTickHandler.java - Process ended auctions, login notifications -EnumRoleType.java - Add Auctioneer -EnumGuiType.java - Add auction GUI types -DataAdvanced.java - Handle Auctioneer role -PacketHandler.java - Register auction packets -CustomNpcsPermissions.java - Auction permissions -ConfigMarket.java - Add Auction section -RoleTrader.java - Add currency cost per trade -ContainerNPCTrader.java - Currency + stock integration -en_US.lang - All auction lang keys +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. Data Storage +## 6. Configuration -### Auction Data File -Location: `customnpcs/auction.dat` +### ConfigMarket.java Structure -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 ``` -NBTTagCompound root - ├── NextListingId: int - ├── NextClaimId: int - ├── Listings: NBTTagList - │ └── [AuctionListing NBT...] - ├── Claims: NBTTagList - │ └── [AuctionClaim NBT...] - └── PendingNotifications: NBTTagList - └── [AuctionNotification NBT...] + +### 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 ` | 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 ` | OP | Add to blacklist | +| `remove` | `/cnpc auction blacklist remove ` | 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) ``` --- -## 7. Lang Keys +## 8. Implementation Status -```properties -# Auction House -auction.title=Auction House -auction.browse=Browse -auction.sell=Sell -auction.claims=My Auctions -auction.filter=Filter +### 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 ✅ -# Browse -auction.nextPage=Next > -auction.prevPage=< Prev -auction.page=Page %d of %d -auction.noResults=No auctions found -auction.refresh=Refresh +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~~ -# Listing Display -auction.seller=Seller: %s -auction.currentBid=Current Bid: %s -auction.buyout=Buyout: %s -auction.timeLeft=Time Left: %s -auction.bids=%d bids -auction.noBids=No bids +### Remaining Work -# Create Listing -auction.sell.title=Create Listing -auction.sell.startingPrice=Starting Price -auction.sell.buyoutPrice=Buyout Price -auction.sell.noBuyout=(0 = no buyout) -auction.sell.fee=Listing Fee: %s -auction.sell.create=Create Listing -auction.sell.noItem=Place an item to sell +- [ ] **GuiManageAuction (Admin GUI)** - Admin interface to view all auctions, cancel listings, manage claims +- [ ] Additional testing -# Claims -auction.claims.title=My Auctions -auction.claims.active=Active Listings -auction.claims.pending=Pending Claims -auction.claims.claimAll=Claim All -auction.claims.cancel=Cancel Listing -auction.claims.empty=No pending claims +### Implemented Since Initial Plan -# Filter -auction.filter.title=Filter Auctions -auction.filter.search=Search -auction.filter.minPrice=Min Price -auction.filter.maxPrice=Max Price -auction.filter.myOnly=My Listings Only -auction.filter.apply=Apply - -# Sort Options -auction.sort.newest=Newest -auction.sort.endingSoon=Ending Soon -auction.sort.priceLow=Price: Low to High -auction.sort.priceHigh=Price: High to Low -auction.sort.mostBids=Most Bids - -# Notifications -auction.notify.won=You won the auction for %s! -auction.notify.outbid=You were outbid on %s! -auction.notify.sold=Your %s sold for %s! -auction.notify.expired=Your auction for %s expired with no bids. -auction.notify.claim=You have items to claim at the Auction House. - -# Errors -auction.error.notEnoughCurrency=Not enough currency! -auction.error.auctionEnded=This auction has ended. -auction.error.bidTooLow=Bid must be at least %s. -auction.error.maxListings=You have reached your maximum listings. -auction.error.blacklisted=This item cannot be auctioned. -auction.error.noAuctioneer=You must visit an Auctioneer NPC. - -# Trader Currency -trader.currencyCost=Currency Cost -trader.outOfStock=Out of Stock -trader.stock=Stock: %d -``` - ---- - -## 8. Implementation Order - -### Phase 1: Trader Currency Integration - COMPLETE ✅ -1. ~~Add `currencyCost` field to RoleTrader trades~~ -2. ~~Update ContainerNPCTrader to check/deduct currency~~ -3. ~~Update trader setup GUI with currency input~~ -4. ~~Test with existing item trades (backward compatibility)~~ - -### Phase 2: Trader Stock UI - COMPLETE ✅ -1. ~~Add stock display to player trader GUI~~ -2. ~~Show "Out of Stock" indicator~~ -3. ~~Verify reset timers working~~ -4. ~~Real-time client-side countdown~~ -5. ~~Status messages with costs~~ -6. ~~Reset cooldown button with confirmation~~ - -### Phase 2.5: Trader Sync System - COMPLETE ✅ -1. ~~Implement MarketRegistry for linked market sync~~ -2. ~~Viewer tracking (market-wide and local)~~ -3. ~~NPC unload cleanup~~ -4. ~~Thread safety for stock operations~~ - -### Phase 3: Auction Core -1. Create enums (EnumAuctionStatus, EnumAuctionSort, etc.) -2. Create data classes (AuctionListing, AuctionClaim, AuctionFilter) -3. Create AuctionController with core logic -4. Create RoleAuctioneer -5. Add to CustomNpcs initialization - -### Phase 4: Auction GUI -1. Create GuiAuctionHouse base -2. Create GuiAuction (browse) -3. Create GuiAuctionSell -4. Create GuiAuctionClaims -5. Create GuiAuctionFilter -6. Create SubGuiAuctionDetails -7. Create ContainerAuction - -### Phase 5: Auction Network -1. Create AuctionDataPacket -2. Create AuctionActionPacket -3. Register packets - -### Phase 6: Admin & Polish -1. Create GuiManageAuction -2. Add permissions -3. Add notifications system -4. Add lang keys -5. Testing +- [x] **`/auction` Command** - Console/player command with subcommands: + - `/auction list [page]` - View active auctions from command line + - `/auction view ` - 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 --- @@ -812,23 +705,25 @@ trader.stock=Stock: %d | Decision | Choice | Rationale | |----------|--------|-----------| -| Claims System | Everything to claims | Prevents offline handling issues | -| Auction Duration | Single config value | Simplicity over flexibility | -| Snipe Protection | Reset to X minutes | Fair bidding, prevents last-second sniping | -| Listing Fees | Flat fee only | Simple, predictable | -| Sales Tax | Percentage of sale | Scales with item value | -| Data Storage | Single .dat file | Consistent with quests/dialogs | -| Blacklist | Stub for now | Complex feature for later | -| Currency per Trade | Additive with items | Backward compatible, flexible | +| 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 -- Advanced blacklist system (items, mods, NBT patterns) -- Auction history/statistics -- Command-based auction access -- Auction house tax sink -- Reserve prices -- Private auctions +- 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 diff --git a/Ability_Update_Review.md b/Ability_Update_Review.md new file mode 100644 index 000000000..2ceb38797 --- /dev/null +++ b/Ability_Update_Review.md @@ -0,0 +1,301 @@ +# 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()` diff --git a/Auction_Update.md b/Auction_Update.md new file mode 100644 index 000000000..ae839832f --- /dev/null +++ b/Auction_Update.md @@ -0,0 +1,127 @@ +# 🏪 CustomNPC+ 1.11 - Market Update 🏪 + +--- + +The **Market Update** brings a complete economy system to CNPC+! Create bustling marketplaces with **Traders** that have limited stock, or let players trade with each other through the brand new **Auction House**! + +--- + +## Currency System 💰 + +CNPC+ now has a **built-in currency** used across all Market features! + +- **Fully Configurable** - Name it Coins, Gold, Credits, whatever fits your world +- **Profile Persistent** - Currency carries across all your character slots +- **Vault Support** - CNPC+ integrates seamlessly with Bukkit-compatible economy plugins if Vault is installed + +> Full Scripting API support for currency operations on players. + +--- + +## Auction House 🏛️ + +A **global player marketplace** where players can list items, bid against each other, and purchase instantly! + +### 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. + +> 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**. + +### Buyout + +See something you need right now? Hit **Buy Now** to purchase instantly at the seller's Buyout Price. If you're currently the highest bidder on an item, you only pay the **difference** between your bid and the buyout. + +### My Trades + +One screen to manage all your auction activity: + +- **Active Listings** - Items you're currently selling +- **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. + +### Notifications + +Get notified immediately when your item sells, when you're outbid, or when you win an auction. Offline? You'll get a full **claim summary** when you log back in showing exactly what's waiting for you. + +### 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. + +> Server owners can grant players more slots via permissions up to a maximum of 45. + +--- + +## Item Blacklist 🚫 + +Server owners have full control over what items can be listed on the Auction House. + +**Block by:** +- **Item** - Specific registry names like `minecraft:diamond_sword` +- **Wildcard** - Patterns like `minecraft:diamond*` to block all diamond items +- **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! + +**Blacklist Commands:** +- `/kam auction blacklist add ` - Add entry +- `/kam auction blacklist remove ` - 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 + +--- + +## Trader Enhancements 🛒 + +### Stock System + +Traders now support **limited stock** with automatic restocking! + +**Stock Modes:** +- **Global** - All players share the same stock pool +- **Per-Player** - Each player has their own purchase limits + +**Restock Timers:** +- **MC Daily / Weekly** - Restock based on Minecraft time +- **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 + +> 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! + +--- + +## 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 + +### Auction Command + +Players can also access the Auction House via command: + +- `/kam auction open` - Opens the Auction House GUI + +**Admin Commands:** +- `/kam auction list [page]` - View active auctions in chat +- `/kam auction view ` - View a player's auction activity + +--- + diff --git a/build.gradle b/build.gradle index b76ad9d0c..de4d414d0 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id 'idea' } -version = "1.10.3" +version = "1.11-beta1" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" diff --git a/dependencies.gradle b/dependencies.gradle index cf25adedf..0270713bc 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -41,6 +41,9 @@ dependencies { compileOnly(files("./tools/bukkit-api/spigot-api-ONLY.jar")) compileOnly(files("./tools/bukkit-api/Vault-1.4.1.jar")) + shadowImplementation(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar")) + annotationProcessor(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar")) + if(embedMixin){ // Normal Jar will include built in Mixins shadowImplementation("org.spongepowered:mixin:0.7.11-SNAPSHOT"){ diff --git a/gradle-plugins b/gradle-plugins new file mode 160000 index 000000000..bfa9565fc --- /dev/null +++ b/gradle-plugins @@ -0,0 +1 @@ +Subproject commit bfa9565fce3110adb8fb4e9fd1a52bafdd7d35e6 diff --git a/gradle.properties b/gradle.properties index 48416e9a4..f2e101204 100644 --- a/gradle.properties +++ b/gradle.properties @@ -199,7 +199,7 @@ disableCheckstyle = true # ideaCheckSpotlessOnBuild = true # Non-GTNH properties -modVersion = 1.10.3 +modVersion = 1.11.0 # Increase Gradle's JVM memory usage to avoid daemon expiration due to # insufficient heap space on CI builds. diff --git a/src/main/java/foxz/command/CommandNoppes.java b/src/main/java/foxz/command/CommandNoppes.java index 8c051aa85..f5b5ccd73 100644 --- a/src/main/java/foxz/command/CommandNoppes.java +++ b/src/main/java/foxz/command/CommandNoppes.java @@ -2,9 +2,16 @@ package foxz.command; import net.minecraft.command.CommandBase; import net.minecraft.command.ICommandSender; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; import java.util.List; +/** + * @deprecated The /noppes command is deprecated and no longer supported. + * Please use the /kam command instead for all CNPC+ operations. + */ +@Deprecated public class CommandNoppes extends CommandBase { public CmdNoppes noppes = new CmdNoppes(this); @@ -21,6 +28,15 @@ public class CommandNoppes extends CommandBase { @Override public void processCommand(ICommandSender var1, String[] var2) { + // Send deprecation warning + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "[DEPRECATED] " + + EnumChatFormatting.YELLOW + "The /noppes command is deprecated and no longer supported.")); + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "Please use " + EnumChatFormatting.GREEN + "/kam" + EnumChatFormatting.YELLOW + " commands instead.")); + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + + "Type " + EnumChatFormatting.WHITE + "/kam help" + EnumChatFormatting.GRAY + " for available commands.")); + + // Still execute the command for backwards compatibility noppes.processCommand(var1, var2); } diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java new file mode 100644 index 000000000..4912e4b72 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java @@ -0,0 +1,609 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; +import kamkeel.npcs.entity.EntityAbilityBeam; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.entity.Entity; +import net.minecraft.util.Vec3; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * Renders the AbilityBeam entity with a head, tail orb, and curving trail. + * The trail shows the path the head has traveled using laser-style rectangular segments. + * + * IMPORTANT: Trail points are stored RELATIVE to the origin position. + * We render by translating to the origin first, then drawing all points relative to that. + * + * Trail segments now share vertices at connection points to prevent gaps during curves. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityBeam extends RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityBeam beam = (EntityAbilityBeam) entity; + + setupRenderState(); + + // Handle charging state (windup phase) - render only growing orb + if (beam.isCharging()) { + renderChargingOrb(beam, x, y, z, partialTicks); + restoreRenderState(); + return; + } + + // Get trail points (these are RELATIVE to origin) + List trail = beam.getTrailPoints(); + if (trail.isEmpty()) { + restoreRenderState(); + return; + } + + // Get interpolated head offset + double headOffsetX = beam.getInterpolatedHeadOffsetX(partialTicks); + double headOffsetY = beam.getInterpolatedHeadOffsetY(partialTicks); + double headOffsetZ = beam.getInterpolatedHeadOffsetZ(partialTicks); + + // The render system passes x,y,z as the position to render the entity at + // Entity position = origin + headOffset (in world coords) + // So origin render position = x,y,z - headOffset + double renderOriginX = x - headOffsetX; + double renderOriginY = y - headOffsetY; + double renderOriginZ = z - headOffsetZ; + + // Get beam properties + float beamWidth = beam.getBeamWidth(); + float headSize = beam.getHeadSize(); + int innerColor = beam.getInnerColor(); + int outerColor = beam.getOuterColor(); + float outerAlpha = beam.getOuterColorAlpha(); + + // Render everything relative to origin position + GL11.glPushMatrix(); + GL11.glTranslated(renderOriginX, renderOriginY, renderOriginZ); + + // Calculate head distance from origin (for overlap check) + double headDistFromOrigin = Math.sqrt(headOffsetX * headOffsetX + headOffsetY * headOffsetY + headOffsetZ * headOffsetZ); + + // Render tail orb at origin (0,0,0) - only in anchored mode + // Don't render if head is too close to origin (would cause overlap/extra orb appearance) + if (beam.shouldRenderTailOrb() && headDistFromOrigin > headSize * 0.5) { + renderTailOrb(headSize * 0.8f, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth(), outerAlpha); + } + + // Render trail segments with smooth connections (with fading for non-anchored) + if (beam.hasFadingTrail()) { + renderFadingTrail(beam, trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + } else { + renderSmoothTrail(trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + } + + // Render head at interpolated offset position + renderHead(beam, headOffsetX, headOffsetY, headOffsetZ, headSize, innerColor, outerColor, outerAlpha, partialTicks); + + GL11.glPopMatrix(); + + restoreRenderState(); + } + + /** + * Render the charging orb during windup phase. + * Grows from 0 to headSize based on charge progress. + */ + private void renderChargingOrb(EntityAbilityBeam beam, double x, double y, double z, float partialTicks) { + float headSize = beam.getHeadSize(); + float chargeProgress = beam.getInterpolatedChargeProgress(partialTicks); + float size = headSize * chargeProgress; + + if (size <= 0.01f) return; + + int innerColor = beam.getInnerColor(); + int outerColor = beam.getOuterColor(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Pulsing effect + float pulseTime = beam.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.08f; + float scale = size * (1.0f + scaleModifier); + + // Render lightning during charging if enabled + if (beam.hasLightningEffect()) { + renderAttachedLightning(beam, scale); + } + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer glow only if enabled + if (beam.isOuterColorEnabled()) { + float outerScale = innerScale + beam.getOuterColorWidth(); + float outerAlpha = beam.getOuterColorAlpha(); + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderCube(outerColor, outerAlpha, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner core + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(innerColor, 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + } + + /** + * Render the tail orb at the origin (where the beam emanates from). + * This orb does not rotate - it's a stable anchor point. + */ + private void renderTailOrb(float size, int innerColor, int outerColor, boolean outerColorEnabled, float outerColorWidth, float outerAlpha) { + GL11.glPushMatrix(); + // Already at origin (0,0,0) + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer glow only if enabled + // outerColorWidth is an additive offset from inner size + if (outerColorEnabled) { + float outerScale = innerScale + outerColorWidth; + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(size * outerScale, size * outerScale, size * outerScale); + renderCube(outerColor, outerAlpha, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner core (solid) + GL11.glPushMatrix(); + GL11.glScalef(size * innerScale, size * innerScale, size * innerScale); + renderCube(innerColor, 1.0f, 0.5f); + GL11.glPopMatrix(); + + GL11.glPopMatrix(); + } + + /** + * Render the beam trail with smooth connections between segments. + * Uses averaged perpendicular vectors at connection points to prevent gaps. + */ + private void renderSmoothTrail(List trail, float width, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth) { + if (trail.size() < 2) return; + + int trailSize = trail.size(); + + // Pre-compute perpendicular frames at each point + // At connection points, average the perpendiculars for smooth transitions + List perpFrames = computePerpendiculars(trail); + + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + + // Render outer glow trail (wider, translucent) - only if enabled + // outerColorWidth is an additive offset from inner width + if (outerColorEnabled) { + float outerWidth = innerWidth + outerColorWidth * width; + GL11.glDepthMask(false); + renderTrailTube(trail, perpFrames, outerWidth, outerColor, 0.3f, true); + GL11.glDepthMask(true); + } + + // Render inner core trail + renderTrailTube(trail, perpFrames, innerWidth, innerColor, 0.9f, false); + } + + /** + * Render trail with fading effect (comet style) for non-anchored beams. + * Older trail points fade out based on their age. + */ + private void renderFadingTrail(EntityAbilityBeam beam, List trail, float width, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth) { + if (trail.size() < 2) return; + + List ages = beam.getTrailPointAges(); + int fadeTime = beam.getTrailFadeTime(); + + // Pre-compute perpendicular frames + List perpFrames = computePerpendiculars(trail); + + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + + // Render outer glow trail with fading + if (outerColorEnabled) { + float outerWidth = innerWidth + outerColorWidth * width; + GL11.glDepthMask(false); + renderFadingTrailTube(trail, perpFrames, ages, fadeTime, outerWidth, outerColor, 0.3f); + GL11.glDepthMask(true); + } + + // Render inner core trail with fading + renderFadingTrailTube(trail, perpFrames, ages, fadeTime, innerWidth, innerColor, 0.9f); + } + + /** + * Render trail tube with age-based fading. + */ + private void renderFadingTrailTube(List trail, List perpFrames, List ages, + int fadeTime, float width, int color, float baseAlpha) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + float halfWidth = width * 0.5f; + int size = trail.size(); + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + + for (int i = 0; i < size - 1; i++) { + Vec3 p1 = trail.get(i); + Vec3 p2 = trail.get(i + 1); + double[] frame1 = perpFrames.get(i); + double[] frame2 = perpFrames.get(i + 1); + + // Calculate alpha based on age (older = more faded) + float age1 = i < ages.size() ? ages.get(i) : 0; + float age2 = (i + 1) < ages.size() ? ages.get(i + 1) : 0; + float fade1 = 1.0f - Math.min(1.0f, age1 / fadeTime); + float fade2 = 1.0f - Math.min(1.0f, age2 / fadeTime); + float alpha1 = baseAlpha * fade1; + float alpha2 = baseAlpha * fade2; + + // Skip fully faded segments + if (alpha1 <= 0.01f && alpha2 <= 0.01f) continue; + + // Calculate 4 corners at start + double s1x = p1.xCoord - frame1[0] * halfWidth - frame1[3] * halfWidth; + double s1y = p1.yCoord - frame1[1] * halfWidth - frame1[4] * halfWidth; + double s1z = p1.zCoord - frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s2x = p1.xCoord + frame1[0] * halfWidth - frame1[3] * halfWidth; + double s2y = p1.yCoord + frame1[1] * halfWidth - frame1[4] * halfWidth; + double s2z = p1.zCoord + frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s3x = p1.xCoord + frame1[0] * halfWidth + frame1[3] * halfWidth; + double s3y = p1.yCoord + frame1[1] * halfWidth + frame1[4] * halfWidth; + double s3z = p1.zCoord + frame1[2] * halfWidth + frame1[5] * halfWidth; + + double s4x = p1.xCoord - frame1[0] * halfWidth + frame1[3] * halfWidth; + double s4y = p1.yCoord - frame1[1] * halfWidth + frame1[4] * halfWidth; + double s4z = p1.zCoord - frame1[2] * halfWidth + frame1[5] * halfWidth; + + // Calculate 4 corners at end + double e1x = p2.xCoord - frame2[0] * halfWidth - frame2[3] * halfWidth; + double e1y = p2.yCoord - frame2[1] * halfWidth - frame2[4] * halfWidth; + double e1z = p2.zCoord - frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e2x = p2.xCoord + frame2[0] * halfWidth - frame2[3] * halfWidth; + double e2y = p2.yCoord + frame2[1] * halfWidth - frame2[4] * halfWidth; + double e2z = p2.zCoord + frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e3x = p2.xCoord + frame2[0] * halfWidth + frame2[3] * halfWidth; + double e3y = p2.yCoord + frame2[1] * halfWidth + frame2[4] * halfWidth; + double e3z = p2.zCoord + frame2[2] * halfWidth + frame2[5] * halfWidth; + + double e4x = p2.xCoord - frame2[0] * halfWidth + frame2[3] * halfWidth; + double e4y = p2.yCoord - frame2[1] * halfWidth + frame2[4] * halfWidth; + double e4z = p2.zCoord - frame2[2] * halfWidth + frame2[5] * halfWidth; + + // Bottom face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s3x, s3y, s3z); + + // Left face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + + // Right face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + } + + tess.draw(); + } + + /** + * Compute perpendicular frames for each point in the trail. + * At interior points, average the perpendiculars from both directions for smooth curves. + * Returns list of [horzX, horzY, horzZ, vertX, vertY, vertZ] for each point. + */ + private List computePerpendiculars(List trail) { + List frames = new ArrayList<>(); + int size = trail.size(); + + for (int i = 0; i < size; i++) { + double[] frame = new double[6]; + + // Calculate direction at this point + double dx, dy, dz; + if (i == 0) { + // First point - use direction to next + Vec3 p1 = trail.get(0); + Vec3 p2 = trail.get(1); + dx = p2.xCoord - p1.xCoord; + dy = p2.yCoord - p1.yCoord; + dz = p2.zCoord - p1.zCoord; + } else if (i == size - 1) { + // Last point - use direction from previous + Vec3 p1 = trail.get(size - 2); + Vec3 p2 = trail.get(size - 1); + dx = p2.xCoord - p1.xCoord; + dy = p2.yCoord - p1.yCoord; + dz = p2.zCoord - p1.zCoord; + } else { + // Interior point - average directions + Vec3 p0 = trail.get(i - 1); + Vec3 p1 = trail.get(i); + Vec3 p2 = trail.get(i + 1); + double dx1 = p1.xCoord - p0.xCoord; + double dy1 = p1.yCoord - p0.yCoord; + double dz1 = p1.zCoord - p0.zCoord; + double dx2 = p2.xCoord - p1.xCoord; + double dy2 = p2.yCoord - p1.yCoord; + double dz2 = p2.zCoord - p1.zCoord; + // Average the directions + dx = (dx1 + dx2) * 0.5; + dy = (dy1 + dy2) * 0.5; + dz = (dz1 + dz2) * 0.5; + } + + // Normalize direction + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0.001) { + dx /= len; + dy /= len; + dz /= len; + } else { + dx = 0; dy = 0; dz = 1; + } + + // Calculate perpendicular vectors using consistent up reference + double upX = 0, upY = 1, upZ = 0; + if (Math.abs(dy) > 0.9) { + upX = 1; upY = 0; upZ = 0; + } + + // Cross product for horizontal perpendicular + double horzX = dy * upZ - dz * upY; + double horzY = dz * upX - dx * upZ; + double horzZ = dx * upY - dy * upX; + double horzLen = Math.sqrt(horzX * horzX + horzY * horzY + horzZ * horzZ); + if (horzLen > 0) { + horzX /= horzLen; + horzY /= horzLen; + horzZ /= horzLen; + } + + // Cross product for vertical perpendicular + double vertX = dy * horzZ - dz * horzY; + double vertY = dz * horzX - dx * horzZ; + double vertZ = dx * horzY - dy * horzX; + + frame[0] = horzX; frame[1] = horzY; frame[2] = horzZ; + frame[3] = vertX; frame[4] = vertY; frame[5] = vertZ; + frames.add(frame); + } + + return frames; + } + + /** + * Render the trail as a continuous tube using pre-computed perpendicular frames. + */ + private void renderTrailTube(List trail, List perpFrames, + float width, int color, float baseAlpha, boolean fadeFromStart) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + float halfWidth = width * 0.5f; + int size = trail.size(); + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + + for (int i = 0; i < size - 1; i++) { + Vec3 p1 = trail.get(i); + Vec3 p2 = trail.get(i + 1); + double[] frame1 = perpFrames.get(i); + double[] frame2 = perpFrames.get(i + 1); + + // Calculate alpha based on position (fade from start or constant) + float alpha1, alpha2; + if (fadeFromStart) { + float progress1 = (float) i / size; + float progress2 = (float) (i + 1) / size; + alpha1 = baseAlpha * progress1; + alpha2 = baseAlpha * progress2; + } else { + alpha1 = baseAlpha; + alpha2 = baseAlpha; + } + + // Calculate 4 corners at start + double s1x = p1.xCoord - frame1[0] * halfWidth - frame1[3] * halfWidth; + double s1y = p1.yCoord - frame1[1] * halfWidth - frame1[4] * halfWidth; + double s1z = p1.zCoord - frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s2x = p1.xCoord + frame1[0] * halfWidth - frame1[3] * halfWidth; + double s2y = p1.yCoord + frame1[1] * halfWidth - frame1[4] * halfWidth; + double s2z = p1.zCoord + frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s3x = p1.xCoord + frame1[0] * halfWidth + frame1[3] * halfWidth; + double s3y = p1.yCoord + frame1[1] * halfWidth + frame1[4] * halfWidth; + double s3z = p1.zCoord + frame1[2] * halfWidth + frame1[5] * halfWidth; + + double s4x = p1.xCoord - frame1[0] * halfWidth + frame1[3] * halfWidth; + double s4y = p1.yCoord - frame1[1] * halfWidth + frame1[4] * halfWidth; + double s4z = p1.zCoord - frame1[2] * halfWidth + frame1[5] * halfWidth; + + // Calculate 4 corners at end + double e1x = p2.xCoord - frame2[0] * halfWidth - frame2[3] * halfWidth; + double e1y = p2.yCoord - frame2[1] * halfWidth - frame2[4] * halfWidth; + double e1z = p2.zCoord - frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e2x = p2.xCoord + frame2[0] * halfWidth - frame2[3] * halfWidth; + double e2y = p2.yCoord + frame2[1] * halfWidth - frame2[4] * halfWidth; + double e2z = p2.zCoord + frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e3x = p2.xCoord + frame2[0] * halfWidth + frame2[3] * halfWidth; + double e3y = p2.yCoord + frame2[1] * halfWidth + frame2[4] * halfWidth; + double e3z = p2.zCoord + frame2[2] * halfWidth + frame2[5] * halfWidth; + + double e4x = p2.xCoord - frame2[0] * halfWidth + frame2[3] * halfWidth; + double e4y = p2.yCoord - frame2[1] * halfWidth + frame2[4] * halfWidth; + double e4z = p2.zCoord - frame2[2] * halfWidth + frame2[5] * halfWidth; + + // Bottom face (s1-s2-e2-e1) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face (s4-e4-e3-s3) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s3x, s3y, s3z); + + // Left face (s1-e1-e4-s4) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + + // Right face (s2-s3-e3-e2) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + } + + tess.draw(); + } + + /** + * Render the beam head as a glowing cube. + * Coordinates are relative to origin. + */ + private void renderHead(EntityAbilityBeam beam, double x, double y, double z, + float size, int innerColor, int outerColor, float outerAlpha, float partialTicks) { + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Pulsing effect + float pulseTime = beam.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.1f; + float scale = size * (1.0f + scaleModifier); + + // Render lightning BEFORE rotation so it crackles in all directions (only on head) + if (beam.hasLightningEffect()) { + renderAttachedLightning(beam, scale); + } + + // Rotation + GL11.glRotatef(beam.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); + GL11.glRotatef(beam.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + GL11.glRotatef(beam.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer glow only if enabled + // outerColorWidth is an additive offset from inner size + if (beam.isOuterColorEnabled()) { + float outerScale = innerScale + beam.getOuterColorWidth(); + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderCube(outerColor, outerAlpha, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner core + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(innerColor, 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + } + + /** + * Render fading lightning arcs attached to the beam head (in local space). + */ + private void renderAttachedLightning(EntityAbilityBeam beam, float headScale) { + AttachedLightningRenderer.LightningState state = getLightningState(beam); + + float density = beam.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * headScale * 0.5f; + float radius = innerRadius + beam.getLightningRadius() * headScale; + int outerColor = beam.getOuterColor(); + int innerColor = beam.getInnerColor(); + int fadeTime = beam.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityBeam beam) { + if (beam.lightningState == null) { + beam.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) beam.lightningState; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java new file mode 100644 index 000000000..20486d70e --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -0,0 +1,178 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; +import kamkeel.npcs.entity.EntityAbilityDisc; +import net.minecraft.entity.Entity; +import org.lwjgl.opengl.GL11; + +/** + * Renders the AbilityDisc entity as a flat spinning square. + * Rotates only on Y axis (flat spin like a saw blade). + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityDisc extends RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityDisc disc = (EntityAbilityDisc) entity; + + setupRenderState(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Get interpolated values + float size = disc.getInterpolatedSize(partialTicks); + float radius = disc.getDiscRadius() * size; + float thickness = disc.getDiscThickness() * size; + + // Subtle pulsing effect + float pulseTime = entity.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.1f) * 0.03f; + float scale = 1.0f + scaleModifier; + + // Render lightning BEFORE rotation so it crackles in all directions + if (disc.hasLightningEffect()) { + renderAttachedLightning(disc, radius * scale); + } + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Apply ONLY Y-axis rotation for flat spin + GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer square (translucent) - only if enabled + // outerColorWidth is an additive offset from inner size + if (disc.isOuterColorEnabled()) { + float outerScale = innerScale + disc.getOuterColorWidth(); + GL11.glDepthMask(false); + renderFlatSquare(disc.getOuterColor(), disc.getOuterColorAlpha(), radius * outerScale, thickness); + GL11.glDepthMask(true); + } + + // Render inner square (solid) + renderFlatSquare(disc.getInnerColor(), 1.0f, radius * innerScale, thickness * 1.2f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + + restoreRenderState(); + } + + /** + * Render fading lightning arcs attached to the disc (in local space). + */ + private void renderAttachedLightning(EntityAbilityDisc disc, float discSize) { + AttachedLightningRenderer.LightningState state = getLightningState(disc); + + float density = disc.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * discSize * 0.5f; + float radius = innerRadius + disc.getLightningRadius() * discSize; + int outerColor = disc.getOuterColor(); + int innerColor = disc.getInnerColor(); + int fadeTime = disc.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityDisc disc) { + if (disc.lightningState == null) { + disc.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) disc.lightningState; + } + + /** + * Render a flat square (thin cube) centered at origin. + * The square lies flat on the XZ plane with thin Y height. + */ + private void renderFlatSquare(int color, float alpha, float halfSize, float thickness) { + float halfThick = thickness * 0.5f; + + // Render as a thin cube + renderCubeCustom(color, alpha, halfSize, halfThick, halfSize); + } + + /** + * Render a cube with custom dimensions. + */ + private void renderCubeCustom(int color, float alpha, float halfX, float halfY, float halfZ) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + net.minecraft.client.renderer.Tessellator tess = net.minecraft.client.renderer.Tessellator.instance; + + // Top face (y+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 1, 0); + tess.addVertex(-halfX, halfY, halfZ); + tess.addVertex(halfX, halfY, halfZ); + tess.addVertex(halfX, halfY, -halfZ); + tess.addVertex(-halfX, halfY, -halfZ); + tess.draw(); + + // Bottom face (y-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, -1, 0); + tess.addVertex(-halfX, -halfY, -halfZ); + tess.addVertex(halfX, -halfY, -halfZ); + tess.addVertex(halfX, -halfY, halfZ); + tess.addVertex(-halfX, -halfY, halfZ); + tess.draw(); + + // Front face (z+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, 1); + tess.addVertex(-halfX, -halfY, halfZ); + tess.addVertex(halfX, -halfY, halfZ); + tess.addVertex(halfX, halfY, halfZ); + tess.addVertex(-halfX, halfY, halfZ); + tess.draw(); + + // Back face (z-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, -1); + tess.addVertex(halfX, -halfY, -halfZ); + tess.addVertex(-halfX, -halfY, -halfZ); + tess.addVertex(-halfX, halfY, -halfZ); + tess.addVertex(halfX, halfY, -halfZ); + tess.draw(); + + // Right face (x+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(1, 0, 0); + tess.addVertex(halfX, -halfY, halfZ); + tess.addVertex(halfX, -halfY, -halfZ); + tess.addVertex(halfX, halfY, -halfZ); + tess.addVertex(halfX, halfY, halfZ); + tess.draw(); + + // Left face (x-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(-1, 0, 0); + tess.addVertex(-halfX, -halfY, -halfZ); + tess.addVertex(-halfX, -halfY, halfZ); + tess.addVertex(-halfX, halfY, halfZ); + tess.addVertex(-halfX, halfY, -halfZ); + tess.draw(); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java new file mode 100644 index 000000000..e70e5c6ba --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java @@ -0,0 +1,344 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilityLaser; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.entity.Entity; +import org.lwjgl.opengl.GL11; + +/** + * Renders the AbilityLaser entity as a rectangular beam with clear inner/outer colors. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityLaser extends RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityLaser laser = (EntityAbilityLaser) entity; + + setupRenderState(); + + if (laser.isCharging()) { + renderChargingOrb(laser, x, y, z, partialTicks); + restoreRenderState(); + return; + } + + // Get laser parameters + double startX = laser.getStartX(); + double startY = laser.getStartY(); + double startZ = laser.getStartZ(); + double endX = laser.getEndX(); + double endY = laser.getEndY(); + double endZ = laser.getEndZ(); + float width = laser.getLaserWidth(); + float alpha = laser.getLingerAlpha(); + + // Calculate render offset (entity position is used as reference) + double offsetX = x - laser.posX; + double offsetY = y - laser.posY; + double offsetZ = z - laser.posZ; + + // Render start relative to camera + double renderStartX = startX + offsetX; + double renderStartY = startY + offsetY; + double renderStartZ = startZ + offsetZ; + double renderEndX = endX + offsetX; + double renderEndY = endY + offsetY; + double renderEndZ = endZ + offsetZ; + + // Calculate perpendicular vectors for the beam rectangle + double dx = renderEndX - renderStartX; + double dy = renderEndY - renderStartY; + double dz = renderEndZ - renderStartZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len < 0.001) { + restoreRenderState(); + return; + } + + // Normalize direction + dx /= len; + dy /= len; + dz /= len; + + // Calculate perpendicular vectors (one horizontal, one vertical relative to beam) + double upX = 0, upY = 1, upZ = 0; + if (Math.abs(dy) > 0.9) { + upX = 1; upY = 0; upZ = 0; + } + + // Cross product for horizontal perpendicular + double horzX = dy * upZ - dz * upY; + double horzY = dz * upX - dx * upZ; + double horzZ = dx * upY - dy * upX; + double horzLen = Math.sqrt(horzX * horzX + horzY * horzY + horzZ * horzZ); + if (horzLen > 0) { + horzX /= horzLen; + horzY /= horzLen; + horzZ /= horzLen; + } + + // Cross product for vertical perpendicular + double vertX = dy * horzZ - dz * horzY; + double vertY = dz * horzX - dx * horzZ; + double vertZ = dx * horzY - dy * horzX; + + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + + // Render outer rectangle (larger, outer color, translucent) - only if enabled + // outerColorWidth is an additive offset from inner width + if (laser.isOuterColorEnabled()) { + float outerWidth = innerWidth + laser.getOuterColorWidth() * width; + GL11.glDepthMask(false); + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + outerWidth, laser.getOuterColor(), alpha * 0.4f); + + // Render middle layer (halfway between inner and outer) + float midWidth = innerWidth + (outerWidth - innerWidth) * 0.5f; + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + midWidth, laser.getOuterColor(), alpha * 0.7f); + GL11.glDepthMask(true); + } + + // Render inner core (solid) + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + innerWidth, laser.getInnerColor(), alpha); + + // Render lightning along the laser beam + if (laser.hasLightningEffect() && laser.getCurrentLength() > 0.1f) { + renderLaserLightning(laser, renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, width); + } + + restoreRenderState(); + } + + private void renderChargingOrb(EntityAbilityLaser laser, double x, double y, double z, float partialTicks) { + float headSize = laser.getLaserWidth() * 1.5f; + float chargeProgress = laser.getInterpolatedChargeProgress(partialTicks); + float size = headSize * chargeProgress; + + if (size <= 0.01f) return; + + int innerColor = laser.getInnerColor(); + int outerColor = laser.getOuterColor(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Pulsing effect + float pulseTime = laser.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.08f; + float scale = size * (1.0f + scaleModifier); + + // Render lightning during charging if enabled + if (laser.hasLightningEffect()) { + renderAttachedLightning(laser, scale); + } + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer glow only if enabled + if (laser.isOuterColorEnabled()) { + float outerScale = innerScale + laser.getOuterColorWidth(); + float outerAlpha = laser.getOuterColorAlpha(); + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderCube(outerColor, outerAlpha, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner core + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(innerColor, 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + } + + /** + * Render lightning effects along the laser beam at multiple points. + */ + private void renderLaserLightning(EntityAbilityLaser laser, + double startX, double startY, double startZ, + double endX, double endY, double endZ, float width) { + AttachedLightningRenderer.LightningState state = getLightningState(laser); + + float density = laser.getLightningDensity(); + // Lightning radius extends outward from inner width + float innerRadius = width * 0.6f * 0.5f; + float radius = innerRadius + laser.getLightningRadius() * width; + int outerColor = laser.getOuterColor(); + int innerColor = laser.getInnerColor(); + int fadeTime = laser.getLightningFadeTime(); + + // Render lightning at multiple points along the laser + // Start, middle, and end points + double midX = (startX + endX) * 0.5; + double midY = (startY + endY) * 0.5; + double midZ = (startZ + endZ) * 0.5; + + GL11.glPushMatrix(); + GL11.glTranslated(midX, midY, midZ); + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + GL11.glPopMatrix(); + + // Also render at the tip + GL11.glPushMatrix(); + GL11.glTranslated(endX, endY, endZ); + // Use a secondary state for the tip + AttachedLightningRenderer.LightningState tipState = getTipLightningState(laser); + tipState.update(density * 0.7f, radius * 0.8f, outerColor, innerColor, fadeTime); + tipState.render(); + GL11.glPopMatrix(); + } + + + private void renderAttachedLightning(EntityAbilityLaser laser, float headScale) { + AttachedLightningRenderer.LightningState state = getLightningState(laser); + + float density = laser.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * headScale * 0.5f; + float radius = innerRadius + laser.getLightningRadius() * headScale; + int outerColor = laser.getOuterColor(); + int innerColor = laser.getInnerColor(); + int fadeTime = laser.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + /** + * Get or create the main lightning state for the laser. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityLaser laser) { + if (laser.lightningState == null) { + laser.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) laser.lightningState; + } + + /** + * Get or create the tip lightning state for the laser. + * Stored as a secondary object in lightningState2. + */ + private AttachedLightningRenderer.LightningState getTipLightningState(EntityAbilityLaser laser) { + // Use a simple approach - store in a transient field + // Since lightningState is already used, we'll create a new state each time + // This works because we're updating and rendering immediately + return new AttachedLightningRenderer.LightningState(); + } + + /** + * Render a beam as a 3D rectangular prism (box along a line). + */ + private void renderBeamRectangle(double x1, double y1, double z1, + double x2, double y2, double z2, + double horzX, double horzY, double horzZ, + double vertX, double vertY, double vertZ, + float width, int color, float alpha) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + float halfWidth = width * 0.5f; + + // Calculate 8 corners of the box + // Start corners + double s1x = x1 - horzX * halfWidth - vertX * halfWidth; + double s1y = y1 - horzY * halfWidth - vertY * halfWidth; + double s1z = z1 - horzZ * halfWidth - vertZ * halfWidth; + + double s2x = x1 + horzX * halfWidth - vertX * halfWidth; + double s2y = y1 + horzY * halfWidth - vertY * halfWidth; + double s2z = z1 + horzZ * halfWidth - vertZ * halfWidth; + + double s3x = x1 + horzX * halfWidth + vertX * halfWidth; + double s3y = y1 + horzY * halfWidth + vertY * halfWidth; + double s3z = z1 + horzZ * halfWidth + vertZ * halfWidth; + + double s4x = x1 - horzX * halfWidth + vertX * halfWidth; + double s4y = y1 - horzY * halfWidth + vertY * halfWidth; + double s4z = z1 - horzZ * halfWidth + vertZ * halfWidth; + + // End corners + double e1x = x2 - horzX * halfWidth - vertX * halfWidth; + double e1y = y2 - horzY * halfWidth - vertY * halfWidth; + double e1z = z2 - horzZ * halfWidth - vertZ * halfWidth; + + double e2x = x2 + horzX * halfWidth - vertX * halfWidth; + double e2y = y2 + horzY * halfWidth - vertY * halfWidth; + double e2z = z2 + horzZ * halfWidth - vertZ * halfWidth; + + double e3x = x2 + horzX * halfWidth + vertX * halfWidth; + double e3y = y2 + horzY * halfWidth + vertY * halfWidth; + double e3z = z2 + horzZ * halfWidth + vertZ * halfWidth; + + double e4x = x2 - horzX * halfWidth + vertX * halfWidth; + double e4y = y2 - horzY * halfWidth + vertY * halfWidth; + double e4z = z2 - horzZ * halfWidth + vertZ * halfWidth; + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + + // Bottom face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face + tess.addVertex(s4x, s4y, s4z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(s3x, s3y, s3z); + + // Left face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(s4x, s4y, s4z); + + // Right face + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + + // Start cap + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s4x, s4y, s4z); + tess.addVertex(s3x, s3y, s3z); + tess.addVertex(s2x, s2y, s2z); + + // End cap + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e4x, e4y, e4z); + + tess.draw(); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java new file mode 100644 index 000000000..048b3642f --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java @@ -0,0 +1,106 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; +import kamkeel.npcs.entity.EntityAbilityOrb; +import net.minecraft.entity.Entity; +import org.lwjgl.opengl.GL11; + +/** + * Renders the AbilityOrb entity as a cube with inner and outer layers. + * Minecraft-style blocky appearance using Tessellator. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityOrb extends RenderAbilityProjectile { + + private static final int LIGHTNING_FADE_TICKS = 6; + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityOrb orb = (EntityAbilityOrb) entity; + + setupRenderState(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Get interpolated size for smooth rendering + float size = orb.getInterpolatedSize(partialTicks); + + // Pulsing scale effect + float pulseTime = entity.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.15f) * 0.05f; + float scale = size * (1.0f + scaleModifier); + + // Render lightning BEFORE rotation so it crackles in all directions + if (orb.hasLightningEffect()) { + renderAttachedLightning(orb, scale); + } + + // Use entity's interpolated rotation for smooth spinning + GL11.glRotatef(orb.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); + GL11.glRotatef(orb.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + GL11.glRotatef(orb.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer cube (translucent) - only if enabled + // outerColorWidth is an additive offset from inner size + if (orb.isOuterColorEnabled()) { + float outerScale = innerScale + orb.getOuterColorWidth(); + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderCube(orb.getOuterColor(), orb.getOuterColorAlpha(), 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner cube (solid) + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(orb.getInnerColor(), 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + + restoreRenderState(); + } + + /** + * Render fading lightning arcs attached to the orb (in local space). + * Arcs persist for a few ticks and fade out, while staying attached to the orb. + */ + private void renderAttachedLightning(EntityAbilityOrb orb, float orbScale) { + // Get or create the lightning state for this entity + AttachedLightningRenderer.LightningState state = getLightningState(orb); + + float density = orb.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * orbScale * 0.5f; // Half size of inner cube + float radius = innerRadius + orb.getLightningRadius() * orbScale; + int outerColor = orb.getOuterColor(); + int innerColor = orb.getInnerColor(); + int fadeTime = orb.getLightningFadeTime(); + + // Update: age existing arcs, spawn new ones based on density + state.update(density, radius, outerColor, innerColor, fadeTime); + + // Render all active arcs in local space (they move with the orb) + state.render(); + } + + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityOrb orb) { + if (orb.lightningState == null) { + orb.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) orb.lightningState; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java new file mode 100644 index 000000000..2ba840aec --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java @@ -0,0 +1,259 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +/** + * Base renderer for ability projectiles. + * Provides common GL state setup and rendering utilities. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public abstract class RenderAbilityProjectile extends Render { + + protected static final ResourceLocation WHITE_TEXTURE = new ResourceLocation("customnpcs", "textures/entity/white.png"); + + public RenderAbilityProjectile() { + this.shadowSize = 0.0f; + } + + /** + * Setup GL state for translucent, self-illuminated rendering. + * Forces full brightness so world lighting does not affect projectiles. + */ + protected void setupRenderState() { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glAlphaFunc(GL11.GL_GREATER, 0.003921569F); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_TEXTURE_2D); + + // Force full brightness - prevents world lighting from darkening projectiles + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240.0f, 240.0f); + } + + /** + * Restore GL state after rendering. + */ + protected void restoreRenderState() { + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + + /** + * Extract RGB components from color int. + */ + protected float[] extractRGB(int color) { + float r = ((color >> 16) & 0xFF) / 255.0f; + float g = ((color >> 8) & 0xFF) / 255.0f; + float b = (color & 0xFF) / 255.0f; + return new float[]{r, g, b}; + } + + /** + * Render a cube centered at origin with given color and alpha. + */ + protected void renderCube(int color, float alpha, float halfSize) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + Tessellator tess = Tessellator.instance; + + // Front face (z+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, 1); + tess.addVertex(-halfSize, -halfSize, halfSize); + tess.addVertex(halfSize, -halfSize, halfSize); + tess.addVertex(halfSize, halfSize, halfSize); + tess.addVertex(-halfSize, halfSize, halfSize); + tess.draw(); + + // Back face (z-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, -1); + tess.addVertex(halfSize, -halfSize, -halfSize); + tess.addVertex(-halfSize, -halfSize, -halfSize); + tess.addVertex(-halfSize, halfSize, -halfSize); + tess.addVertex(halfSize, halfSize, -halfSize); + tess.draw(); + + // Top face (y+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 1, 0); + tess.addVertex(-halfSize, halfSize, halfSize); + tess.addVertex(halfSize, halfSize, halfSize); + tess.addVertex(halfSize, halfSize, -halfSize); + tess.addVertex(-halfSize, halfSize, -halfSize); + tess.draw(); + + // Bottom face (y-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, -1, 0); + tess.addVertex(-halfSize, -halfSize, -halfSize); + tess.addVertex(halfSize, -halfSize, -halfSize); + tess.addVertex(halfSize, -halfSize, halfSize); + tess.addVertex(-halfSize, -halfSize, halfSize); + tess.draw(); + + // Right face (x+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(1, 0, 0); + tess.addVertex(halfSize, -halfSize, halfSize); + tess.addVertex(halfSize, -halfSize, -halfSize); + tess.addVertex(halfSize, halfSize, -halfSize); + tess.addVertex(halfSize, halfSize, halfSize); + tess.draw(); + + // Left face (x-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(-1, 0, 0); + tess.addVertex(-halfSize, -halfSize, -halfSize); + tess.addVertex(-halfSize, -halfSize, halfSize); + tess.addVertex(-halfSize, halfSize, halfSize); + tess.addVertex(-halfSize, halfSize, -halfSize); + tess.draw(); + } + + /** + * Render a flat disc (cylinder with very small height) centered at origin. + * The disc lies flat on the XZ plane. + */ + protected void renderDisc(int color, float alpha, float radius, float thickness, int segments) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + Tessellator tess = Tessellator.instance; + float halfThick = thickness * 0.5f; + + // Top face + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 1, 0); + for (int i = 0; i < segments; i++) { + float angle1 = (float) (2 * Math.PI * i / segments); + float angle2 = (float) (2 * Math.PI * (i + 1) / segments); + float x1 = (float) Math.cos(angle1) * radius; + float z1 = (float) Math.sin(angle1) * radius; + float x2 = (float) Math.cos(angle2) * radius; + float z2 = (float) Math.sin(angle2) * radius; + + tess.addVertex(0, halfThick, 0); + tess.addVertex(x1, halfThick, z1); + tess.addVertex(x2, halfThick, z2); + tess.addVertex(0, halfThick, 0); + } + tess.draw(); + + // Bottom face + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, -1, 0); + for (int i = 0; i < segments; i++) { + float angle1 = (float) (2 * Math.PI * i / segments); + float angle2 = (float) (2 * Math.PI * (i + 1) / segments); + float x1 = (float) Math.cos(angle1) * radius; + float z1 = (float) Math.sin(angle1) * radius; + float x2 = (float) Math.cos(angle2) * radius; + float z2 = (float) Math.sin(angle2) * radius; + + tess.addVertex(0, -halfThick, 0); + tess.addVertex(x2, -halfThick, z2); + tess.addVertex(x1, -halfThick, z1); + tess.addVertex(0, -halfThick, 0); + } + tess.draw(); + + // Edge + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + for (int i = 0; i < segments; i++) { + float angle1 = (float) (2 * Math.PI * i / segments); + float angle2 = (float) (2 * Math.PI * (i + 1) / segments); + float x1 = (float) Math.cos(angle1) * radius; + float z1 = (float) Math.sin(angle1) * radius; + float x2 = (float) Math.cos(angle2) * radius; + float z2 = (float) Math.sin(angle2) * radius; + + // Normal pointing outward + float nx = (float) Math.cos((angle1 + angle2) * 0.5); + float nz = (float) Math.sin((angle1 + angle2) * 0.5); + tess.setNormal(nx, 0, nz); + + tess.addVertex(x1, -halfThick, z1); + tess.addVertex(x2, -halfThick, z2); + tess.addVertex(x2, halfThick, z2); + tess.addVertex(x1, halfThick, z1); + } + tess.draw(); + } + + /** + * Render a line segment as a quad billboard facing the camera. + */ + protected void renderLineSegment(double x1, double y1, double z1, + double x2, double y2, double z2, + float width, int color, float alpha) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + // Calculate direction + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len < 0.001) return; + + // Perpendicular vectors for billboard + double perpX, perpY, perpZ; + if (Math.abs(dy) < 0.9) { + // Cross with up vector + perpX = -dz; + perpY = 0; + perpZ = dx; + } else { + // Cross with right vector + perpX = 0; + perpY = dz; + perpZ = -dy; + } + double perpLen = Math.sqrt(perpX * perpX + perpY * perpY + perpZ * perpZ); + if (perpLen > 0) { + perpX = perpX / perpLen * width * 0.5; + perpY = perpY / perpLen * width * 0.5; + perpZ = perpZ / perpLen * width * 0.5; + } + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.addVertex(x1 - perpX, y1 - perpY, z1 - perpZ); + tess.addVertex(x2 - perpX, y2 - perpY, z2 - perpZ); + tess.addVertex(x2 + perpX, y2 + perpY, z2 + perpZ); + tess.addVertex(x1 + perpX, y1 + perpY, z1 + perpZ); + tess.draw(); + } + + @Override + protected ResourceLocation getEntityTexture(Entity entity) { + return WHITE_TEXTURE; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java new file mode 100644 index 000000000..10705d762 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java @@ -0,0 +1,219 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.entity.EntityAbilitySweeper; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL11; + +/** + * Renders the Sweeper beam as a thin rotating laser attached to the NPC. + * Uses similar rendering style to the Beam trail for visual consistency. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilitySweeper extends Render { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilitySweeper sweeper = (EntityAbilitySweeper) entity; + + setupRenderState(); + + float beamLength = sweeper.getBeamLength(); + float beamWidth = sweeper.getBeamWidth(); + int innerColor = sweeper.getInnerColor(); + int outerColor = sweeper.getOuterColor(); + boolean outerColorEnabled = sweeper.isOuterColorEnabled(); + float outerColorWidth = sweeper.getOuterColorWidth(); + float angle = sweeper.getInterpolatedAngle(partialTicks); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Rotate the beam around Y axis (angle already includes baseYaw) + GL11.glRotatef(-angle, 0, 1, 0); // Negative for correct direction + + // Render the beam extending in the +Z direction (like beam trail segments) + renderBeam(beamLength, beamWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth); + + GL11.glPopMatrix(); + + restoreRenderState(); + } + + private void setupRenderState() { + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glShadeModel(GL11.GL_SMOOTH); + + // Force full brightness - prevents world lighting from darkening sweeper + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240.0f, 240.0f); + } + + private void restoreRenderState() { + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glPopAttrib(); + } + + /** + * Render beam using the same style as the Beam entity trail. + * Three layers: outer glow, middle, inner core. + */ + private void renderBeam(float length, float width, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth) { + // Extract colors + float outerR = ((outerColor >> 16) & 0xFF) / 255.0f; + float outerG = ((outerColor >> 8) & 0xFF) / 255.0f; + float outerB = (outerColor & 0xFF) / 255.0f; + + float innerR = ((innerColor >> 16) & 0xFF) / 255.0f; + float innerG = ((innerColor >> 8) & 0xFF) / 255.0f; + float innerB = (innerColor & 0xFF) / 255.0f; + + // Render from origin (0,0,0) to (0,0,length) in local coords + // After rotation this extends in the correct world direction + + // Outer glow (wider, translucent) - like beam trail - only if enabled + if (outerColorEnabled) { + GL11.glDepthMask(false); + renderBeamSegment(0, 0, 0, 0, 0, length, width * outerColorWidth, outerR, outerG, outerB, 0.3f); + GL11.glDepthMask(true); + + // Middle layer + renderBeamSegment(0, 0, 0, 0, 0, length, width * 1.3f, outerR, outerG, outerB, 0.6f); + } + + // Inner core (solid) - like beam trail inner + renderBeamSegment(0, 0, 0, 0, 0, length, width * 0.6f, innerR, innerG, innerB, 1.0f); + } + + /** + * Render a beam segment as a 3D rectangular prism. + * Uses the same approach as RenderAbilityBeam.renderBeamRectangle. + */ + private void renderBeamSegment(double x1, double y1, double z1, + double x2, double y2, double z2, + float width, float r, float g, float b, float a) { + // Calculate direction + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len < 0.001) return; + + // Normalize direction + dx /= len; + dy /= len; + dz /= len; + + // Calculate perpendicular vectors + double upX = 0, upY = 1, upZ = 0; + if (Math.abs(dy) > 0.9) { + upX = 1; upY = 0; upZ = 0; + } + + // Cross product for horizontal perpendicular + double horzX = dy * upZ - dz * upY; + double horzY = dz * upX - dx * upZ; + double horzZ = dx * upY - dy * upX; + double horzLen = Math.sqrt(horzX * horzX + horzY * horzY + horzZ * horzZ); + if (horzLen > 0) { + horzX /= horzLen; + horzY /= horzLen; + horzZ /= horzLen; + } + + // Cross product for vertical perpendicular + double vertX = dy * horzZ - dz * horzY; + double vertY = dz * horzX - dx * horzZ; + double vertZ = dx * horzY - dy * horzX; + + float halfWidth = width * 0.5f; + + // Calculate 8 corners of the box + double s1x = x1 - horzX * halfWidth - vertX * halfWidth; + double s1y = y1 - horzY * halfWidth - vertY * halfWidth; + double s1z = z1 - horzZ * halfWidth - vertZ * halfWidth; + + double s2x = x1 + horzX * halfWidth - vertX * halfWidth; + double s2y = y1 + horzY * halfWidth - vertY * halfWidth; + double s2z = z1 + horzZ * halfWidth - vertZ * halfWidth; + + double s3x = x1 + horzX * halfWidth + vertX * halfWidth; + double s3y = y1 + horzY * halfWidth + vertY * halfWidth; + double s3z = z1 + horzZ * halfWidth + vertZ * halfWidth; + + double s4x = x1 - horzX * halfWidth + vertX * halfWidth; + double s4y = y1 - horzY * halfWidth + vertY * halfWidth; + double s4z = z1 - horzZ * halfWidth + vertZ * halfWidth; + + double e1x = x2 - horzX * halfWidth - vertX * halfWidth; + double e1y = y2 - horzY * halfWidth - vertY * halfWidth; + double e1z = z2 - horzZ * halfWidth - vertZ * halfWidth; + + double e2x = x2 + horzX * halfWidth - vertX * halfWidth; + double e2y = y2 + horzY * halfWidth - vertY * halfWidth; + double e2z = z2 + horzZ * halfWidth - vertZ * halfWidth; + + double e3x = x2 + horzX * halfWidth + vertX * halfWidth; + double e3y = y2 + horzY * halfWidth + vertY * halfWidth; + double e3z = z2 + horzZ * halfWidth + vertZ * halfWidth; + + double e4x = x2 - horzX * halfWidth + vertX * halfWidth; + double e4y = y2 - horzY * halfWidth + vertY * halfWidth; + double e4z = z2 - horzZ * halfWidth + vertZ * halfWidth; + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, a); + + // Bottom face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face + tess.addVertex(s4x, s4y, s4z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(s3x, s3y, s3z); + + // Left face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(s4x, s4y, s4z); + + // Right face + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + + // End cap (tip of beam) + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e4x, e4y, e4z); + + tess.draw(); + } + + @Override + protected ResourceLocation getEntityTexture(Entity entity) { + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java index 37e1232c4..3b5e49e2d 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java @@ -1,10 +1,10 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.Tessellator; import net.minecraft.entity.player.EntityPlayer; @@ -76,10 +76,10 @@ public class TelegraphRenderer { return; } - // Calculate render position relative to camera - double renderX = instance.getX() - playerX; - double renderY = instance.getY() - playerY + telegraph.getHeightOffset(); - double renderZ = instance.getZ() - playerZ; + // Calculate render position relative to camera using interpolated positions for smooth movement + double renderX = instance.getInterpolatedX(partialTicks) - playerX; + double renderY = instance.getInterpolatedY(partialTicks) - playerY + telegraph.getHeightOffset(); + double renderZ = instance.getInterpolatedZ(partialTicks) - playerZ; // Get animated color int color = instance.getAnimatedColor(partialTicks); @@ -91,8 +91,8 @@ public class TelegraphRenderer { GL11.glPushMatrix(); GL11.glTranslated(renderX, renderY, renderZ); - // Rotate for directional shapes - GL11.glRotatef(-instance.getYaw(), 0, 1, 0); + // Rotate for directional shapes using interpolated yaw for smooth rotation + GL11.glRotatef(-instance.getInterpolatedYaw(partialTicks), 0, 1, 0); switch (telegraph.getType()) { case CIRCLE: @@ -257,4 +257,83 @@ public class TelegraphRenderer { tessellator.draw(); } + + // ==================== GUI PREVIEW RENDERING ==================== + + /** + * Render a telegraph in GUI space for ability preview. + * This is used by the Manage Abilities GUI to show telegraph shapes. + * + * @param instance The telegraph instance to render + * @param centerX X center position in GUI space + * @param centerY Y center position in GUI space + * @param centerZ Z center position in GUI space + * @param scale Scale factor for the telegraph + * @param partialTicks Partial ticks for interpolation + */ + public void renderTelegraphInGUI(TelegraphInstance instance, + double centerX, double centerY, double centerZ, + float scale, float partialTicks) { + if (instance == null) return; + + Telegraph telegraph = instance.getTelegraph(); + if (telegraph == null || telegraph.getType() == TelegraphType.NONE) { + return; + } + + // Get animated color + int color = instance.getAnimatedColor(partialTicks); + float alpha = ((color >> 24) & 0xFF) / 255.0f; + float red = ((color >> 16) & 0xFF) / 255.0f; + float green = ((color >> 8) & 0xFF) / 255.0f; + float blue = (color & 0xFF) / 255.0f; + + // Setup GL state + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDepthMask(false); + + GL11.glTranslated(centerX, centerY, centerZ); + GL11.glScalef(scale, scale, scale); + + // Rotate for directional shapes + GL11.glRotatef(-instance.getInterpolatedYaw(partialTicks), 0, 1, 0); + + // Render based on type + switch (telegraph.getType()) { + case CIRCLE: + renderCircle(telegraph.getRadius(), red, green, blue, alpha); + break; + case RING: + renderRing(telegraph.getRadius(), telegraph.getInnerRadius(), red, green, blue, alpha); + break; + case LINE: + renderLine(telegraph.getLength(), telegraph.getWidth(), red, green, blue, alpha); + break; + case CONE: + renderCone(telegraph.getLength(), telegraph.getAngle(), red, green, blue, alpha); + break; + case POINT: + renderPoint(red, green, blue, alpha); + break; + default: + break; + } + + // Restore GL state + GL11.glDepthMask(true); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_TEXTURE_2D); + + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } } diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java new file mode 100644 index 000000000..4fa28cda8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java @@ -0,0 +1,281 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.client.renderer.Tessellator; +import org.lwjgl.opengl.GL11; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +/** + * Renders lightning arcs attached to an entity (in local space). + * Supports both instant arcs and fading persistent bolts that move with the entity. + */ +public class AttachedLightningRenderer { + + private static final Random rand = new Random(); + + /** + * Data for a single lightning bolt with fading support. + */ + public static class LightningArc { + public List points; + public int age; + public int maxAge; + public int outerColor; + public int innerColor; + + public LightningArc(List points, int maxAge, int outerColor, int innerColor) { + this.points = points; + this.age = 0; + this.maxAge = maxAge; + this.outerColor = outerColor; + this.innerColor = innerColor; + } + + public float getAlphaMultiplier() { + // Fade out over lifetime + return 1.0f - ((float) age / (float) maxAge); + } + + public boolean isDead() { + return age >= maxAge; + } + + public void tick() { + age++; + } + } + + /** + * Storage for persistent lightning arcs per entity. + * Call updateAndRender() each frame from the entity's renderer. + */ + public static class LightningState { + private final List arcs = new ArrayList<>(); + private float spawnAccumulator = 0; + + /** + * Update arc ages, remove dead arcs, spawn new ones based on density. + * + * @param density Arcs per tick (can be fractional) + * @param radius Max arc distance from center + * @param outerColor Outer glow color + * @param innerColor Inner core color + * @param maxAge How many ticks arcs live (for fading) + */ + public void update(float density, float radius, int outerColor, int innerColor, int maxAge) { + // Age existing arcs and remove dead ones + Iterator iter = arcs.iterator(); + while (iter.hasNext()) { + LightningArc arc = iter.next(); + arc.tick(); + if (arc.isDead()) { + iter.remove(); + } + } + + // Spawn new arcs based on density + spawnAccumulator += density; + while (spawnAccumulator >= 1.0f) { + spawnAccumulator -= 1.0f; + arcs.add(createArc(radius, outerColor, innerColor, maxAge)); + } + } + + /** + * Render all active arcs in local space. + * Call this after glTranslated to entity position. + */ + public void render() { + if (arcs.isEmpty()) return; + + // Setup GL state + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDepthMask(false); + + Tessellator tess = Tessellator.instance; + + for (LightningArc arc : arcs) { + float alphaMult = arc.getAlphaMultiplier(); + + // Render outer glow + renderBoltPath(tess, arc.points, arc.outerColor, 0.025f, 0.35f * alphaMult); + + // Render inner core + renderBoltPath(tess, arc.points, arc.innerColor, 0.012f, 0.7f * alphaMult); + } + + GL11.glDepthMask(true); + GL11.glPopAttrib(); + } + + public int getArcCount() { + return arcs.size(); + } + } + + /** + * Create a new lightning arc from center to a random point. + */ + private static LightningArc createArc(float radius, int outerColor, int innerColor, int maxAge) { + // Random end point on sphere + double theta = rand.nextDouble() * Math.PI * 2; + double phi = Math.acos(2 * rand.nextDouble() - 1); + double r = radius * (0.5 + rand.nextDouble() * 0.5); + + double endX = r * Math.sin(phi) * Math.cos(theta); + double endY = r * Math.sin(phi) * Math.sin(theta); + double endZ = r * Math.cos(phi); + + int segments = 4 + rand.nextInt(3); + float displacement = radius * 0.25f; + + List points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement); + + return new LightningArc(points, maxAge, outerColor, innerColor); + } + + /** + * Generate a jagged lightning path between two points. + */ + private static List generateLightningPath(double x1, double y1, double z1, + double x2, double y2, double z2, + int segments, float displacement) { + List points = new ArrayList<>(); + points.add(new double[]{x1, y1, z1}); + + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + + // Create perpendicular vectors for displacement + Vec3d dir = new Vec3d(dx, dy, dz); + Vec3d perp1 = dir.copy().perpendicular().normalize(); + Vec3d perp2 = dir.copy().crossProduct(perp1).normalize(); + + for (int i = 1; i < segments; i++) { + double t = (double) i / segments; + + double px = x1 + dx * t; + double py = y1 + dy * t; + double pz = z1 + dz * t; + + // Displacement strongest in middle + float strength = (float) (1.0 - Math.abs(t - 0.5) * 2.0) * displacement; + double offset1 = (rand.nextDouble() - 0.5) * 2 * strength; + double offset2 = (rand.nextDouble() - 0.5) * 2 * strength; + + px += perp1.x * offset1 + perp2.x * offset2; + py += perp1.y * offset1 + perp2.y * offset2; + pz += perp1.z * offset1 + perp2.z * offset2; + + points.add(new double[]{px, py, pz}); + } + + points.add(new double[]{x2, y2, z2}); + return points; + } + + /** + * Render the lightning path as quads. + */ + private static void renderBoltPath(Tessellator tess, List points, int color, float width, float alpha) { + if (alpha <= 0.01f) return; + + Color c = new Color(color); + int r = c.getRed(); + int g = c.getGreen(); + int b = c.getBlue(); + int a = Math.min(255, Math.max(0, (int) (alpha * 255))); + + for (int i = 0; i < points.size() - 1; i++) { + double[] p1 = points.get(i); + double[] p2 = points.get(i + 1); + + double segDx = p2[0] - p1[0]; + double segDy = p2[1] - p1[1]; + double segDz = p2[2] - p1[2]; + + Vec3d segDir = new Vec3d(segDx, segDy, segDz); + Vec3d perp = segDir.copy().perpendicular().normalize().multiply(width); + + // First quad + tess.startDrawingQuads(); + tess.setBrightness(0xF000F0); + tess.setColorRGBA(r, g, b, a); + + tess.addVertex(p1[0] - perp.x, p1[1] - perp.y, p1[2] - perp.z); + tess.addVertex(p1[0] + perp.x, p1[1] + perp.y, p1[2] + perp.z); + tess.addVertex(p2[0] + perp.x, p2[1] + perp.y, p2[2] + perp.z); + tess.addVertex(p2[0] - perp.x, p2[1] - perp.y, p2[2] - perp.z); + + tess.draw(); + + // Second quad (perpendicular for 3D look) + Vec3d perp2 = segDir.copy().crossProduct(perp).normalize().multiply(width); + + tess.startDrawingQuads(); + tess.setBrightness(0xF000F0); + tess.setColorRGBA(r, g, b, a); + + tess.addVertex(p1[0] - perp2.x, p1[1] - perp2.y, p1[2] - perp2.z); + tess.addVertex(p1[0] + perp2.x, p1[1] + perp2.y, p1[2] + perp2.z); + tess.addVertex(p2[0] + perp2.x, p2[1] + perp2.y, p2[2] + perp2.z); + tess.addVertex(p2[0] - perp2.x, p2[1] - perp2.y, p2[2] - perp2.z); + + tess.draw(); + } + } + + // ==================== INSTANT RENDERING (no persistence) ==================== + + /** + * Render a single instant lightning arc (no fading, drawn this frame only). + */ + public static void renderInstantArc(float radius, int outerColor, int innerColor) { + double theta = rand.nextDouble() * Math.PI * 2; + double phi = Math.acos(2 * rand.nextDouble() - 1); + double r = radius * (0.5 + rand.nextDouble() * 0.5); + + double endX = r * Math.sin(phi) * Math.cos(theta); + double endY = r * Math.sin(phi) * Math.sin(theta); + double endZ = r * Math.cos(phi); + + int segments = 4 + rand.nextInt(3); + float displacement = radius * 0.25f; + + List points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement); + + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDepthMask(false); + + Tessellator tess = Tessellator.instance; + renderBoltPath(tess, points, outerColor, 0.025f, 0.35f); + renderBoltPath(tess, points, innerColor, 0.012f, 0.7f); + + GL11.glDepthMask(true); + GL11.glPopAttrib(); + } + + /** + * Render multiple instant arcs. + */ + public static void renderCracklingLightning(int count, float radius, int outerColor, int innerColor) { + for (int i = 0; i < count; i++) { + renderInstantArc(radius, outerColor, innerColor); + } + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java new file mode 100644 index 000000000..6cf6e7d3a --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java @@ -0,0 +1,295 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.world.World; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Random; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Lightning bolt data model with fractal segment generation. + * Adapted from Botania's LightningBolt by Vazkii/ChickenBones. + */ +public class LightningBolt { + + public static ConcurrentLinkedQueue boltList = new ConcurrentLinkedQueue<>(); + + public ArrayList segments = new ArrayList<>(); + public Vec3d start; + public Vec3d end; + + public double length; + public int numSegments0; + private int numSplits; + private boolean finalized; + private Random rand; + public long seed; + + public int particleAge; + public int particleMaxAge; + public boolean isDead; + + private World world; + private HashMap splitParents = new HashMap<>(); + + public float speed = 1.5F; + public static final int FADE_TIME = 4; // Short lifespan for projectile-attached lightning + + public int colorOuter; + public int colorInner; + + public LightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, long seed, int colorOuter, int colorInner) { + this.world = world; + this.seed = seed; + this.rand = new Random(seed); + + this.start = start; + this.end = end; + this.speed = ticksPerMeter; + + this.colorOuter = colorOuter; + this.colorInner = colorInner; + + this.numSegments0 = 1; + this.length = end.copy().subtract(start).mag(); + this.particleMaxAge = FADE_TIME + rand.nextInt(FADE_TIME) - FADE_TIME / 2; + this.particleAge = -(int) (length * speed); + + segments.add(new Segment(start, end)); + } + + /** + * Apply default fractal splitting for natural-looking lightning. + */ + public void defaultFractal() { + fractal(2, length / 1.5, 0.7F, 0.7F, 45); + fractal(2, length / 4, 0.5F, 0.8F, 50); + fractal(2, length / 15, 0.5F, 0.9F, 55); + fractal(2, length / 30, 0.5F, 1.0F, 60); + fractal(2, length / 60, 0, 0, 0); + fractal(2, length / 100, 0, 0, 0); + fractal(2, length / 400, 0, 0, 0); + } + + /** + * Simplified fractal for short-lived projectile lightning. + * Less detail but faster and more appropriate for fast-moving objects. + */ + public void simpleFractal() { + fractal(2, length / 2.0, 0.4F, 0.6F, 35); + fractal(2, length / 6, 0.3F, 0.7F, 40); + fractal(2, length / 20, 0, 0, 0); + } + + /** + * Apply fractal splitting to create branching lightning. + * + * @param splits Number of splits per segment + * @param amount Displacement amount + * @param splitChance Probability of branching (0-1) + * @param splitLength Length multiplier for branches + * @param splitAngle Angle for branch splits in degrees + */ + public void fractal(int splits, double amount, double splitChance, double splitLength, double splitAngle) { + if (finalized) return; + + ArrayList oldSegments = segments; + segments = new ArrayList<>(); + + Segment prev = null; + + for (Segment segment : oldSegments) { + prev = segment.prev; + + Vec3d subSegment = segment.diff.copy().multiply(1.0 / splits); + + BoltPoint[] newPoints = new BoltPoint[splits + 1]; + + Vec3d startPoint = segment.startPoint.point; + newPoints[0] = segment.startPoint; + newPoints[splits] = segment.endPoint; + + for (int i = 1; i < splits; i++) { + Vec3d randOff = segment.diff.copy().perpendicular().normalize() + .rotate(rand.nextFloat() * 360, segment.diff); + randOff.multiply((rand.nextFloat() - 0.5F) * amount * 2); + + Vec3d basePoint = startPoint.copy().add(subSegment.copy().multiply(i)); + + newPoints[i] = new BoltPoint(basePoint, randOff); + } + + for (int i = 0; i < splits; i++) { + Segment next = new Segment(newPoints[i], newPoints[i + 1], segment.light, + segment.segmentNo * splits + i, segment.splitNo); + next.prev = prev; + if (prev != null) { + prev.next = next; + } + + if (i != 0 && rand.nextFloat() < splitChance) { + Vec3d splitRot = next.diff.copy().xCrossProduct() + .rotate(rand.nextFloat() * 360, next.diff); + Vec3d diff = next.diff.copy() + .rotate((rand.nextFloat() * 0.66F + 0.33F) * splitAngle, splitRot) + .multiply(splitLength); + + numSplits++; + splitParents.put(numSplits, next.splitNo); + + Segment split = new Segment(newPoints[i], + new BoltPoint(newPoints[i + 1].basePoint, newPoints[i + 1].offsetVec.copy().add(diff)), + segment.light / 2F, next.segmentNo, numSplits); + split.prev = prev; + + segments.add(split); + } + + prev = next; + segments.add(next); + } + + if (segment.next != null) { + segment.next.prev = prev; + } + } + + numSegments0 *= splits; + } + + /** + * Finalize the bolt after all fractal passes. + */ + public void finalizeBolt() { + if (finalized) return; + finalized = true; + + calculateEndDiffs(); + Collections.sort(segments, new SegmentLightSorter()); + } + + private void calculateEndDiffs() { + Collections.sort(segments, new SegmentSorter()); + + for (Segment segment : segments) { + segment.calcEndDiffs(); + } + } + + public void onUpdate() { + particleAge++; + if (particleAge >= particleMaxAge) { + isDead = true; + } + } + + /** + * Update all active lightning bolts. Called from client tick handler. + */ + public static void updateAll() { + for (Iterator iterator = boltList.iterator(); iterator.hasNext(); ) { + LightningBolt bolt = iterator.next(); + bolt.onUpdate(); + if (bolt.isDead) { + iterator.remove(); + } + } + } + + // ==================== INNER CLASSES ==================== + + public class BoltPoint { + public Vec3d point; + public Vec3d basePoint; + public Vec3d offsetVec; + + public BoltPoint(Vec3d basePoint, Vec3d offsetVec) { + this.point = basePoint.copy().add(offsetVec); + this.basePoint = basePoint; + this.offsetVec = offsetVec; + } + } + + public class Segment { + public BoltPoint startPoint; + public BoltPoint endPoint; + + public Vec3d diff; + public Segment prev; + public Segment next; + + public Vec3d nextDiff; + public Vec3d prevDiff; + + public float sinPrev; + public float sinNext; + public float light; + + public int segmentNo; + public int splitNo; + + public Segment(BoltPoint start, BoltPoint end, float light, int segmentNo, int splitNo) { + this.startPoint = start; + this.endPoint = end; + this.light = light; + this.segmentNo = segmentNo; + this.splitNo = splitNo; + calcDiff(); + } + + public Segment(Vec3d start, Vec3d end) { + this(new BoltPoint(start, new Vec3d(0, 0, 0)), + new BoltPoint(end, new Vec3d(0, 0, 0)), 1, 0, 0); + } + + public void calcDiff() { + diff = endPoint.point.copy().subtract(startPoint.point); + } + + public void calcEndDiffs() { + if (prev != null) { + Vec3d prevDiffNorm = prev.diff.copy().normalize(); + Vec3d thisDiffNorm = diff.copy().normalize(); + + prevDiff = thisDiffNorm.copy().add(prevDiffNorm).normalize(); + sinPrev = (float) Math.sin(thisDiffNorm.angle(prevDiffNorm.multiply(-1)) / 2); + } else { + prevDiff = diff.copy().normalize(); + sinPrev = 1; + } + + if (next != null) { + Vec3d nextDiffNorm = next.diff.copy().normalize(); + Vec3d thisDiffNorm = diff.copy().normalize(); + + nextDiff = thisDiffNorm.add(nextDiffNorm).normalize(); + sinNext = (float) Math.sin(thisDiffNorm.angle(nextDiffNorm.multiply(-1)) / 2); + } else { + nextDiff = diff.copy().normalize(); + sinNext = 1; + } + } + } + + public class SegmentSorter implements Comparator { + @Override + public int compare(Segment o1, Segment o2) { + int comp = Integer.compare(o1.splitNo, o2.splitNo); + if (comp == 0) { + return Integer.compare(o1.segmentNo, o2.segmentNo); + } + return comp; + } + } + + public class SegmentLightSorter implements Comparator { + @Override + public int compare(Segment o1, Segment o2) { + return Float.compare(o2.light, o1.light); + } + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java new file mode 100644 index 000000000..1951666e4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java @@ -0,0 +1,202 @@ +package kamkeel.npcs.client.renderer.lightning; + +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.LightningBolt.Segment; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ActiveRenderInfo; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.World; +import net.minecraftforge.client.event.RenderWorldLastEvent; +import org.lwjgl.opengl.GL11; + +import java.awt.Color; + +/** + * Handles rendering of lightning bolts in the world. + * Adapted from Botania's LightningHandler by Vazkii/ChickenBones. + * + * Register this handler with: MinecraftForge.EVENT_BUS.register(new LightningHandler()); + */ +@SideOnly(Side.CLIENT) +public class LightningHandler { + + private static final ResourceLocation TEXTURE_OUTER = new ResourceLocation("customnpcs", "textures/effects/lightning_outer.png"); + private static final ResourceLocation TEXTURE_INNER = new ResourceLocation("customnpcs", "textures/effects/lightning_inner.png"); + + public static int lightningCount = 0; + + private static Vec3d getRelativeViewVector(Vec3d pos) { + Entity renderEntity = Minecraft.getMinecraft().renderViewEntity; + return new Vec3d( + (float) renderEntity.posX - pos.x, + (float) renderEntity.posY + renderEntity.getEyeHeight() - pos.y, + (float) renderEntity.posZ - pos.z + ); + } + + @SubscribeEvent + public void onRenderWorldLast(RenderWorldLastEvent event) { + float partialTicks = event.partialTicks; + Entity entity = Minecraft.getMinecraft().thePlayer; + TextureManager textureManager = Minecraft.getMinecraft().renderEngine; + + double interpPosX = entity.lastTickPosX + (entity.posX - entity.lastTickPosX) * partialTicks; + double interpPosY = entity.lastTickPosY + (entity.posY - entity.lastTickPosY) * partialTicks; + double interpPosZ = entity.lastTickPosZ + (entity.posZ - entity.lastTickPosZ) * partialTicks; + + if (LightningBolt.boltList.isEmpty()) { + return; + } + + GL11.glPushMatrix(); + GL11.glTranslated(-interpPosX, -interpPosY, -interpPosZ); + + Tessellator tessellator = Tessellator.instance; + + GL11.glDepthMask(false); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + + lightningCount = 0; + + // Render outer layer (translucent glow) + for (LightningBolt bolt : LightningBolt.boltList) { + renderBolt(bolt, tessellator, partialTicks, false); + } + + // Render inner layer (bright core) + for (LightningBolt bolt : LightningBolt.boltList) { + renderBolt(bolt, tessellator, partialTicks, true); + } + + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_BLEND); + GL11.glDepthMask(true); + + GL11.glPopMatrix(); + } + + private void renderBolt(LightningBolt bolt, Tessellator tessellator, float partialTicks, boolean inner) { + lightningCount++; + + float boltAge = bolt.particleAge < 0 ? 0 : (float) bolt.particleAge / (float) bolt.particleMaxAge; + float mainAlpha; + if (!inner) { + mainAlpha = (1 - boltAge) * 0.4F; + } else { + mainAlpha = 1 - boltAge * 0.5F; + } + + int expandTime = (int) (bolt.length * bolt.speed); + + int renderStart = (int) ((expandTime / 2 - bolt.particleMaxAge + bolt.particleAge) / (float) (expandTime / 2) * bolt.numSegments0); + int renderEnd = (int) ((bolt.particleAge + expandTime) / (float) expandTime * bolt.numSegments0); + + for (Segment segment : bolt.segments) { + if (segment.segmentNo < renderStart || segment.segmentNo > renderEnd) { + continue; + } + + Vec3d playerVec = getRelativeViewVector(segment.startPoint.point).multiply(-1); + + double width = 0.008F * (playerVec.mag() / 8 + 1) * (1 + segment.light) * 0.4F; + + Vec3d diff1 = playerVec.copy().crossProduct(segment.prevDiff).normalize().multiply(width / segment.sinPrev); + Vec3d diff2 = playerVec.copy().crossProduct(segment.nextDiff).normalize().multiply(width / segment.sinNext); + + Vec3d startVec = segment.startPoint.point; + Vec3d endVec = segment.endPoint.point; + + int color = inner ? bolt.colorInner : bolt.colorOuter; + Color c = new Color(color); + int alpha = (int) (mainAlpha * segment.light * c.getAlpha()); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(endVec.x - diff2.x, endVec.y - diff2.y, endVec.z - diff2.z); + tessellator.addVertex(startVec.x - diff1.x, startVec.y - diff1.y, startVec.z - diff1.z); + tessellator.addVertex(startVec.x + diff1.x, startVec.y + diff1.y, startVec.z + diff1.z); + tessellator.addVertex(endVec.x + diff2.x, endVec.y + diff2.y, endVec.z + diff2.z); + + tessellator.draw(); + + // Rounded end caps + if (segment.next == null) { + Vec3d roundEnd = segment.endPoint.point.copy().add(segment.diff.copy().normalize().multiply(width)); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(roundEnd.x - diff2.x, roundEnd.y - diff2.y, roundEnd.z - diff2.z); + tessellator.addVertex(endVec.x - diff2.x, endVec.y - diff2.y, endVec.z - diff2.z); + tessellator.addVertex(endVec.x + diff2.x, endVec.y + diff2.y, endVec.z + diff2.z); + tessellator.addVertex(roundEnd.x + diff2.x, roundEnd.y + diff2.y, roundEnd.z + diff2.z); + + tessellator.draw(); + } + + if (segment.prev == null) { + Vec3d roundEnd = segment.startPoint.point.copy().subtract(segment.diff.copy().normalize().multiply(width)); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(startVec.x - diff1.x, startVec.y - diff1.y, startVec.z - diff1.z); + tessellator.addVertex(roundEnd.x - diff1.x, roundEnd.y - diff1.y, roundEnd.z - diff1.z); + tessellator.addVertex(roundEnd.x + diff1.x, roundEnd.y + diff1.y, roundEnd.z + diff1.z); + tessellator.addVertex(startVec.x + diff1.x, startVec.y + diff1.y, startVec.z + diff1.z); + + tessellator.draw(); + } + } + } + + /** + * Spawn a lightning bolt between two points. + * + * @param world The world + * @param start Start position + * @param end End position + * @param ticksPerMeter Speed of bolt expansion + * @param seed Random seed for consistent appearance + * @param colorOuter Outer glow color (ARGB) + * @param colorInner Inner core color (ARGB) + */ + public static void spawnLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, long seed, int colorOuter, int colorInner) { + LightningBolt bolt = new LightningBolt(world, start, end, ticksPerMeter, seed, colorOuter, colorInner); + bolt.defaultFractal(); + bolt.finalizeBolt(); + LightningBolt.boltList.add(bolt); + } + + /** + * Spawn a lightning bolt with automatic seed. + */ + public static void spawnLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, int colorOuter, int colorInner) { + spawnLightningBolt(world, start, end, ticksPerMeter, System.nanoTime(), colorOuter, colorInner); + } + + /** + * Spawn a simple lightning bolt for projectiles - less detail, faster. + */ + public static void spawnSimpleLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, int colorOuter, int colorInner) { + LightningBolt bolt = new LightningBolt(world, start, end, ticksPerMeter, System.nanoTime(), colorOuter, colorInner); + bolt.simpleFractal(); + bolt.finalizeBolt(); + LightningBolt.boltList.add(bolt); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java new file mode 100644 index 000000000..cda0307b7 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java @@ -0,0 +1,85 @@ +package kamkeel.npcs.client.renderer.lightning; + +/** + * Quaternion class for axis-angle rotations in lightning calculations. + * Adapted from Botania's Quat by ChickenBones. + */ +public class Quaternion { + public double x; + public double y; + public double z; + public double s; + + public Quaternion() { + s = 1.0; + x = 0.0; + y = 0.0; + z = 0.0; + } + + public Quaternion(Quaternion quat) { + x = quat.x; + y = quat.y; + z = quat.z; + s = quat.s; + } + + public Quaternion(double s, double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + this.s = s; + } + + public static Quaternion aroundAxis(double ax, double ay, double az, double angle) { + angle *= 0.5; + double sinAngle = Math.sin(angle); + return new Quaternion(Math.cos(angle), ax * sinAngle, ay * sinAngle, az * sinAngle); + } + + public static Quaternion aroundAxis(Vec3d axis, double angle) { + return aroundAxis(axis.x, axis.y, axis.z, angle); + } + + public void multiply(Quaternion quat) { + double d = s * quat.s - x * quat.x - y * quat.y - z * quat.z; + double d1 = s * quat.x + x * quat.s - y * quat.z + z * quat.y; + double d2 = s * quat.y + x * quat.z + y * quat.s - z * quat.x; + double d3 = s * quat.z - x * quat.y + y * quat.x + z * quat.s; + s = d; + x = d1; + y = d2; + z = d3; + } + + public double mag() { + return Math.sqrt(x * x + y * y + z * z + s * s); + } + + public void normalize() { + double d = mag(); + if (d == 0.0) { + return; + } + d = 1.0 / d; + x *= d; + y *= d; + z *= d; + s *= d; + } + + public void rotate(Vec3d vec) { + double d = -x * vec.x - y * vec.y - z * vec.z; + double d1 = s * vec.x + y * vec.z - z * vec.y; + double d2 = s * vec.y - x * vec.z + z * vec.x; + double d3 = s * vec.z + x * vec.y - y * vec.x; + vec.x = d1 * s - d * x - d2 * z + d3 * y; + vec.y = d2 * s - d * y + d1 * z - d3 * x; + vec.z = d3 * s - d * z - d1 * y + d2 * x; + } + + @Override + public String toString() { + return "Quaternion(" + s + ", " + x + ", " + y + ", " + z + ")"; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java new file mode 100644 index 000000000..018e55bdb --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java @@ -0,0 +1,184 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.entity.Entity; +import net.minecraft.util.Vec3; +import org.lwjgl.opengl.GL11; + +/** + * 3D Vector helper class for lightning calculations. + * Adapted from Botania's Vector3 by ChickenBones. + */ +public class Vec3d { + public double x; + public double y; + public double z; + + public Vec3d() { + } + + public Vec3d(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vec3d(Vec3d vec) { + this.x = vec.x; + this.y = vec.y; + this.z = vec.z; + } + + public Vec3d(Vec3 vec) { + this.x = vec.xCoord; + this.y = vec.yCoord; + this.z = vec.zCoord; + } + + public Vec3d copy() { + return new Vec3d(this); + } + + public static Vec3d fromEntity(Entity e) { + return new Vec3d(e.posX, e.posY, e.posZ); + } + + public static Vec3d fromEntityCenter(Entity e) { + return new Vec3d(e.posX, e.posY - e.yOffset + e.height / 2, e.posZ); + } + + public Vec3d set(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + public Vec3d set(Vec3d vec) { + this.x = vec.x; + this.y = vec.y; + this.z = vec.z; + return this; + } + + public double dotProduct(Vec3d vec) { + double d = vec.x * x + vec.y * y + vec.z * z; + if (d > 1 && d < 1.00001) d = 1; + else if (d < -1 && d > -1.00001) d = -1; + return d; + } + + public Vec3d crossProduct(Vec3d vec) { + double d = y * vec.z - z * vec.y; + double d1 = z * vec.x - x * vec.z; + double d2 = x * vec.y - y * vec.x; + x = d; + y = d1; + z = d2; + return this; + } + + public Vec3d add(double x, double y, double z) { + this.x += x; + this.y += y; + this.z += z; + return this; + } + + public Vec3d add(Vec3d vec) { + this.x += vec.x; + this.y += vec.y; + this.z += vec.z; + return this; + } + + public Vec3d subtract(Vec3d vec) { + this.x -= vec.x; + this.y -= vec.y; + this.z -= vec.z; + return this; + } + + public Vec3d multiply(double d) { + this.x *= d; + this.y *= d; + this.z *= d; + return this; + } + + public double mag() { + return Math.sqrt(x * x + y * y + z * z); + } + + public double magSquared() { + return x * x + y * y + z * z; + } + + public Vec3d normalize() { + double d = mag(); + if (d != 0) { + multiply(1 / d); + } + return this; + } + + public Vec3d perpendicular() { + if (z == 0) { + return zCrossProduct(); + } + return xCrossProduct(); + } + + public Vec3d xCrossProduct() { + double d = z; + double d1 = -y; + x = 0; + y = d; + z = d1; + return this; + } + + public Vec3d zCrossProduct() { + double d = y; + double d1 = -x; + x = d; + y = d1; + z = 0; + return this; + } + + public Vec3 toVec3() { + return Vec3.createVectorHelper(x, y, z); + } + + public double angle(Vec3d vec) { + return Math.acos(copy().normalize().dotProduct(vec.copy().normalize())); + } + + public Vec3d negate() { + x = -x; + y = -y; + z = -z; + return this; + } + + public Vec3d rotate(double angle, Vec3d axis) { + Quaternion.aroundAxis(axis.copy().normalize(), angle).rotate(this); + return this; + } + + public void glVertex() { + GL11.glVertex3d(x, y, z); + } + + @Override + public String toString() { + return "Vec3d(" + x + ", " + y + ", " + z + ")"; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Vec3d)) return false; + Vec3d v = (Vec3d) o; + return x == v.x && y == v.y && z == v.z; + } +} diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index 4a783f551..d5b9420cc 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -25,25 +25,23 @@ public class AbilityCommand extends CommandKamkeelBase { } @SubCommand( - desc = "List all saved abilities" + desc = "List all custom abilities" ) public void list(ICommandSender sender, String[] args) { - Set names = AbilityController.Instance.getSavedAbilityNames(); - if (names.isEmpty()) { - sendResult(sender, "No saved abilities found."); + Set uuids = AbilityController.Instance.getCustomAbilityIds(); + if (uuids.isEmpty()) { + sendResult(sender, "No custom abilities found."); return; } - List sortedNames = new ArrayList<>(names); - Collections.sort(sortedNames); - - sendResult(sender, "Saved Abilities (" + sortedNames.size() + "):"); - for (String name : sortedNames) { - Ability ability = AbilityController.Instance.getSavedAbility(name); + sendResult(sender, "Custom Abilities (" + uuids.size() + "):"); + for (String uuid : uuids) { + Ability ability = AbilityController.Instance.getCustomAbility(uuid); if (ability != null) { - sendResult(sender, " - \u00A7b" + name + "\u00A77 (" + ability.getTypeId() + ")"); + String name = ability.getName() != null ? ability.getName() : uuid; + sendResult(sender, " - \u00A7b" + name + "\u00A77 (" + ability.getTypeId() + ") [" + uuid + "]"); } else { - sendResult(sender, " - \u00A7b" + name); + sendResult(sender, " - \u00A7b" + uuid); } } } @@ -79,39 +77,39 @@ public class AbilityCommand extends CommandKamkeelBase { } @SubCommand( - desc = "Delete a saved ability", - usage = "" + desc = "Delete a custom ability by UUID", + usage = "" ) public void delete(ICommandSender sender, String[] args) { if (args.length < 1) { - sendError(sender, "Usage: /kam ability delete "); + sendError(sender, "Usage: /kam ability delete "); return; } - String name = args[0]; - if (!AbilityController.Instance.getSavedAbilityNames().contains(name)) { - sendError(sender, "No saved ability with name: " + name); + String uuid = args[0]; + if (!AbilityController.Instance.hasCustomAbility(uuid)) { + sendError(sender, "No custom ability with UUID: " + uuid); return; } - AbilityController.Instance.deleteAbility(name); - sendResult(sender, "Deleted ability: \u00A7b" + name); + AbilityController.Instance.deleteCustomAbility(uuid); + sendResult(sender, "Deleted ability: \u00A7b" + uuid); } @SubCommand( - desc = "Get info about a saved ability", - usage = "" + desc = "Get info about an ability", + usage = "" ) public void info(ICommandSender sender, String[] args) { if (args.length < 1) { - sendError(sender, "Usage: /kam ability info "); + sendError(sender, "Usage: /kam ability info "); return; } - String name = args[0]; - Ability ability = AbilityController.Instance.getSavedAbility(name); + String key = args[0]; + Ability ability = AbilityController.Instance.resolveAbility(key); if (ability == null) { - sendError(sender, "No saved ability with name: " + name); + sendError(sender, "No ability found for: " + key); return; } @@ -119,8 +117,9 @@ public class AbilityCommand extends CommandKamkeelBase { sendResult(sender, " Type: \u00A7d" + ability.getTypeId()); sendResult(sender, " Cooldown: \u00A7e" + ability.getCooldownTicks() + " ticks"); sendResult(sender, " Wind Up: \u00A7e" + ability.getWindUpTicks() + " ticks"); - sendResult(sender, " Active: \u00A7e" + ability.getActiveTicks() + " ticks"); - sendResult(sender, " Recovery: \u00A7e" + ability.getRecoveryTicks() + " ticks"); + if (ability.isInterruptible()) { + sendResult(sender, " Dazed: \u00A7e" + ability.getDazedTicks() + " ticks"); + } sendResult(sender, " Range: \u00A7e" + ability.getMinRange() + " - " + ability.getMaxRange()); sendResult(sender, " Telegraph: \u00A7e" + (ability.isShowTelegraph() ? "Yes" : "No")); } diff --git a/src/main/java/kamkeel/npcs/command/AuctionCommand.java b/src/main/java/kamkeel/npcs/command/AuctionCommand.java new file mode 100644 index 000000000..87d7e4385 --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/AuctionCommand.java @@ -0,0 +1,179 @@ +package kamkeel.npcs.command; + +import kamkeel.npcs.command.auction.BlacklistSubCommand; +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.controllers.data.PlayerData; + +import java.util.List; +import java.util.UUID; + +import static kamkeel.npcs.util.ColorUtil.sendError; +import static kamkeel.npcs.util.ColorUtil.sendMessage; +import static kamkeel.npcs.util.ColorUtil.sendResult; + +public class AuctionCommand extends CommandKamkeelBase { + + public AuctionCommand() { + super(); + registerNestedCommand(new BlacklistSubCommand()); + } + + @Override + public String getCommandName() { + return "auction"; + } + + @Override + public String getDescription() { + return "Auction operations"; + } + + @SubCommand( + desc = "List active auctions", + usage = "[page]" + ) + public void list(ICommandSender sender, String[] args) throws CommandException { + if (!ConfigMarket.AuctionEnabled) { + sendError(sender, "Auction system is disabled"); + return; + } + + AuctionController controller = AuctionController.getInstance(); + if (controller == null) { + sendError(sender, "Auction system is not available"); + return; + } + + int page = 1; + if (args.length > 0) { + try { + page = Integer.parseInt(args[0]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid page number: " + args[0]); + return; + } + } + + List listings = controller.getActiveListings(null, page - 1, 10); + int total = controller.getTotalActiveListings(null); + int totalPages = Math.max(1, (int) Math.ceil((double) total / 10)); + + if (listings.isEmpty()) { + sendMessage(sender, "\u00A77No active auctions found"); + return; + } + + sendMessage(sender, String.format("\u00A76=== Active Auctions (Page %d/%d) ===", page, totalPages)); + for (AuctionListing listing : listings) { + String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown"; + String price = listing.hasBids() ? + String.format("%,d (bid)", listing.currentBid) : + String.format("%,d (start)", listing.startingPrice); + sendMessage(sender, String.format("\u00A7e%s \u00A77by \u00A7b%s\u00A77 - \u00A76%s %s", + itemName, listing.sellerName, price, ConfigMarket.CurrencyName)); + } + } + + @SubCommand( + desc = "View player's auction activity", + usage = "" + ) + public void view(ICommandSender sender, String[] args) throws CommandException { + if (!ConfigMarket.AuctionEnabled) { + sendError(sender, "Auction system is disabled"); + return; + } + + AuctionController controller = AuctionController.getInstance(); + if (controller == null) { + sendError(sender, "Auction system is not available"); + return; + } + + String playername = args[0]; + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + UUID playerUUID = UUID.fromString(playerdata.uuid); + List activeListings = controller.getPlayerActiveListings(playerUUID); + List activeBids = controller.getPlayerActiveBids(playerUUID); + int claimCount = controller.getPlayerClaims(playerUUID).size(); + + sendMessage(sender, String.format("\u00A76=== Auction Activity for %s ===", playerdata.playername)); + sendMessage(sender, String.format("\u00A77Active Listings: \u00A7e%d", activeListings.size())); + sendMessage(sender, String.format("\u00A77Active Bids: \u00A7e%d", activeBids.size())); + sendMessage(sender, String.format("\u00A77Pending Claims: \u00A7e%d", claimCount)); + + if (!activeListings.isEmpty()) { + sendMessage(sender, "\u00A7a--- Selling ---"); + for (AuctionListing listing : activeListings) { + String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown"; + String price = listing.hasBids() ? + String.format("%,d (bid)", listing.currentBid) : + String.format("%,d (start)", listing.startingPrice); + sendMessage(sender, String.format(" \u00A7e%s \u00A77- \u00A76%s", itemName, price)); + } + } + + if (!activeBids.isEmpty()) { + sendMessage(sender, "\u00A7e--- Bidding ---"); + for (AuctionListing bid : activeBids) { + String itemName = bid.item != null ? bid.item.getDisplayName() : "Unknown"; + sendMessage(sender, String.format(" \u00A7e%s \u00A77- \u00A76%,d %s", + itemName, bid.currentBid, ConfigMarket.CurrencyName)); + } + } + } + } + + @SubCommand( + desc = "Open the Auction GUI", + usage = "", + permission = 0 + ) + public void open(ICommandSender sender, String[] args) throws CommandException { + if (!ConfigMarket.AuctionEnabled) { + sendError(sender, "Auction system is disabled"); + return; + } + + if (!(sender instanceof EntityPlayer)) { + sendError(sender, "This command can only be used by players"); + return; + } + + EntityPlayer player = (EntityPlayer) sender; + + // Open the auction GUI without requiring an NPC + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, null); + sendResult(sender, "Opening Auction..."); + } + + @Override + public List addTabCompletionOptions(ICommandSender sender, String[] args) { + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, getAllSubCommandNames()); + } + + // Delegate to nested commands for deeper tab completion + List nestedCompletions = getNestedTabCompletions(sender, args); + if (nestedCompletions != null) { + return nestedCompletions; + } + + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java index 356ca8a31..d0341f211 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java @@ -10,6 +10,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.server.MinecraftServer; import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.config.ConfigMain; +import noppes.npcs.config.ConfigMarket; import noppes.npcs.scripted.CustomNPCsException; import java.lang.reflect.Method; @@ -44,6 +45,9 @@ public class CommandKamkeel extends CommandBase { registerCommand(new CommandCommand()); registerCommand(new EffectCommand()); registerCommand(new AbilityCommand()); + registerCommand(new MoneyCommand()); + if (ConfigMarket.AuctionEnabled) + registerCommand(new AuctionCommand()); if (ConfigMain.AttributesEnabled) registerCommand(new AttributeCommand()); } diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java b/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java index af2366f89..df4d34998 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java @@ -11,13 +11,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public abstract class CommandKamkeelBase extends CommandBase { public Map subcommands = new HashMap(); + public Map nestedCommands = new HashMap(); public CommandKamkeelBase() { for (Method m : this.getClass().getDeclaredMethods()) { @@ -31,6 +29,14 @@ public abstract class CommandKamkeelBase extends CommandBase { } } + /** + * Register a nested command class as a subcommand. + * The nested command's getCommandName() becomes the subcommand name. + */ + public void registerNestedCommand(CommandKamkeelBase command) { + nestedCommands.put(command.getCommandName().toLowerCase(), command); + } + @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { @@ -51,7 +57,7 @@ public abstract class CommandKamkeelBase extends CommandBase { } public boolean runSubCommands() { - return !subcommands.isEmpty(); + return !subcommands.isEmpty() || !nestedCommands.isEmpty(); } @Retention(value = RetentionPolicy.RUNTIME) @@ -71,6 +77,22 @@ public abstract class CommandKamkeelBase extends CommandBase { } public void processSubCommand(ICommandSender sender, String command, String[] args) throws CommandException { + // Check nested commands first + CommandKamkeelBase nested = nestedCommands.get(command.toLowerCase()); + if (nested != null) { + if (!canSendNestedCommand(sender, nested)) { + throw new CommandException("You are not allowed to use this command: " + command); + } + + if (args.length == 0 || !nested.runSubCommands()) { + nested.processCommand(sender, args); + } else { + nested.processSubCommand(sender, args[0], Arrays.copyOfRange(args, 1, args.length)); + } + return; + } + + // Fall back to method subcommands Method m = subcommands.get(command.toLowerCase()); if (m == null) throw new CommandException("Unknown subcommand " + command); @@ -132,4 +154,55 @@ public abstract class CommandKamkeelBase extends CommandBase { return false; } + + public boolean canSendNestedCommand(ICommandSender sender, CommandKamkeelBase nested) { + int permission = nested.getRequiredPermissionLevel(); + String subCommand = nested.getCommandName(); + + if (sender.canCommandSenderUseCommand(permission, getSubUniversalPermission())) { + return true; + } + + if (sender.canCommandSenderUseCommand(permission, getSubCommandPermission(subCommand))) { + return true; + } + + if (sender instanceof EntityPlayer) { + return CustomNpcsPermissions.hasCustomPermission((EntityPlayer) sender, getSubCommandPermission(subCommand)); + } + + return false; + } + + // ========================================= + // Tab Completion Helpers + // ========================================= + + /** + * Get all subcommand names (both method-based and nested) + */ + public String[] getAllSubCommandNames() { + Set names = new HashSet<>(); + names.addAll(subcommands.keySet()); + names.addAll(nestedCommands.keySet()); + return names.toArray(new String[0]); + } + + /** + * Get tab completions for nested command at given depth + */ + public List getNestedTabCompletions(ICommandSender sender, String[] args) { + if (args.length == 0) return null; + + String subCmd = args[0].toLowerCase(); + CommandKamkeelBase nested = nestedCommands.get(subCmd); + + if (nested != null && args.length > 1) { + // Delegate to nested command + String[] nestedArgs = Arrays.copyOfRange(args, 1, args.length); + return nested.addTabCompletionOptions(sender, nestedArgs); + } + + return null; + } } diff --git a/src/main/java/kamkeel/npcs/command/MoneyCommand.java b/src/main/java/kamkeel/npcs/command/MoneyCommand.java new file mode 100644 index 000000000..ee034243d --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/MoneyCommand.java @@ -0,0 +1,168 @@ +package kamkeel.npcs.command; + +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerTradeData; + +import java.util.List; + +import static kamkeel.npcs.util.ColorUtil.sendError; +import static kamkeel.npcs.util.ColorUtil.sendResult; + +public class MoneyCommand extends CommandKamkeelBase { + + @Override + public String getCommandName() { + return "money"; + } + + @Override + public String getDescription() { + return "Currency operations"; + } + + @SubCommand( + desc = "Show player's balance", + usage = "" + ) + public void balance(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerTradeData currency = playerdata.tradeData; + long balance = currency.getBalance(); + sendResult(sender, String.format("Player \u00A7b%s\u00A77 has \u00A76%,d %s", + playerdata.playername, balance, ConfigMarket.CurrencyName)); + } + } + + @SubCommand( + desc = "Give currency to player", + usage = " " + ) + public void give(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + long amount; + try { + amount = Long.parseLong(args[1]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid amount: " + args[1]); + return; + } + + if (amount <= 0) { + sendError(sender, "Amount must be positive"); + return; + } + + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerTradeData currency = playerdata.tradeData; + if (currency.deposit(amount)) { + playerdata.save(); + sendResult(sender, String.format("Gave \u00A76%,d %s\u00A77 to player \u00A7b%s\u00A77. New balance: \u00A76%,d", + amount, ConfigMarket.CurrencyName, playerdata.playername, currency.getBalance())); + } else { + sendError(sender, String.format("Failed to give currency to %s (would exceed max balance)", + playerdata.playername)); + } + } + } + + @SubCommand( + desc = "Withdraw currency from player", + usage = " " + ) + public void withdraw(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + long amount; + try { + amount = Long.parseLong(args[1]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid amount: " + args[1]); + return; + } + + if (amount <= 0) { + sendError(sender, "Amount must be positive"); + return; + } + + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerTradeData currency = playerdata.tradeData; + if (currency.withdraw(amount)) { + playerdata.save(); + sendResult(sender, String.format("Withdrew \u00A76%,d %s\u00A77 from player \u00A7b%s\u00A77. New balance: \u00A76%,d", + amount, ConfigMarket.CurrencyName, playerdata.playername, currency.getBalance())); + } else { + sendError(sender, String.format("Failed to withdraw from %s (insufficient funds)", + playerdata.playername)); + } + } + } + + @SubCommand( + desc = "Set player's balance", + usage = " " + ) + public void set(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + long amount; + try { + amount = Long.parseLong(args[1]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid amount: " + args[1]); + return; + } + + if (amount < 0) { + sendError(sender, "Amount cannot be negative"); + return; + } + + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerTradeData currency = playerdata.tradeData; + currency.setBalance(amount); + playerdata.save(); + sendResult(sender, String.format("Set balance of player \u00A7b%s\u00A77 to \u00A76%,d %s", + playerdata.playername, currency.getBalance(), ConfigMarket.CurrencyName)); + } + } + + @Override + public List addTabCompletionOptions(ICommandSender par1, String[] args) { + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, new String[]{"balance", "give", "withdraw", "set"}); + } + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/command/ScriptCommand.java b/src/main/java/kamkeel/npcs/command/ScriptCommand.java index 41415cb81..61b380dbd 100644 --- a/src/main/java/kamkeel/npcs/command/ScriptCommand.java +++ b/src/main/java/kamkeel/npcs/command/ScriptCommand.java @@ -12,6 +12,7 @@ public class ScriptCommand extends CommandKamkeelBase { @SubCommand(desc = "Reload scripts data and folders.") public Boolean reload(ICommandSender sender, String args[]) { ScriptController.Instance.loadCategories(); + ScriptController.Instance.syncClientScripts(null); if (ScriptController.Instance.loadPlayerScripts()) sendResult(sender, "Reload player scripts successfully"); diff --git a/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java b/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java new file mode 100644 index 000000000..a9902a890 --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java @@ -0,0 +1,226 @@ +package kamkeel.npcs.command.auction; + +import kamkeel.npcs.command.CommandKamkeelBase; +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import noppes.npcs.controllers.data.AuctionBlacklist; + +import java.util.List; + +import static kamkeel.npcs.util.ColorUtil.sendError; +import static kamkeel.npcs.util.ColorUtil.sendMessage; +import static kamkeel.npcs.util.ColorUtil.sendResult; + +public class BlacklistSubCommand extends CommandKamkeelBase { + + @Override + public String getCommandName() { + return "blacklist"; + } + + @Override + public String getDescription() { + return "Manage auction item blacklist"; + } + + @Override + public void processCommand(ICommandSender sender, String[] args) throws CommandException { + // Show help when called with no subcommand + sendHelp(sender); + } + + private void sendHelp(ICommandSender sender) { + sendMessage(sender, "\u00A76=== Auction Blacklist Commands ==="); + sendMessage(sender, "\u00A7e/cnpc auction blacklist add "); + sendMessage(sender, "\u00A7e/cnpc auction blacklist remove "); + sendMessage(sender, "\u00A7e/cnpc auction blacklist list [item|mod|nbt]"); + sendMessage(sender, "\u00A7e/cnpc auction blacklist reload"); + sendMessage(sender, "\u00A7e/cnpc auction blacklist check \u00A77(checks held item)"); + } + + @SubCommand( + desc = "Add item, mod, or NBT tag to blacklist", + usage = " " + ) + public void add(ICommandSender sender, String[] args) throws CommandException { + if (args.length < 2) { + sendError(sender, "Usage: /cnpc auction blacklist add "); + return; + } + + String type = args[0].toLowerCase(); + String value = args[1]; + + switch (type) { + case "item": + if (AuctionBlacklist.addItem(value)) { + sendResult(sender, "Added item to blacklist: " + value); + } else { + sendError(sender, "Failed to add item to blacklist"); + } + break; + case "mod": + if (AuctionBlacklist.addMod(value)) { + sendResult(sender, "Added mod to blacklist: " + value); + } else { + sendError(sender, "Failed to add mod to blacklist"); + } + break; + case "nbt": + if (AuctionBlacklist.addNBTTag(value)) { + sendResult(sender, "Added NBT tag to blacklist: " + value); + } else { + sendError(sender, "Failed to add NBT tag to blacklist"); + } + break; + default: + sendError(sender, "Unknown type: " + type + ". Use: item, mod, or nbt"); + } + } + + @SubCommand( + desc = "Remove item, mod, or NBT tag from blacklist", + usage = " " + ) + public void remove(ICommandSender sender, String[] args) throws CommandException { + if (args.length < 2) { + sendError(sender, "Usage: /cnpc auction blacklist remove "); + return; + } + + String type = args[0].toLowerCase(); + String value = args[1]; + + switch (type) { + case "item": + if (AuctionBlacklist.removeItem(value)) { + sendResult(sender, "Removed item from blacklist: " + value); + } else { + sendError(sender, "Item not found in blacklist: " + value); + } + break; + case "mod": + if (AuctionBlacklist.removeMod(value)) { + sendResult(sender, "Removed mod from blacklist: " + value); + } else { + sendError(sender, "Mod not found in blacklist: " + value); + } + break; + case "nbt": + if (AuctionBlacklist.removeNBTTag(value)) { + sendResult(sender, "Removed NBT tag from blacklist: " + value); + } else { + sendError(sender, "NBT tag not found in blacklist: " + value); + } + break; + default: + sendError(sender, "Unknown type: " + type + ". Use: item, mod, or nbt"); + } + } + + @SubCommand( + desc = "List blacklisted items, mods, or NBT tags", + usage = "[item|mod|nbt]" + ) + public void list(ICommandSender sender, String[] args) throws CommandException { + String filter = args.length > 0 ? args[0].toLowerCase() : "all"; + + if (filter.equals("all") || filter.equals("item")) { + List items = AuctionBlacklist.getBlacklistedItems(); + sendMessage(sender, "\u00A76=== Blacklisted Items (" + items.size() + ") ==="); + if (items.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String item : items) { + sendMessage(sender, "\u00A7e " + item); + } + } + } + + if (filter.equals("all") || filter.equals("mod")) { + List mods = AuctionBlacklist.getBlacklistedMods(); + sendMessage(sender, "\u00A76=== Blacklisted Mods (" + mods.size() + ") ==="); + if (mods.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String mod : mods) { + sendMessage(sender, "\u00A7e " + mod); + } + } + } + + if (filter.equals("all") || filter.equals("nbt")) { + List tags = AuctionBlacklist.getBlacklistedNBTTags(); + sendMessage(sender, "\u00A76=== Blacklisted NBT Tags (" + tags.size() + ") ==="); + if (tags.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String tag : tags) { + sendMessage(sender, "\u00A7e " + tag); + } + } + } + + sendMessage(sender, "\u00A77Note: Soulbound and Profile-Slotbound items are always blocked."); + } + + @SubCommand( + desc = "Reload blacklist from config" + ) + public void reload(ICommandSender sender, String[] args) throws CommandException { + AuctionBlacklist.reload(); + sendResult(sender, "Blacklist reloaded from config"); + } + + @SubCommand( + desc = "Check if held item is blacklisted", + permission = 0 + ) + public void check(ICommandSender sender, String[] args) throws CommandException { + if (!(sender instanceof EntityPlayer)) { + sendError(sender, "This command can only be used by players"); + return; + } + + EntityPlayer player = (EntityPlayer) sender; + ItemStack heldItem = player.getHeldItem(); + + if (heldItem == null) { + sendError(sender, "You are not holding an item"); + return; + } + + String registryName = AuctionBlacklist.getRegistryName(heldItem); + sendMessage(sender, "\u00A76=== Blacklist Check ==="); + sendMessage(sender, "\u00A77Item: \u00A7e" + heldItem.getDisplayName()); + sendMessage(sender, "\u00A77Registry: \u00A7e" + (registryName != null ? registryName : "unknown")); + + boolean isBlacklisted = AuctionBlacklist.isBlacklisted(heldItem); + boolean canBypass = AuctionBlacklist.canBypass(player); + + if (isBlacklisted) { + sendMessage(sender, "\u00A7cStatus: BLACKLISTED"); + if (canBypass) { + sendMessage(sender, "\u00A7aYou have bypass permission"); + } + } else { + sendMessage(sender, "\u00A7aStatus: ALLOWED"); + } + } + + @Override + public List addTabCompletionOptions(ICommandSender sender, String[] args) { + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, getAllSubCommandNames()); + } + if (args.length == 2) { + String subCmd = args[0].toLowerCase(); + if (subCmd.equals("add") || subCmd.equals("remove") || subCmd.equals("list")) { + return getListOfStringsMatchingLastWord(args, new String[]{"item", "mod", "nbt"}); + } + } + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/ProfileController.java b/src/main/java/kamkeel/npcs/controllers/ProfileController.java index 73f16ebf6..103da1309 100644 --- a/src/main/java/kamkeel/npcs/controllers/ProfileController.java +++ b/src/main/java/kamkeel/npcs/controllers/ProfileController.java @@ -1,6 +1,5 @@ package kamkeel.npcs.controllers; -import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.data.profile.CNPCData; import kamkeel.npcs.controllers.data.profile.EnumProfileOperation; import kamkeel.npcs.controllers.data.profile.IProfileData; @@ -833,6 +832,15 @@ public class ProfileController implements IProfileHandler { else compound = (NBTTagCompound) compound.copy(); playerData.setNBT(compound); + + // Trade data is shared across all profile slots - copy from player's live data + PlayerData liveData = PlayerData.get(player); + if (liveData != null) { + NBTTagCompound tradeCompound = new NBTTagCompound(); + liveData.tradeData.writeToNBT(tradeCompound); + playerData.tradeData.readFromNBT(tradeCompound); + } + return playerData; } diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 9617f4d12..b6010ad86 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -2,9 +2,9 @@ package kamkeel.npcs.controllers; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.addon.DBCAddon; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.network.LargeAbstractPacket; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumSyncAction; @@ -39,11 +39,14 @@ import noppes.npcs.controllers.data.Quest; import noppes.npcs.controllers.data.QuestCategory; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; import java.io.IOException; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -72,7 +75,8 @@ public class SyncController { EnumSyncType.ANVIL_RECIPES, EnumSyncType.CUSTOM_EFFECTS, EnumSyncType.MAGIC, - EnumSyncType.MAGIC_CYCLE + EnumSyncType.MAGIC_CYCLE, + EnumSyncType.CUSTOM_ABILITY }; public static void load() { @@ -88,6 +92,7 @@ public class SyncController { registerCache(EnumSyncType.CUSTOM_EFFECTS, SyncController::customEffectsNBT); registerCache(EnumSyncType.MAGIC, SyncController::magicsNBT); registerCache(EnumSyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT); + registerCache(EnumSyncType.CUSTOM_ABILITY, SyncController::customAbilitiesNBT); } public static void syncPlayer(EntityPlayerMP player) { @@ -323,6 +328,27 @@ public class SyncController { return compound; } + public static NBTTagCompound customAbilitiesNBT() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Map.Entry entry : AbilityController.Instance.getCustomAbilities().entrySet()) { + NBTTagCompound abilityNBT = entry.getValue().writeNBT(); + abilityNBT.setString("CustomAbilityId", entry.getKey()); + list.appendTag(abilityNBT); + } + compound.setTag("Data", list); + return compound; + } + + public static void syncAllCustomAbilities() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.CUSTOM_ABILITY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_ABILITY, payload)); + updateAllPlayerRevisions(EnumSyncType.CUSTOM_ABILITY, payload.getRevision()); + } + public static void syncPlayerData(EntityPlayerMP player, boolean update) { PlayerData data = PlayerData.get(player); if (data != null) { @@ -595,6 +621,21 @@ public class SyncController { ce.customEffectsSync.clear(); break; } + case CUSTOM_ABILITY: { + NBTTagList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound nbt = list.getCompoundTagAt(i); + String uuid = nbt.getString("CustomAbilityId"); + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability != null) { + ability.setId(uuid); + sync.put(uuid, ability); + } + } + AbilityController.Instance.setCustomAbilities(sync); + break; + } } ClientCacheHandler.updateClientRevision(enumSyncType, revision); @@ -843,6 +884,8 @@ public class SyncController { return EnumSet.of(EnumSyncType.ANVIL_RECIPES); case CUSTOM_EFFECTS: return EnumSet.of(EnumSyncType.CUSTOM_EFFECTS); + case CUSTOM_ABILITY: + return EnumSet.of(EnumSyncType.CUSTOM_ABILITY); default: return EnumSet.noneOf(EnumSyncType.class); } diff --git a/src/main/java/kamkeel/npcs/controllers/TelegraphController.java b/src/main/java/kamkeel/npcs/controllers/TelegraphController.java new file mode 100644 index 000000000..5449245a6 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/TelegraphController.java @@ -0,0 +1,392 @@ +package kamkeel.npcs.controllers; + +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.world.World; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.api.ITelegraph; +import noppes.npcs.api.ITelegraphInstance; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.handler.ITelegraphHandler; +import noppes.npcs.scripted.ScriptTelegraph; +import noppes.npcs.scripted.ScriptTelegraphInstance; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.File; +import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Server-side controller for managing telegraphs. + * Provides factory methods for creating telegraphs and spawn methods for sending to clients. + * Also manages saved telegraph presets. + */ +public class TelegraphController implements ITelegraphHandler { + + public static TelegraphController Instance; + + // Saved telegraph presets (name -> Telegraph) + private HashMap savedTelegraphs = new HashMap<>(); + + // Active server-side telegraph instances (for removal tracking) + private ConcurrentHashMap activeInstances = new ConcurrentHashMap<>(); + + public TelegraphController() { + } + + public static void init() { + Instance = new TelegraphController(); + Instance.load(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // INTERNAL SPAWN METHODS + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Spawn a telegraph at a fixed position. + * + * @param telegraph The telegraph configuration + * @param world The world + * @param x X position + * @param y Y position + * @param z Z position + * @param yaw Rotation yaw (for directional telegraphs like LINE/CONE) + * @return The spawned telegraph instance + */ + public TelegraphInstance spawn(Telegraph telegraph, World world, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(world); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToDimension(instance, world.provider.dimensionId); + return instance; + } + + /** + * Spawn a telegraph at a position with default yaw. + */ + public TelegraphInstance spawn(Telegraph telegraph, World world, double x, double y, double z) { + return spawn(telegraph, world, x, y, z, 0); + } + + /** + * Spawn a telegraph that follows an entity. + * + * @param telegraph The telegraph configuration + * @param entity The entity to follow + * @return The spawned telegraph instance + */ + public TelegraphInstance spawn(Telegraph telegraph, Entity entity) { + return spawn(telegraph, entity, 0); + } + + /** + * Spawn a telegraph that follows an entity with yaw. + */ + public TelegraphInstance spawn(Telegraph telegraph, Entity entity, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, entity.posX, entity.posY, entity.posZ, yaw); + instance.setWorld(entity.worldObj); + instance.setEntityIdToFollow(entity.getEntityId()); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToTracking(instance, entity); + return instance; + } + + /** + * Spawn a telegraph to a specific player only. + */ + public TelegraphInstance spawnToPlayer(Telegraph telegraph, EntityPlayerMP player, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(player.worldObj); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.send(instance, player); + return instance; + } + + /** + * Spawn a telegraph to all players on the server. + */ + public TelegraphInstance spawnToAll(Telegraph telegraph, World world, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(world); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToAll(instance); + return instance; + } + + /** + * Spawn a telegraph to all players tracking an entity. + */ + public TelegraphInstance spawnToTracking(Telegraph telegraph, Entity entity, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(entity.worldObj); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToTracking(instance, entity); + return instance; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // REMOVAL METHODS + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Remove a telegraph by instance ID. + */ + public void remove(String instanceId) { + TelegraphInstance instance = activeInstances.remove(instanceId); + if (instance != null && instance.getWorld() != null) { + TelegraphRemovePacket.sendToDimension(instanceId, instance.getDimensionId()); + } + } + + /** + * Remove a telegraph instance. + */ + public void remove(TelegraphInstance instance) { + if (instance != null) { + remove(instance.getInstanceId()); + } + } + + /** + * Remove a telegraph for a specific player only. + */ + public void removeFromPlayer(String instanceId, EntityPlayerMP player) { + activeInstances.remove(instanceId); + TelegraphRemovePacket.send(instanceId, player); + } + + /** + * Get an active telegraph instance by ID. + */ + public TelegraphInstance getActiveInstance(String instanceId) { + return activeInstances.get(instanceId); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // PRESET MANAGEMENT + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Save a telegraph preset (internal use). + * + * @param name The name for the preset + * @param telegraph The telegraph configuration to save + */ + public void saveInternal(String name, Telegraph telegraph) { + if (name == null || name.isEmpty() || telegraph == null) return; + + telegraph.setId(name); + savedTelegraphs.put(name, telegraph); + saveTelegraphFile(name, telegraph); + } + + /** + * Get a saved telegraph preset by name (internal use). + * + * @param name The preset name + * @return The telegraph configuration, or null if not found + */ + public Telegraph getInternal(String name) { + return savedTelegraphs.get(name); + } + + /** + * Delete a saved telegraph preset. + * + * @param name The preset name + */ + public void delete(String name) { + Telegraph removed = savedTelegraphs.remove(name); + if (removed != null) { + File file = new File(getDir(), name + ".json"); + if (file.exists()) { + file.delete(); + } + } + } + + /** + * Check if a preset exists. + */ + public boolean has(String name) { + return savedTelegraphs.containsKey(name); + } + + /** + * Get all saved preset names. + */ + public String[] getSavedNames() { + return savedTelegraphs.keySet().toArray(new String[0]); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // PERSISTENCE + // ═══════════════════════════════════════════════════════════════════════════ + + private File getDir() { + return new File(CustomNpcs.getWorldSaveDirectory(), "telegraphs"); + } + + public void load() { + savedTelegraphs.clear(); + LogWriter.info("Loading telegraph presets..."); + + File dir = getDir(); + if (!dir.exists()) { + dir.mkdirs(); + return; + } + + File[] files = dir.listFiles(); + if (files == null) return; + + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) continue; + try { + Telegraph telegraph = new Telegraph(); + telegraph.readNBT(NBTJsonUtil.LoadFile(file)); + String name = file.getName().substring(0, file.getName().length() - 5); + telegraph.setId(name); + savedTelegraphs.put(name, telegraph); + } catch (Exception e) { + LogWriter.error("Error loading telegraph preset: " + file.getName(), e); + } + } + + LogWriter.info("Loaded " + savedTelegraphs.size() + " telegraph presets."); + } + + private void saveTelegraphFile(String name, Telegraph telegraph) { + try { + File dir = getDir(); + if (!dir.exists()) { + dir.mkdirs(); + } + + File file = new File(dir, name + ".json_new"); + File file2 = new File(dir, name + ".json"); + + NBTJsonUtil.SaveFile(file, telegraph.writeNBT()); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + } catch (Exception e) { + LogWriter.error("Error saving telegraph preset: " + name, e); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ITelegraphHandler INTERFACE IMPLEMENTATION + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public ITelegraph createCircle(float radius) { + return new ScriptTelegraph(Telegraph.circle(radius)); + } + + @Override + public ITelegraph createRing(float outerRadius, float innerRadius) { + return new ScriptTelegraph(Telegraph.ring(outerRadius, innerRadius)); + } + + @Override + public ITelegraph createLine(float length, float width) { + return new ScriptTelegraph(Telegraph.line(length, width)); + } + + @Override + public ITelegraph createCone(float length, float angle) { + return new ScriptTelegraph(Telegraph.cone(length, angle)); + } + + @Override + public ITelegraph createPoint() { + return new ScriptTelegraph(Telegraph.point()); + } + + @Override + public ITelegraph create(String type) { + if (type == null) return null; + try { + TelegraphType telegraphType = TelegraphType.valueOf(type.toUpperCase()); + Telegraph telegraph; + switch (telegraphType) { + case CIRCLE: + telegraph = Telegraph.circle(3.0f); + break; + case RING: + telegraph = Telegraph.ring(5.0f, 2.0f); + break; + case LINE: + telegraph = Telegraph.line(5.0f, 2.0f); + break; + case CONE: + telegraph = Telegraph.cone(5.0f, 45.0f); + break; + case POINT: + telegraph = Telegraph.point(); + break; + default: + telegraph = Telegraph.circle(3.0f); + } + return new ScriptTelegraph(telegraph); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IWorld world, double x, double y, double z) { + return spawn(telegraph, world, x, y, z, 0); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IWorld world, double x, double y, double z, float yaw) { + if (telegraph == null || world == null) return null; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + TelegraphInstance instance = spawn(internal, (World) world.getMCWorld(), x, y, z, yaw); + return new ScriptTelegraphInstance(instance); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IEntity entity) { + return spawn(telegraph, entity, 0); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IEntity entity, float yaw) { + if (telegraph == null || entity == null) return null; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + TelegraphInstance instance = spawn(internal, (Entity) entity.getMCEntity(), yaw); + return new ScriptTelegraphInstance(instance); + } + + @Override + public ITelegraph get(String name) { + Telegraph telegraph = savedTelegraphs.get(name); + return telegraph != null ? new ScriptTelegraph(new Telegraph(telegraph)) : null; + } + + @Override + public void save(String name, ITelegraph telegraph) { + if (name == null || name.isEmpty() || telegraph == null) return; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + saveInternal(name, internal); + } + + @Override + public void remove(ITelegraphInstance instance) { + if (instance != null) { + remove(instance.getInstanceId()); + } + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 4d62a934f..f5214fd5b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -2,9 +2,9 @@ package kamkeel.npcs.controllers.data.ability; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.block.Block; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -15,23 +15,43 @@ import noppes.npcs.DataAbilities; import noppes.npcs.NpcDamageSource; import noppes.npcs.api.INbt; import noppes.npcs.api.ability.IAbility; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.controllers.data.Frame; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.data.PlayerDataScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; +import noppes.npcs.scripted.event.player.PlayerAbilityEvent; + +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import net.minecraft.entity.Entity; +import somehussar.gui.annotationHandling.GuiEditable; + +@GuiEditable public abstract class Ability implements IAbility { // ═══════════════════════════════════════════════════════════════════ // CONFIGURATION (saved to NBT) // ═══════════════════════════════════════════════════════════════════ + @GuiEditable.Field("id") protected String id; + @GuiEditable.Field("name") protected String name; + @GuiEditable.Field("test") protected String typeId; // e.g., "ability.cnpc.slam" (also used as lang key) // Selection @@ -45,16 +65,16 @@ public abstract class Ability implements IAbility { protected float maxRange = 20; // Timing (ticks) - protected int cooldownTicks = 100; + protected int cooldownTicks = 0; // Added to global cooldown after ability completes protected int windUpTicks = 20; - protected int activeTicks = 10; - protected int recoveryTicks = 20; + protected boolean syncWindupWithAnimation = true; + protected int dazedTicks = 80; // Only used when interrupted during WINDUP (if interruptible) // Interruption protected boolean interruptible = true; // Feedback - protected boolean lockMovement = true; + protected LockMovementType lockMovement = LockMovementType.WINDUP; protected int windUpColor = 0x80FF4400; // Telegraph color during wind up protected int activeColor = 0xC0FF0000; // Telegraph warning/active color @@ -63,8 +83,12 @@ public abstract class Ability implements IAbility { protected String activeSound = ""; // Sound to play when active phase starts // Animations (global animation IDs, -1 = none) - protected int windUpAnimationId = -1; // Animation to play during wind up - protected int activeAnimationId = -1; // Animation to play during active phase + protected int windUpAnimationId = -1; // Animation to play during wind up (user animations) + protected int activeAnimationId = -1; // Animation to play during active phase (user animations) + + // Built-in animation names (empty = none, takes priority over IDs if set) + protected String windUpAnimationName = ""; // Built-in animation name for wind up + protected String activeAnimationName = ""; // Built-in animation name for active phase // Telegraph configuration protected boolean showTelegraph = true; @@ -74,13 +98,21 @@ public abstract class Ability implements IAbility { // Custom data for external mods protected NBTTagCompound customData = new NBTTagCompound(); + // User type restriction + protected UserType allowedBy = UserType.BOTH; + + // Cooldown override + protected boolean ignoreCooldown = false; + + // Configurable potion effects + protected List effects = new ArrayList<>(); + // ═══════════════════════════════════════════════════════════════════ // EXECUTION STATE (not saved, reset each combat) // ═══════════════════════════════════════════════════════════════════ protected transient AbilityPhase phase = AbilityPhase.IDLE; protected transient int currentTick = 0; - protected transient long cooldownEndTime = 0; protected transient EntityLivingBase currentTarget; protected transient long executionStartTime; protected transient TelegraphInstance telegraphInstance; @@ -89,170 +121,357 @@ public abstract class Ability implements IAbility { // ABSTRACT METHODS // ═══════════════════════════════════════════════════════════════════ - /** Called first tick of ACTIVE phase */ - public abstract void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world); + /** + * Called first tick of ACTIVE phase + */ + public abstract void onExecute(EntityLivingBase caster, EntityLivingBase target, World world); - /** Called every tick of ACTIVE phase */ - public abstract void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick); + /** + * Called every tick of ACTIVE phase + */ + public abstract void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick); - /** Write type-specific config to NBT */ + /** + * Write type-specific config to NBT + */ public abstract void writeTypeNBT(NBTTagCompound nbt); - /** Read type-specific config from NBT */ + /** + * Read type-specific config from NBT + */ public abstract void readTypeNBT(NBTTagCompound nbt); // ═══════════════════════════════════════════════════════════════════ // OPTIONAL OVERRIDES // ═══════════════════════════════════════════════════════════════════ - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {} - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {} - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {} + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + } + + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + } + + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } /** * Apply damage to an entity with ability hit event support. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability + * @param caster The entity executing the ability (NPC or Player) * @param hitEntity The entity being hit - * @param damage The damage amount + * @param damage The damage amount * @param knockback The horizontal knockback * @return true if damage was applied (not cancelled), false if cancelled */ - protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, + protected boolean applyAbilityDamage(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback) { - return applyAbilityDamage(npc, hitEntity, damage, knockback, 0.0f); + double dx = hitEntity.posX - caster.posX; + double dz = hitEntity.posZ - caster.posZ; + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, 0.0f, dx, dz); } /** - * Backward-compatible signature (vertical knockback is ignored). + * Apply damage to an entity with ability hit event support and vertical knockback. + * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. + * + * @param caster The entity executing the ability (NPC or Player) + * @param hitEntity The entity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback + * @param knockbackUp The vertical knockback + * @return true if damage was applied (not cancelled), false if cancelled */ - protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, + protected boolean applyAbilityDamage(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { - DataAbilities dataAbilities = npc.abilities; - AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( - this, currentTarget, hitEntity, damage, knockback, knockbackUp); - - if (event == null) { - return false; // Cancelled - } - - // Apply damage with potentially modified values - float finalDamage = event.getDamage(); - float finalKnockback = event.getKnockback(); - - // Apply damage - if (finalDamage > 0) { - hitEntity.attackEntityFrom(new NpcDamageSource("mob", npc), finalDamage); - } - - // Apply knockback if any - if (finalKnockback > 0) { - double dx = hitEntity.posX - npc.posX; - double dz = hitEntity.posZ - npc.posZ; - applyNpcKnockback(hitEntity, dx, dz, finalKnockback); - } - - return true; + double dx = hitEntity.posX - caster.posX; + double dz = hitEntity.posZ - caster.posZ; + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, knockbackUp, dx, dz); } /** * Apply damage to an entity with ability hit event support and custom knockback direction. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability - * @param hitEntity The entity being hit - * @param damage The damage amount - * @param knockback The horizontal knockback - * @param knockbackDirX The X component of knockback direction (normalized) - * @param knockbackDirZ The Z component of knockback direction (normalized) + * @param caster The entity executing the ability (NPC or Player) + * @param hitEntity The entity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback + * @param knockbackDirX The X component of knockback direction + * @param knockbackDirZ The Z component of knockback direction * @return true if damage was applied (not cancelled), false if cancelled */ - protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity, - float damage, float knockback, - double knockbackDirX, double knockbackDirZ) { - DataAbilities dataAbilities = npc.abilities; - AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( - this, currentTarget, hitEntity, damage, knockback, 0.0f); + protected boolean applyAbilityDamageWithDirection(EntityLivingBase caster, EntityLivingBase hitEntity, + float damage, float knockback, + double knockbackDirX, double knockbackDirZ) { + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, 0.0f, knockbackDirX, knockbackDirZ); + } - if (event == null) { - return false; // Cancelled + /** + * Internal unified damage application method. + * All public damage methods delegate to this. + * Supports both NPC and Player casters. + */ + private boolean applyAbilityDamageInternal(EntityLivingBase caster, EntityLivingBase hitEntity, + float damage, float knockback, float knockbackUp, + double knockbackDirX, double knockbackDirZ) { + // Fire hit event for NPC casters (NPC event system) + if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + DataAbilities dataAbilities = npc.abilities; + AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( + this, currentTarget, hitEntity, damage, knockback, knockbackUp); + + if (event == null) { + return false; // Cancelled + } + + damage = event.getDamage(); + knockback = event.getKnockback(); + knockbackUp = event.getKnockbackUp(); } + // Fire hit event for Player casters (Player event system) + else if (caster instanceof EntityPlayer && ScriptController.Instance != null) { + EntityPlayer player = (EntityPlayer) caster; + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); + if (handler != null) { + IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); + PlayerAbilityEvent.HitEvent event = new PlayerAbilityEvent.HitEvent( + iPlayer, this, currentTarget, hitEntity, damage, knockback, knockbackUp); - // Apply damage with potentially modified values - float finalDamage = event.getDamage(); - float finalKnockback = event.getKnockback(); + if (noppes.npcs.EventHooks.onPlayerAbilityHit(handler, event)) { + return false; // Cancelled + } + + damage = event.getDamage(); + knockback = event.getKnockback(); + knockbackUp = event.getKnockbackUp(); + } + } // Apply damage - if (finalDamage > 0) { - hitEntity.attackEntityFrom(DamageSource.causeMobDamage(npc), finalDamage); + 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); + } } - // Apply knockback in specified direction - if (finalKnockback > 0) { - applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback); + // Apply knockback if any + if (knockback > 0 || knockbackUp > 0) { + applyKnockback(hitEntity, knockbackDirX, knockbackDirZ, knockback, knockbackUp); } return true; } - private void applyNpcKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback) { + private void applyKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback, float knockbackUp) { double len = Math.sqrt(dirX * dirX + dirZ * dirZ); - if (len <= 0) { - return; + double x = 0; + double z = 0; + if (len > 0) { + x = (dirX / len) * knockback * 0.5; + z = (dirZ / len) * knockback * 0.5; } - double x = (dirX / len) * knockback * 0.5; - double z = (dirZ / len) * knockback * 0.5; - hitEntity.addVelocity(x, 0.1D, z); + double y = knockbackUp > 0 ? knockbackUp : 0.1D; + hitEntity.addVelocity(x, y, z); hitEntity.velocityChanged = true; } - public float getTelegraphRadius() { return 5.0f; } - public float getTelegraphLength() { return 5.0f; } - public float getTelegraphWidth() { return 2.0f; } - public float getTelegraphAngle() { return 45.0f; } + + /** + * Applies all configured effects to the given entity. + * Call this after damage is applied to the target. + * + * @param entity The entity to apply effects to + */ + protected void applyEffects(EntityLivingBase entity) { + if (entity == null || effects.isEmpty()) { + return; + } + for (AbilityEffect effect : effects) { + effect.apply(entity); + } + } + + public float getTelegraphRadius() { + return 5.0f; + } + + public float getTelegraphLength() { + return 5.0f; + } + + public float getTelegraphWidth() { + return 2.0f; + } + + public float getTelegraphAngle() { + return 45.0f; + } /** * Returns true if this ability controls NPC movement during ACTIVE phase. * When true, AI pathfinding will be blocked during ability execution. * Override in abilities that move the NPC (Charge, Dash, Slam, etc.) */ - public boolean hasAbilityMovement() { return false; } + public boolean hasAbilityMovement() { + return false; + } /** * Returns true if the targeting mode for this ability type is locked and cannot be changed. * Most abilities have fixed targeting (e.g., Heal is always SELF, Charge is always AGGRO_TARGET). * Override and return false for abilities with flexible targeting. */ - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } /** * Returns the allowed targeting modes for this ability, or null if all modes are allowed. * Override in abilities that only support specific targeting modes. */ - public TargetingMode[] getAllowedTargetingModes() { return null; } + public TargetingMode[] getAllowedTargetingModes() { + return null; + } /** * Returns true if the telegraph type for this ability is locked and cannot be changed. * Telegraph type is inherent to how the ability works (e.g., Charge uses LINE, Slam uses CIRCLE). */ - public boolean isTelegraphTypeLocked() { return true; } + public boolean isTelegraphTypeLocked() { + return true; + } + + /** + * Returns the declarative field definitions for this ability's GUI. + * Override in subclasses to declare type-specific and visual fields. + * The GUI rendering engine in SubGuiAbilityConfig uses this list to + * automatically build the Type and Visual tabs. + */ + public List getFieldDefinitions() { + return Collections.emptyList(); + } + + /** + * Returns field definitions for all base Ability fields (General, Target, Effects tabs). + */ + @SideOnly(Side.CLIENT) + public List getBaseFieldDefinitions() { + List defs = new ArrayList<>(); + + // General tab + defs.add(FieldDef.stringField("gui.name", this::getName, this::setName) + .tab(TabTarget.GENERAL)); + defs.add(FieldDef.intField("ability.weight", this::getWeight, this::setWeight) + .tab(TabTarget.GENERAL).range(1, 1000).column(ColumnHint.LEFT)); + defs.add(FieldDef.boolField("gui.enabled", this::isEnabled, this::setEnabled) + .tab(TabTarget.GENERAL).column(ColumnHint.RIGHT)); + defs.add(FieldDef.section("ability.section.timing").tab(TabTarget.GENERAL)); + defs.add(FieldDef.intField("ability.windUpTicks", this::getRawWindUpTicks, this::setWindUpTicks) + .tab(TabTarget.GENERAL).range(0, 1000).column(ColumnHint.LEFT) + .visibleWhen(() -> !isSyncWindupWithAnimation())); + defs.add(FieldDef.labelField("ability.windUpTicks", () -> getWindUpTicks() + "t") + .tab(TabTarget.GENERAL).column(ColumnHint.LEFT) + .visibleWhen(this::isSyncWindupWithAnimation)); + defs.add(FieldDef.boolField("ability.syncWindup", this::isSyncWindupWithAnimation, this::setSyncWindupWithAnimation) + .tab(TabTarget.GENERAL).hover("ability.hover.sync").column(ColumnHint.RIGHT) + .visibleWhen(this::hasWindUpAnimation)); + defs.add(FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks) + .tab(TabTarget.GENERAL).range(0, 10000)); + defs.add(FieldDef.section("ability.section.movement").tab(TabTarget.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(TabTarget.GENERAL).hover("ability.hover.lockMove")); + defs.add(FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible) + .tab(TabTarget.GENERAL).hover("ability.hover.interruptible").column(ColumnHint.LEFT)); + defs.add(FieldDef.intField("ability.dazedTicks", this::getDazedTicks, this::setDazedTicks) + .tab(TabTarget.GENERAL).range(0, 1000) + .visibleWhen(this::isInterruptible).column(ColumnHint.RIGHT)); + + // Target tab + defs.add(FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)) + .tab(TabTarget.TARGET).range(0, 100).column(ColumnHint.LEFT)); + defs.add(FieldDef.intField("ability.maxRange", () -> (int) getMaxRange(), v -> setMaxRange(v)) + .tab(TabTarget.TARGET).range(1, 100).column(ColumnHint.RIGHT)); + if (!isTargetingModeLocked()) { + defs.add(FieldDef.enumField("ability.targetingMode", TargetingMode.class, + this::getTargetingMode, this::setTargetingMode) + .tab(TabTarget.TARGET).hover("ability.hover.targeting")); + } + + // Effects tab - Sounds + defs.add(FieldDef.section("ability.section.sounds").tab(TabTarget.EFFECTS)); + defs.add(FieldDef.soundSubGui("ability.windUpSound", this::getWindUpSound, this::setWindUpSound) + .tab(TabTarget.EFFECTS)); + defs.add(FieldDef.soundSubGui("ability.activeSound", this::getActiveSound, this::setActiveSound) + .tab(TabTarget.EFFECTS)); + // Effects tab - Animations + defs.add(FieldDef.section("ability.section.animations").tab(TabTarget.EFFECTS)); + defs.add(FieldDef.animSubGui("ability.windUpAnimation", + this::getWindUpAnimationId, this::setWindUpAnimationId, + this::getWindUpAnimationName, this::setWindUpAnimationName) + .tab(TabTarget.EFFECTS)); + defs.add(FieldDef.animSubGui("ability.activeAnimation", + this::getActiveAnimationId, this::setActiveAnimationId, + this::getActiveAnimationName, this::setActiveAnimationName) + .tab(TabTarget.EFFECTS)); + + // Effects tab - Telegraph + TelegraphType tType = getTelegraphType(); + if (tType != null && tType != TelegraphType.NONE) { + defs.add(FieldDef.section("ability.section.telegraph").tab(TabTarget.EFFECTS) + .tooltip("telegraph." + tType.name().toLowerCase())); + defs.add(FieldDef.boolField("ability.showTelegraph", this::isShowTelegraph, this::setShowTelegraph) + .tab(TabTarget.EFFECTS).hover("ability.hover.showTelegraph")); + defs.add(FieldDef.colorSubGui("ability.windUpColor", this::getWindUpColor, this::setWindUpColor) + .tab(TabTarget.EFFECTS).visibleWhen(this::isShowTelegraph)); + defs.add(FieldDef.colorSubGui("ability.activeColor", this::getActiveColor, this::setActiveColor) + .tab(TabTarget.EFFECTS).visibleWhen(this::isShowTelegraph)); + } + + return defs; + } + + /** + * Returns ALL field definitions: base (General/Target/Effects) + type-specific (Type/Visual). + */ + @SideOnly(Side.CLIENT) + public final List getAllFieldDefinitions() { + List all = new ArrayList<>(getBaseFieldDefinitions()); + all.addAll(getFieldDefinitions()); + return all; + } /** * Returns true if this ability type has custom settings that need a Type-specific tab. * Override to return true if the ability has settings beyond the base Ability fields. */ - public boolean hasTypeSettings() { return false; } + public boolean hasTypeSettings() { + return false; + } /** * Get the number of rows needed for type-specific settings in the GUI. * Each row is approximately 24 pixels. Used for GUI layout. */ - public int getTypeSettingsRowCount() { return 0; } + public int getTypeSettingsRowCount() { + return 0; + } /** * Creates the GUI for configuring this ability. * Override this method to provide a custom GUI subclass for type-specific settings. - * + *

* Third-party mods can extend SubGuiAbilityConfig and override initTypeTab(), * handleTypeButton(), and handleTypeTextField() to provide custom type settings. * @@ -268,37 +487,37 @@ public abstract class Ability implements IAbility { * Create a telegraph instance for this ability. * Override for custom telegraph shapes. * - * @param npc The caster + * @param caster The caster * @param target The target (for position calculation) * @return The telegraph instance, or null if no telegraph */ - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE) { return null; } Telegraph telegraph; double x, y, z; - float yaw = npc.rotationYaw; + float yaw = caster.rotationYaw; // Determine position based on targeting mode - boolean positionAtNpc = targetingMode == TargetingMode.AOE_SELF || - targetingMode == TargetingMode.SELF || - telegraphType == TelegraphType.LINE || - telegraphType == TelegraphType.CONE; + boolean positionAtCaster = targetingMode == TargetingMode.AOE_SELF || + targetingMode == TargetingMode.SELF || + telegraphType == TelegraphType.LINE || + telegraphType == TelegraphType.CONE; - if (positionAtNpc) { - x = npc.posX; - y = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ); - z = npc.posZ; + if (positionAtCaster) { + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; } else if (target != null) { x = target.posX; - y = findGroundLevel(npc.worldObj, target.posX, target.posY, target.posZ); + y = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ); z = target.posZ; } else { - x = npc.posX; - y = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ); - z = npc.posZ; + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; } // Create telegraph based on type @@ -329,12 +548,17 @@ public abstract class Ability implements IAbility { telegraph.setHeightOffset(telegraphHeightOffset); TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); - instance.setCasterEntityId(npc.getEntityId()); + instance.setCasterEntityId(caster.getEntityId()); // Set entity to follow based on targeting mode - if (positionAtNpc) { - // AOE_SELF abilities: telegraph follows NPC during windup - instance.setEntityIdToFollow(npc.getEntityId()); + if (positionAtCaster) { + // AOE_SELF abilities: telegraph follows caster during windup + instance.setEntityIdToFollow(caster.getEntityId()); + + // For LINE/CONE telegraphs: track target direction during windup + if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) { + instance.setTargetEntityId(target.getEntityId()); + } } else if (target != null) { // AOE_TARGET abilities: telegraph follows target during windup instance.setEntityIdToFollow(target.getEntityId()); @@ -347,13 +571,13 @@ public abstract class Ability implements IAbility { * Find the ground level at a given position. * Searches downward from the given Y to find a solid block. * - * @param world The world - * @param x X coordinate + * @param world The world + * @param x X coordinate * @param startY Starting Y coordinate (entity feet position) - * @param z Z coordinate + * @param z Z coordinate * @return The Y coordinate of the ground surface */ - protected double findGroundLevel(World world, double x, double startY, double z) { + public static double findGroundLevel(World world, double x, double startY, double z) { if (world == null) return startY; int blockX = (int) Math.floor(x); @@ -373,6 +597,43 @@ public abstract class Ability implements IAbility { return startY; } + /** + * Calculates offset position near the given coordinates. + * Used to place effects near a target rather than exactly on them. + * + * @param baseX Base X coordinate + * @param baseY Base Y coordinate + * @param baseZ Base Z coordinate + * @param minOffset Minimum offset distance + * @param maxOffset Maximum offset distance + * @param randomOffset Whether to use random offset within range, or fixed at max + * @param random Random instance to use + * @return Array of [x, y, z] with offset applied + */ + public static double[] calculateOffsetPosition(double baseX, double baseY, double baseZ, + float minOffset, float maxOffset, + boolean randomOffset, java.util.Random random) { + if (maxOffset <= 0) { + return new double[]{baseX, baseY, baseZ}; + } + + double offsetDist; + double offsetAngle; + + if (randomOffset) { + offsetDist = minOffset + random.nextDouble() * (maxOffset - minOffset); + offsetAngle = random.nextDouble() * Math.PI * 2; + } else { + offsetDist = maxOffset; + offsetAngle = random.nextDouble() * Math.PI * 2; + } + + double offsetX = Math.cos(offsetAngle) * offsetDist; + double offsetZ = Math.sin(offsetAngle) * offsetDist; + + return new double[]{baseX + offsetX, baseY, baseZ + offsetZ}; + } + /** * Get the current telegraph instance. */ @@ -391,7 +652,9 @@ public abstract class Ability implements IAbility { // EXECUTION LOGIC (called by CombatHandler) // ═══════════════════════════════════════════════════════════════════ - /** Start executing this ability */ + /** + * Start executing this ability + */ public void start(EntityLivingBase target) { this.phase = AbilityPhase.WINDUP; this.currentTick = 0; @@ -399,12 +662,18 @@ public abstract class Ability implements IAbility { this.executionStartTime = System.currentTimeMillis(); } - /** Tick the ability. Returns true if phase changed. */ + /** + * Tick the ability forward. Called every game tick while executing. + *

+ * Normal flow: WINDUP -> ACTIVE -> IDLE + * Interrupted flow: WINDUP -> DAZED -> IDLE (dazed only on interrupt during WINDUP) + * + * @return true if phase changed this tick + */ public boolean tick() { if (phase == AbilityPhase.IDLE) return false; currentTick++; - AbilityPhase oldPhase = phase; switch (phase) { case WINDUP: @@ -415,16 +684,15 @@ public abstract class Ability implements IAbility { } break; case ACTIVE: - if (currentTick >= activeTicks) { - phase = AbilityPhase.RECOVERY; - currentTick = 0; - return true; - } + // Abilities control their own completion by calling signalCompletion() + // No auto-complete - each ability determines when it's done break; - case RECOVERY: - if (currentTick >= recoveryTicks) { + case DAZED: + // Dazed phase is only entered on interrupt during WINDUP + // NPC cannot attack during this time + if (currentTick >= dazedTicks) { phase = AbilityPhase.IDLE; - startCooldown(); + currentTick = 0; return true; } break; @@ -432,25 +700,45 @@ public abstract class Ability implements IAbility { return false; } - /** Interrupt this ability */ + /** + * Signal that the ability has completed its active phase. + * Called by abilities when they are done. + * For example, AbilitySlam calls this when the NPC lands. + * + * @return true if phase changed to IDLE + */ + public boolean signalCompletion() { + if (phase == AbilityPhase.ACTIVE) { + cleanup(); + phase = AbilityPhase.IDLE; + currentTick = 0; + return true; + } + return false; + } + + /** + * Interrupt this ability. Transitions to DAZED phase if interruptible during WINDUP. + * Called when NPC takes direct damage during WINDUP. + */ public void interrupt() { - phase = AbilityPhase.IDLE; - currentTick = 0; + cleanup(); + if (interruptible && phase == AbilityPhase.WINDUP) { + // Interrupted during windup - go to dazed state + phase = AbilityPhase.DAZED; + currentTick = 0; + } else { + // Force stop - go directly to IDLE + phase = AbilityPhase.IDLE; + currentTick = 0; + } currentTarget = null; telegraphInstance = null; } - /** Start cooldown timer */ - public void startCooldown() { - cooldownEndTime = System.currentTimeMillis() + (cooldownTicks * 50L); - } - - /** Check if on cooldown */ - public boolean isOnCooldown() { - return System.currentTimeMillis() < cooldownEndTime; - } - - /** Check if executing */ + /** + * Check if executing + */ public boolean isExecuting() { return phase != AbilityPhase.IDLE; } @@ -487,38 +775,136 @@ public abstract class Ability implements IAbility { return true; } - /** Reset all execution state (called on combat end) */ + /** + * Reset all execution state (called on combat end, NPC death, target lost, etc.) + */ public void reset() { + cleanup(); phase = AbilityPhase.IDLE; currentTick = 0; currentTarget = null; - cooldownEndTime = 0; telegraphInstance = null; } + /** + * Cleanup any resources created by this ability. + * Override this to kill spawned entities, cancel telegraphs, etc. + * Called when ability ends for any reason (completion, interrupt, reset, NPC death). + */ + public void cleanup() { + // Override in subclasses to clean up spawned entities, etc. + } + + // ═══════════════════════════════════════════════════════════════════ + // CLIENT-SIDE PREVIEW METHODS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Get total active phase duration for preview (in ticks). + * Override in subclasses to provide accurate preview duration. + * + * @return Duration of active phase in ticks (default: 40 = 2 seconds) + */ + public int getPreviewActiveDuration() { + return 40; + } + + /** + * Called during preview windup tick (client-side only). + * Override for visual updates during windup preview. + * Do NOT apply damage or world effects here. + * + * @param npc The preview NPC + * @param tick Current tick within windup phase + */ + @SideOnly(Side.CLIENT) + public void onPreviewWindUpTick(EntityNPCInterface npc, int tick) { + // Default: no-op. Override in subclasses for visual effects. + } + + /** + * Called during preview active tick (client-side only). + * Override for visual updates during active preview. + * Do NOT apply damage or world effects here. + * + * @param npc The preview NPC + * @param tick Current tick within active phase + */ + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + // Default: no-op. Override in subclasses for visual effects. + } + + /** + * Create a preview entity for GUI display (client-side only). + * The entity should be in preview mode and not apply damage. + * Override in abilities that spawn entities (Beam, Orb, Disc, etc.) + * + * @param npc The preview NPC + * @return Preview entity, or null if ability doesn't spawn entities + */ + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + return null; // Override in entity-spawning abilities + } + + /** + * Whether to spawn preview entity during WINDUP phase (true) or ACTIVE phase (false). + * Most abilities (Orb, Beam, Disc) spawn during windup for charging effect. + * Laser spawns at active phase since it has no charging state. + */ + public boolean spawnPreviewDuringWindup() { + return true; // Default: spawn during windup for charging + } + + // ── Preview target (set by executor for abilities that need targeting) ── + + protected transient EntityLivingBase previewTarget; + + /** + * Set the fake target entity for preview mode. + * Called by AbilityPreviewExecutor before starting preview. + */ + public void setPreviewTarget(EntityLivingBase target) { + this.previewTarget = target; + } + + public EntityLivingBase getPreviewTarget() { + return previewTarget; + } + + /** + * Called once when transitioning from WINDUP to ACTIVE in preview mode. + * Override in movement-based abilities to initiate movement. + */ + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + // Default: no-op. Override in movement abilities. + } + // ═══════════════════════════════════════════════════════════════════ // CONDITION CHECKING // ═══════════════════════════════════════════════════════════════════ - public boolean checkConditions(EntityNPCInterface npc, EntityLivingBase target) { + public boolean checkConditions(EntityLivingBase caster, EntityLivingBase target) { for (Condition c : conditions) { - if (!c.check(npc, target)) return false; + if (!c.check(caster, target)) return false; } return true; } - /** Full eligibility check */ - public boolean canUse(EntityNPCInterface npc, EntityLivingBase target) { - if (!enabled) return false; - if (isOnCooldown()) return false; - if (isExecuting()) return false; - - float distance = npc.getDistanceToEntity(target); - if (distance < minRange || distance > maxRange) return false; - - return checkConditions(npc, target); + /** + * Check conditions for a player caster, skipping target-requiring conditions. + */ + public boolean checkConditionsForPlayer(EntityLivingBase caster) { + for (Condition c : conditions) { + if (c.requiresTarget()) continue; + if (!c.check(caster, null)) return false; + } + return true; } + // ═══════════════════════════════════════════════════════════════════ // NBT (config only, execution state is transient) // ═══════════════════════════════════════════════════════════════════ @@ -535,26 +921,37 @@ public abstract class Ability implements IAbility { nbt.setFloat("maxRange", maxRange); nbt.setInteger("cooldown", cooldownTicks); nbt.setInteger("windUp", windUpTicks); - nbt.setInteger("active", activeTicks); - nbt.setInteger("recovery", recoveryTicks); + nbt.setBoolean("syncWindup", syncWindupWithAnimation); + nbt.setInteger("recovery", dazedTicks); nbt.setBoolean("interruptible", interruptible); - nbt.setBoolean("lockMovement", lockMovement); + nbt.setInteger("lockMovement", lockMovement.ordinal()); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); nbt.setString("windUpSound", windUpSound); nbt.setString("activeSound", activeSound); nbt.setInteger("windUpAnimationId", windUpAnimationId); nbt.setInteger("activeAnimationId", activeAnimationId); + nbt.setString("windUpAnimationName", windUpAnimationName); + nbt.setString("activeAnimationName", activeAnimationName); nbt.setBoolean("showTelegraph", showTelegraph); nbt.setString("telegraphType", telegraphType.name()); nbt.setFloat("telegraphHeightOffset", telegraphHeightOffset); nbt.setTag("customData", customData); + nbt.setInteger("allowedBy", allowedBy.ordinal()); + nbt.setBoolean("ignoreCooldown", ignoreCooldown); // Conditions NBTTagList condList = new NBTTagList(); for (Condition c : conditions) condList.appendTag(c.writeNBT()); nbt.setTag("conditions", condList); + // Effects + NBTTagList effectList = new NBTTagList(); + for (AbilityEffect effect : effects) { + effectList.appendTag(effect.writeNBT()); + } + nbt.setTag("effects", effectList); + // Type-specific NBTTagCompound typeNBT = new NBTTagCompound(); writeTypeNBT(typeNBT); @@ -574,40 +971,18 @@ public abstract class Ability implements IAbility { maxRange = nbt.getFloat("maxRange"); cooldownTicks = nbt.getInteger("cooldown"); windUpTicks = nbt.getInteger("windUp"); - activeTicks = nbt.getInteger("active"); - recoveryTicks = nbt.getInteger("recovery"); + syncWindupWithAnimation = !nbt.hasKey("syncWindup") || nbt.getBoolean("syncWindup"); + dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); - lockMovement = nbt.getBoolean("lockMovement"); - // Support old telegraphColor key for backwards compatibility - if (nbt.hasKey("windUpColor")) { - windUpColor = nbt.getInteger("windUpColor"); - } else if (nbt.hasKey("telegraphColor")) { - windUpColor = nbt.getInteger("telegraphColor"); - } - if (nbt.hasKey("activeColor")) { - activeColor = nbt.getInteger("activeColor"); - } else { - // Default active color to windUpColor with higher alpha - activeColor = (windUpColor & 0x00FFFFFF) | 0xC0000000; - } - // Sound fields (with backwards compatibility for old castSound) - if (nbt.hasKey("windUpSound")) { - windUpSound = nbt.getString("windUpSound"); - } else if (nbt.hasKey("castSound")) { - windUpSound = nbt.getString("castSound"); // Migrate old field - } else { - windUpSound = ""; - } - activeSound = nbt.hasKey("activeSound") ? nbt.getString("activeSound") : ""; - // Animation fields (with backwards compatibility for old animationId) - if (nbt.hasKey("windUpAnimationId")) { - windUpAnimationId = nbt.getInteger("windUpAnimationId"); - } else if (nbt.hasKey("animationId")) { - windUpAnimationId = nbt.getInteger("animationId"); // Migrate old field - } else { - windUpAnimationId = -1; - } + lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement")); + windUpColor = nbt.getInteger("windUpColor"); + activeColor = nbt.getInteger("activeColor"); + windUpSound = nbt.getString("windUpSound"); + activeSound = nbt.getString("activeSound"); + windUpAnimationId = nbt.hasKey("windUpAnimationId") ? nbt.getInteger("windUpAnimationId") : -1; activeAnimationId = nbt.hasKey("activeAnimationId") ? nbt.getInteger("activeAnimationId") : -1; + windUpAnimationName = nbt.getString("windUpAnimationName"); + activeAnimationName = nbt.getString("activeAnimationName"); showTelegraph = !nbt.hasKey("showTelegraph") || nbt.getBoolean("showTelegraph"); if (nbt.hasKey("telegraphType")) { try { @@ -618,6 +993,8 @@ public abstract class Ability implements IAbility { } telegraphHeightOffset = nbt.hasKey("telegraphHeightOffset") ? nbt.getFloat("telegraphHeightOffset") : 0.1f; customData = nbt.getCompoundTag("customData"); + allowedBy = nbt.hasKey("allowedBy") ? UserType.fromOrdinal(nbt.getInteger("allowedBy")) : UserType.BOTH; + ignoreCooldown = nbt.hasKey("ignoreCooldown") && nbt.getBoolean("ignoreCooldown"); // Conditions conditions.clear(); @@ -627,6 +1004,18 @@ public abstract class Ability implements IAbility { if (c != null) conditions.add(c); } + // Effects + effects.clear(); + if (nbt.hasKey("effects")) { + NBTTagList effectList = nbt.getTagList("effects", 10); + for (int i = 0; i < effectList.tagCount(); i++) { + AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i)); + if (effect != null && effect.isValid()) { + effects.add(effect); + } + } + } + readTypeNBT(nbt.getCompoundTag("typeData")); } @@ -634,115 +1023,404 @@ public abstract class Ability implements IAbility { // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ - public String getId() { return id; } - public void setId(String id) { this.id = id; } + public String getId() { + return id; + } - public String getName() { return name; } - public void setName(String name) { this.name = name; } + public void setId(String id) { + this.id = id; + } - public String getTypeId() { return typeId; } + public String getName() { + return name; + } - public int getWeight() { return weight; } - public void setWeight(int weight) { this.weight = weight; } + public void setName(String name) { + this.name = name; + } - public boolean isEnabled() { return enabled; } - public void setEnabled(boolean enabled) { this.enabled = enabled; } + public String getTypeId() { + return typeId; + } - public TargetingMode getTargetingMode() { return targetingMode; } - public void setTargetingMode(TargetingMode targetingMode) { this.targetingMode = targetingMode; } + public int getWeight() { + return weight; + } - public float getMinRange() { return minRange; } - public void setMinRange(float minRange) { this.minRange = minRange; } + public void setWeight(int weight) { + this.weight = weight; + } - public float getMaxRange() { return maxRange; } - public void setMaxRange(float maxRange) { this.maxRange = maxRange; } + public boolean isEnabled() { + return enabled; + } - public int getCooldownTicks() { return cooldownTicks; } - public void setCooldownTicks(int cooldownTicks) { this.cooldownTicks = cooldownTicks; } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } - public int getWindUpTicks() { return windUpTicks; } - public void setWindUpTicks(int windUpTicks) { this.windUpTicks = windUpTicks; } + public TargetingMode getTargetingMode() { + return targetingMode; + } - public int getActiveTicks() { return activeTicks; } - public void setActiveTicks(int activeTicks) { this.activeTicks = activeTicks; } + public void setTargetingMode(TargetingMode targetingMode) { + this.targetingMode = targetingMode; + } - public int getRecoveryTicks() { return recoveryTicks; } - public void setRecoveryTicks(int recoveryTicks) { this.recoveryTicks = recoveryTicks; } + public float getMinRange() { + return minRange; + } - public boolean isInterruptible() { return interruptible; } - public void setInterruptible(boolean interruptible) { this.interruptible = interruptible; } + public void setMinRange(float minRange) { + this.minRange = minRange; + } - public boolean isLockMovement() { return lockMovement; } - public void setLockMovement(boolean lockMovement) { this.lockMovement = lockMovement; } + public float getMaxRange() { + return maxRange; + } - public int getWindUpColor() { return windUpColor; } - public void setWindUpColor(int windUpColor) { this.windUpColor = windUpColor; } + public void setMaxRange(float maxRange) { + this.maxRange = maxRange; + } - public int getActiveColor() { return activeColor; } - public void setActiveColor(int activeColor) { this.activeColor = activeColor; } + public int getCooldownTicks() { + return cooldownTicks; + } - /** @deprecated Use getWindUpColor() instead */ - @Deprecated - public int getTelegraphColor() { return windUpColor; } - /** @deprecated Use setWindUpColor() instead */ - @Deprecated - public void setTelegraphColor(int telegraphColor) { this.windUpColor = telegraphColor; } + public void setCooldownTicks(int cooldownTicks) { + this.cooldownTicks = Math.max(0, cooldownTicks); + } - public String getWindUpSound() { return windUpSound; } - public void setWindUpSound(String windUpSound) { this.windUpSound = windUpSound; } + public int getWindUpTicks() { + return calculateWindupFromAnimation(); + } - public String getActiveSound() { return activeSound; } - public void setActiveSound(String activeSound) { this.activeSound = activeSound; } + public int getRawWindUpTicks() { + return windUpTicks; + } - public int getWindUpAnimationId() { return windUpAnimationId; } - public void setWindUpAnimationId(int windUpAnimationId) { this.windUpAnimationId = windUpAnimationId; } + public void setWindUpTicks(int windUpTicks) { + this.windUpTicks = Math.max(0, windUpTicks); + } - public int getActiveAnimationId() { return activeAnimationId; } - public void setActiveAnimationId(int activeAnimationId) { this.activeAnimationId = activeAnimationId; } + public boolean isSyncWindupWithAnimation() { + return syncWindupWithAnimation; + } - /** @deprecated Use getWindUpSound() instead */ - @Deprecated - public String getCastSound() { return windUpSound; } - /** @deprecated Use setWindUpSound() instead */ - @Deprecated - public void setCastSound(String castSound) { this.windUpSound = castSound; } + public boolean hasWindUpAnimation() { + return (windUpAnimationName != null && !windUpAnimationName.isEmpty()) || windUpAnimationId >= 0; + } - /** @deprecated Use getWindUpAnimationId() instead */ - @Deprecated - public int getAnimationId() { return windUpAnimationId; } - /** @deprecated Use setWindUpAnimationId() instead */ - @Deprecated - public void setAnimationId(int animationId) { this.windUpAnimationId = animationId; } + public void setSyncWindupWithAnimation(boolean syncWindupWithAnimation) { + this.syncWindupWithAnimation = syncWindupWithAnimation; + } - public boolean isShowTelegraph() { return showTelegraph; } - public void setShowTelegraph(boolean showTelegraph) { this.showTelegraph = showTelegraph; } + public int getDazedTicks() { + return dazedTicks; + } - public TelegraphType getTelegraphType() { return telegraphType; } - public void setTelegraphType(TelegraphType telegraphType) { this.telegraphType = telegraphType; } + public void setDazedTicks(int dazedTicks) { + this.dazedTicks = Math.max(0, dazedTicks); + } - public float getTelegraphHeightOffset() { return telegraphHeightOffset; } - public void setTelegraphHeightOffset(float telegraphHeightOffset) { this.telegraphHeightOffset = telegraphHeightOffset; } + public boolean isInterruptible() { + return interruptible; + } - public AbilityPhase getPhase() { return phase; } + public void setInterruptible(boolean interruptible) { + this.interruptible = interruptible; + } + + public LockMovementType getLockMovement() { + return lockMovement; + } + + public void setLockMovement(LockMovementType lockMovement) { + this.lockMovement = lockMovement; + } /** - * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=RECOVERY). + * API method: Get lock movement type as integer. + * @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public int getLockMovementType() { + return lockMovement.ordinal(); + } + + /** + * API method: Set lock movement type from integer. + * @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public void setLockMovementType(int type) { + this.lockMovement = LockMovementType.fromOrdinal(type); + } + + /** + * Check if movement should be locked during WINDUP phase. + */ + @Override + public boolean isMovementLockedDuringWindup() { + return lockMovement.locksWindup(); + } + + /** + * Check if movement should be locked during ACTIVE phase. + */ + @Override + public boolean isMovementLockedDuringActive() { + return lockMovement.locksActive(); + } + + /** + * Check if movement should be locked during the current phase. + */ + public boolean isMovementLockedForCurrentPhase() { + switch (phase) { + case WINDUP: + return lockMovement.locksWindup(); + case ACTIVE: + return lockMovement.locksActive(); + default: + return false; + } + } + + public int getWindUpColor() { + return windUpColor; + } + + public void setWindUpColor(int windUpColor) { + this.windUpColor = windUpColor; + } + + public int getActiveColor() { + return activeColor; + } + + public void setActiveColor(int activeColor) { + this.activeColor = activeColor; + } + + public String getWindUpSound() { + return windUpSound; + } + + public void setWindUpSound(String windUpSound) { + this.windUpSound = windUpSound; + } + + public String getActiveSound() { + return activeSound; + } + + public void setActiveSound(String activeSound) { + this.activeSound = activeSound; + } + + public Animation getWindUpAnimation() { + if (AnimationController.Instance == null) return null; + + // Built-in animation by name takes priority + if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { + return (Animation) AnimationController.Instance.get(windUpAnimationName, true); + } + // Fall back to user animation by ID + if (windUpAnimationId >= 0) { + return (Animation) AnimationController.Instance.get(windUpAnimationId); + } + return null; + } + + public int getWindUpAnimationId() { + return windUpAnimationId; + } + + public void setWindUpAnimationId(int windUpAnimationId) { + this.windUpAnimationId = windUpAnimationId; + } + + public String getWindUpAnimationName() { + return windUpAnimationName; + } + + public void setWindUpAnimationName(String windUpAnimationName) { + this.windUpAnimationName = windUpAnimationName != null ? windUpAnimationName : ""; + } + + public Animation getActiveAnimation() { + if (AnimationController.Instance == null) return null; + + // Built-in animation by name takes priority + if (activeAnimationName != null && !activeAnimationName.isEmpty()) { + return (Animation) AnimationController.Instance.get(activeAnimationName, true); + } + // Fall back to user animation by ID + if (activeAnimationId >= 0) { + return (Animation) AnimationController.Instance.get(activeAnimationId); + } + return null; + } + + public int getActiveAnimationId() { + return activeAnimationId; + } + + public void setActiveAnimationId(int activeAnimationId) { + this.activeAnimationId = activeAnimationId; + } + + public String getActiveAnimationName() { + return activeAnimationName; + } + + public void setActiveAnimationName(String activeAnimationName) { + this.activeAnimationName = activeAnimationName != null ? activeAnimationName : ""; + } + + public boolean isShowTelegraph() { + return showTelegraph; + } + + public void setShowTelegraph(boolean showTelegraph) { + this.showTelegraph = showTelegraph; + } + + public TelegraphType getTelegraphType() { + return telegraphType; + } + + public void setTelegraphType(TelegraphType telegraphType) { + this.telegraphType = telegraphType; + } + + public float getTelegraphHeightOffset() { + return telegraphHeightOffset; + } + + public void setTelegraphHeightOffset(float telegraphHeightOffset) { + this.telegraphHeightOffset = telegraphHeightOffset; + } + + public AbilityPhase getPhase() { + return phase; + } + + /** + * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=DAZED). * Required for IAbility interface. */ @Override - public int getPhaseInt() { return phase.ordinal(); } + public int getPhaseInt() { + return phase.ordinal(); + } - public int getCurrentTick() { return currentTick; } - public EntityLivingBase getCurrentTarget() { return currentTarget; } - public NBTTagCompound getCustomData() { return customData; } + public int getCurrentTick() { + return currentTick; + } - public List getConditions() { return conditions; } - public void addCondition(Condition c) { conditions.add(c); } + public EntityLivingBase getCurrentTarget() { + return currentTarget; + } + + public NBTTagCompound getCustomData() { + return customData; + } + + public List getConditions() { + return conditions; + } + + public void addCondition(Condition c) { + conditions.add(c); + } + + public UserType getAllowedBy() { + return allowedBy; + } + + public void setAllowedBy(UserType allowedBy) { + this.allowedBy = allowedBy; + } + + public boolean isIgnoreCooldown() { + return ignoreCooldown; + } + + public void setIgnoreCooldown(boolean ignoreCooldown) { + this.ignoreCooldown = ignoreCooldown; + } + + public List getEffects() { + return effects; + } + + public void setEffects(List effects) { + this.effects = effects != null ? effects : new ArrayList<>(); + } + + public void addEffect(AbilityEffect effect) { + if (effect != null && effect.isValid()) { + effects.add(effect); + } + } + + public void clearEffects() { + effects.clear(); + } + + // ═══════════════════════════════════════════════════════════════════ + // SHIT FROM THE ASS + // ═══════════════════════════════════════════════════════════════════ + + private int calculateWindupFromAnimation() { + if (!syncWindupWithAnimation) { + return windUpTicks; + } + + if (AnimationController.Instance == null) { + return this.windUpTicks; + } + + Animation animation = null; + // Check for built-in animation (by name) first + if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationName, true); + } + // Fall back to user animation (by ID) + else if (windUpAnimationId >= 0) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationId); + } + + if (animation == null || animation.frames.isEmpty()) { + return this.windUpTicks; + } + + // Calculate total duration by summing all frame durations + int totalDuration = 0; + for (Frame frame : animation.frames) { + totalDuration += frame.getDuration(); + } + + // Return total duration + return totalDuration; + } // ═══════════════════════════════════════════════════════════════════ // IAbility API METHODS // ═══════════════════════════════════════════════════════════════════ + @Override + public int getAllowedByType() { + return allowedBy.ordinal(); + } + + @Override + public void setAllowedByType(int type) { + this.allowedBy = UserType.fromOrdinal(type); + } + @Override public INbt getNbt() { return NpcAPI.Instance().getINbt(writeNBT()); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 685e5403d..eba8795f7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -1,6 +1,7 @@ package kamkeel.npcs.controllers.data.ability; import kamkeel.npcs.controllers.data.ability.type.*; +import kamkeel.npcs.controllers.SyncController; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; @@ -12,30 +13,46 @@ import java.util.*; import java.util.function.Supplier; /** - * Controller for ability types. Allows external mods to register custom ability types. - * Initialize during FMLInitializationEvent. - * - * Also manages saved ability presets that can be reused across NPCs. + * Central controller for ability types and presets. + *

+ * Three maps: + *

    + *
  • {@code typeFactories} — keyed by typeId (lang key), creates new instances of each ability type
  • + *
  • {@code abilities} — built-in pre-configured abilities, keyed by unique name
  • + *
  • {@code customAbilities} — user-created presets, keyed by UUID
  • + *
*/ public class AbilityController implements IAbilityHandler { - public static AbilityController Instance; + public static AbilityController Instance = new AbilityController(); - private final Map> factories = new LinkedHashMap<>(); - private final Map> factoriesByTypeId = new LinkedHashMap<>(); - private final Map savedAbilities = new LinkedHashMap<>(); + /** Type factories keyed by typeId (lang key, e.g. "ability.cnpc.slam"). */ + private final Map> typeFactories = new LinkedHashMap<>(); + + /** Built-in pre-configured abilities keyed by unique name. */ + private final Map abilities = new LinkedHashMap<>(); + + /** User-created custom ability presets keyed by UUID. */ + private final Map customAbilities = new LinkedHashMap<>(); + + /** Version counter — incremented on any custom ability add/modify/delete. + * Used by AbilitySlot for cache invalidation. */ + private int version = 0; public AbilityController() { - Instance = this; - registerBuiltins(); + registerBuiltinTypes(); } + // ═══════════════════════════════════════════════════════════════════════════ + // LOADING / SAVING CUSTOM ABILITIES + // ═══════════════════════════════════════════════════════════════════════════ + /** - * Load saved abilities from world directory. - * Called when world loads. + * Load custom ability presets from world directory. + * Called when world loads. Files are named {@code .json}. */ public void load() { - savedAbilities.clear(); + customAbilities.clear(); File dir = getDir(); if (!dir.exists()) { return; @@ -47,22 +64,22 @@ public class AbilityController implements IAbilityHandler { for (File file : files) { if (!file.isFile() || !file.getName().endsWith(".json")) continue; try { + String filename = file.getName(); + String uuid = filename.substring(0, filename.length() - 5); // strip .json + NBTTagCompound nbt = NBTJsonUtil.LoadFile(file); Ability ability = fromNBT(nbt); - if (ability != null && ability.getName() != null && !ability.getName().isEmpty()) { - savedAbilities.put(ability.getName(), ability); - LogWriter.info("Loaded ability preset: " + ability.getName()); + if (ability != null) { + ability.setId(uuid); + customAbilities.put(uuid, ability); } } catch (Exception e) { - LogWriter.error("Error loading ability preset: " + file.getAbsolutePath(), e); + LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e); } } - LogWriter.info("Loaded " + savedAbilities.size() + " ability presets"); + LogWriter.info("Loaded " + customAbilities.size() + " custom abilities"); } - /** - * Get the ability save directory. - */ private File getDir() { File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities"); if (!dir.exists()) { @@ -72,26 +89,26 @@ public class AbilityController implements IAbilityHandler { } /** - * Save an ability preset to disk. - * - * @param ability The ability to save - * @return true if successful + * Save a custom ability preset. If the ability has no id, generates a UUID. + * Stores in customAbilities map and writes to disk. */ - public boolean saveAbility(Ability ability) { - if (ability == null || ability.getName() == null || ability.getName().isEmpty()) { - return false; + public boolean saveCustomAbility(Ability ability) { + if (ability == null) return false; + + String uuid = ability.getId(); + if (uuid == null || uuid.isEmpty()) { + uuid = UUID.randomUUID().toString(); + ability.setId(uuid); } File dir = getDir(); - String safeName = ability.getName().replaceAll("[^a-zA-Z0-9_-]", "_"); - File fileNew = new File(dir, safeName + ".json_new"); - File fileCurrent = new File(dir, safeName + ".json"); + File fileNew = new File(dir, uuid + ".json_new"); + File fileCurrent = new File(dir, uuid + ".json"); try { NBTTagCompound nbt = ability.writeNBT(); NBTJsonUtil.SaveFile(fileNew, nbt); - // Atomic rename if (fileCurrent.exists()) { fileCurrent.delete(); } @@ -100,76 +117,144 @@ public class AbilityController implements IAbilityHandler { fileNew.delete(); } - savedAbilities.put(ability.getName(), ability); - LogWriter.info("Saved ability preset: " + ability.getName()); + customAbilities.put(uuid, ability); + version++; + LogWriter.info("Saved custom ability: " + ability.getName() + " [" + uuid + "]"); + SyncController.syncAllCustomAbilities(); return true; } catch (Exception e) { - LogWriter.error("Error saving ability preset: " + ability.getName(), e); + LogWriter.error("Error saving custom ability: " + uuid, e); return false; } } /** - * Delete a saved ability preset. - * - * @param name The name of the ability to delete - * @return true if successful + * Delete a custom ability preset by UUID. */ - public boolean deleteAbility(String name) { - if (name == null || name.isEmpty()) { - return false; - } + public boolean deleteCustomAbility(String uuid) { + if (uuid == null || uuid.isEmpty()) return false; - Ability removed = savedAbilities.remove(name); - if (removed == null) { - return false; - } + Ability removed = customAbilities.remove(uuid); + if (removed == null) return false; File dir = getDir(); - String safeName = name.replaceAll("[^a-zA-Z0-9_-]", "_"); - File file = new File(dir, safeName + ".json"); + File file = new File(dir, uuid + ".json"); if (file.exists()) { file.delete(); } - LogWriter.info("Deleted ability preset: " + name); + version++; + LogWriter.info("Deleted custom ability: " + uuid); + SyncController.syncAllCustomAbilities(); return true; } + // ═══════════════════════════════════════════════════════════════════════════ + // BUILT-IN ABILITY REGISTRY + // ═══════════════════════════════════════════════════════════════════════════ + /** - * Get a saved ability preset by name. + * Register a pre-configured built-in ability by name. + * These are looked up by name in {@link #resolveAbility(String)}. * - * @param name The name of the ability - * @return A copy of the ability, or null if not found + * @param name Unique name (e.g., "Jump Attack", "Ki Blast") + * @param ability Pre-configured ability instance */ - public Ability getSavedAbility(String name) { - Ability saved = savedAbilities.get(name); - if (saved == null) { - return null; + public void registerAbility(String name, Ability ability) { + if (abilities.containsKey(name)) { + LogWriter.info("AbilityController: Overwriting built-in ability: " + name); } - // Return a copy so modifications don't affect the saved version - return fromNBT(saved.writeNBT()); + ability.setName(name); + abilities.put(name, ability); + LogWriter.info("Registered ability: " + name); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // RESOLUTION (used by AbilitySlot, PlayerAbilityData, etc.) + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Resolve an ability by key. Checks built-in abilities first (by name), + * then custom abilities (by UUID). Returns a copy safe to modify. + * + * @param key The ability name (built-in) or UUID (custom) + * @return A copy of the resolved ability, or null if not found + */ + public Ability resolveAbility(String key) { + if (key == null || key.isEmpty()) return null; + + // Check built-in abilities first + Ability builtIn = abilities.get(key); + if (builtIn != null) { + return fromNBT(builtIn.writeNBT()); + } + + // Check custom abilities + Ability custom = customAbilities.get(key); + if (custom != null) { + return fromNBT(custom.writeNBT()); + } + + return null; } /** - * Check if a saved ability exists. + * Check if an ability key exists (either built-in or custom). */ - public boolean hasSavedAbility(String name) { - return savedAbilities.containsKey(name); + public boolean hasAbility(String key) { + return abilities.containsKey(key) || customAbilities.containsKey(key); } - /** - * Get all saved ability names. - */ - public Set getSavedAbilityNames() { - return new LinkedHashSet<>(savedAbilities.keySet()); + // ═══════════════════════════════════════════════════════════════════════════ + // ACCESSORS + // ═══════════════════════════════════════════════════════════════════════════ + + /** Get a built-in ability by name. Returns the stored instance (not a copy). */ + public Ability getAbility(String name) { + return abilities.get(name); } - /** - * Get all saved abilities. - */ - public Collection getSavedAbilities() { - return savedAbilities.values(); + /** Get all built-in ability names. */ + public Set getAbilityNames() { + return new LinkedHashSet<>(abilities.keySet()); + } + + /** Get a custom ability by UUID. Returns the stored instance (not a copy). */ + public Ability getCustomAbility(String uuid) { + return customAbilities.get(uuid); + } + + /** Check if a custom ability exists by UUID. */ + public boolean hasCustomAbility(String uuid) { + return customAbilities.containsKey(uuid); + } + + /** Get all custom ability UUIDs. */ + public Set getCustomAbilityIds() { + return new LinkedHashSet<>(customAbilities.keySet()); + } + + /** Get the display name for a custom ability by UUID. */ + public String getCustomAbilityName(String uuid) { + Ability a = customAbilities.get(uuid); + return a != null ? a.getName() : null; + } + + /** Get the custom abilities map (used by SyncController). */ + public Map getCustomAbilities() { + return customAbilities; + } + + /** Replace the custom abilities map (used by client-side sync). */ + public void setCustomAbilities(Map synced) { + customAbilities.clear(); + customAbilities.putAll(synced); + version++; + } + + /** Get the version counter (incremented on custom ability changes). */ + public int getVersion() { + return version; } // ═══════════════════════════════════════════════════════════════════════════ @@ -178,28 +263,37 @@ public class AbilityController implements IAbilityHandler { @Override public String[] getTypes() { - // Return the ability typeIds (lang keys), not the factory keys - return factoriesByTypeId.keySet().toArray(new String[0]); + return typeFactories.keySet().toArray(new String[0]); } @Override public boolean hasType(String typeId) { - return factories.containsKey(typeId) || factoriesByTypeId.containsKey(typeId); + return typeFactories.containsKey(typeId); } @Override - public String[] getSavedNames() { - return savedAbilities.keySet().toArray(new String[0]); + public String[] getAbilityNameArray() { + return abilities.keySet().toArray(new String[0]); } @Override - public boolean has(String name) { - return savedAbilities.containsKey(name); + public boolean hasAbilityName(String name) { + return abilities.containsKey(name); } @Override - public boolean delete(String name) { - return deleteAbility(name); + public String[] getCustomAbilityIdArray() { + return customAbilities.keySet().toArray(new String[0]); + } + + @Override + public boolean hasCustomAbilityId(String uuid) { + return customAbilities.containsKey(uuid); + } + + @Override + public boolean deleteCustomAbilityById(String uuid) { + return deleteCustomAbility(uuid); } // ═══════════════════════════════════════════════════════════════════════════ @@ -208,26 +302,23 @@ public class AbilityController implements IAbilityHandler { /** * Register a new ability type. - * Call during FMLInitializationEvent. + * Extracts the typeId from a temporary instance and stores in typeFactories. * - * @param factoryKey The factory key (e.g., "cnpc:slam", "mymod:custom_ability") - * @param factory Factory lambda that creates a new instance + * @param factoryKey Unused legacy key — the typeId from the created instance is used + * @param factory Factory lambda that creates a new instance */ public void registerType(String factoryKey, Supplier factory) { - if (factories.containsKey(factoryKey)) { - LogWriter.info("AbilityController: Overwriting existing ability type: " + factoryKey); - } - factories.put(factoryKey, factory); - - // Also register by the ability's actual typeId (lang key) Ability temp = factory.get(); - String abilityTypeId = temp.getTypeId(); - factoriesByTypeId.put(abilityTypeId, factory); + String typeId = temp.getTypeId(); + if (typeFactories.containsKey(typeId)) { + LogWriter.info("AbilityController: Overwriting type: " + typeId); + } + typeFactories.put(typeId, factory); } /** * Create an ability from NBT data. - * Uses the "typeId" field to determine which type to create. + * Uses the "typeId" field to look up in typeFactories. */ public Ability fromNBT(NBTTagCompound nbt) { if (nbt == null || !nbt.hasKey("typeId")) { @@ -235,11 +326,7 @@ public class AbilityController implements IAbilityHandler { } String typeId = nbt.getString("typeId"); - Supplier factory = factories.get(typeId); - if (factory == null) { - // Try looking up by ability typeId (lang key) - factory = factoriesByTypeId.get(typeId); - } + Supplier factory = typeFactories.get(typeId); if (factory == null) { LogWriter.info("AbilityController: Unknown ability type: " + typeId); return null; @@ -252,15 +339,11 @@ public class AbilityController implements IAbilityHandler { /** * Create a new empty ability of the given type. - * @param factoryKey The factory key (e.g., "cnpc:slam") or the ability typeId (e.g., "ability.cnpc.slam") + * + * @param typeId The ability typeId (e.g., "ability.cnpc.slam") */ - public Ability create(String factoryKey) { - // Try direct lookup first (factory key) - Supplier factory = factories.get(factoryKey); - if (factory == null) { - // Try looking up by ability typeId - factory = factoriesByTypeId.get(factoryKey); - } + public Ability create(String typeId) { + Supplier factory = typeFactories.get(typeId); if (factory == null) { return null; } @@ -270,16 +353,22 @@ public class AbilityController implements IAbilityHandler { /** * Register built-in ability types. */ - private void registerBuiltins() { + private void registerBuiltinTypes() { // Melee/AOE damage abilities registerType("cnpc:slam", AbilitySlam::new); registerType("cnpc:heavy_hit", AbilityHeavyHit::new); registerType("cnpc:cutter", AbilityCutter::new); // Ranged/Projectile abilities - registerType("cnpc:beam", AbilityBeam::new); + registerType("cnpc:sweeper", AbilitySweeper::new); registerType("cnpc:projectile", AbilityProjectile::new); registerType("cnpc:orb", AbilityOrb::new); + registerType("cnpc:dual_orb", AbilityOrbDual::new); + registerType("cnpc:dual_disc", AbilityDisc::new); + registerType("cnpc:dual_disc", AbilityDiscDual::new); + registerType("cnpc:laser_shot", AbilityLaserShot::new); + registerType("cnpc:beam", AbilityEnergyBeam::new); + registerType("cnpc:dual_beam", AbilityBeamDual::new); // Movement abilities registerType("cnpc:charge", AbilityCharge::new); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java new file mode 100644 index 000000000..57f0cbf17 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java @@ -0,0 +1,153 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.potion.Potion; +import net.minecraft.potion.PotionEffect; + +/** + * Represents a configurable potion effect that can be applied by abilities. + * Provides a unified system for managing debuffs like slowness, weakness, poison, etc. + */ +public class AbilityEffect { + + public enum EffectType { + NONE("gui.none", -1), + SLOWNESS("potion.moveSlowdown", Potion.moveSlowdown.id), + WEAKNESS("potion.weakness", Potion.weakness.id), + POISON("potion.poison", Potion.poison.id), + WITHER("potion.wither", Potion.wither.id), + BLINDNESS("potion.blindness", Potion.blindness.id), + NAUSEA("potion.confusion", Potion.confusion.id), + HUNGER("potion.hunger", Potion.hunger.id), + MINING_FATIGUE("potion.digSlowDown", Potion.digSlowdown.id); + + private final String langKey; + private final int potionId; + + EffectType(String langKey, int potionId) { + this.langKey = langKey; + this.potionId = potionId; + } + + public String getLangKey() { + return langKey; + } + + public int getPotionId() { + return potionId; + } + + public static String[] getLangKeys() { + EffectType[] types = values(); + String[] keys = new String[types.length]; + for (int i = 0; i < types.length; i++) keys[i] = types[i].langKey; + return keys; + } + + public static EffectType fromOrdinal(int ordinal) { + EffectType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return NONE; + } + } + + private EffectType type = EffectType.NONE; + private int durationTicks = 60; + private int amplifier = 0; // 0-10 + + public AbilityEffect() { + } + + public AbilityEffect(EffectType type, int durationTicks, int amplifier) { + this.type = type; + this.durationTicks = Math.max(1, durationTicks); + this.amplifier = Math.max(0, Math.min(10, amplifier)); + } + + /** + * Creates a copy of this effect. + */ + public AbilityEffect copy() { + return new AbilityEffect(type, durationTicks, amplifier); + } + + /** + * Applies this effect to the given entity. + * Does nothing if type is NONE. + */ + public void apply(EntityLivingBase entity) { + if (entity == null || type == EffectType.NONE) { + return; + } + + int potionId = type.getPotionId(); + if (potionId >= 0) { + entity.addPotionEffect(new PotionEffect(potionId, durationTicks, amplifier)); + } + } + + /** + * Writes this effect to NBT. + */ + public NBTTagCompound writeNBT() { + NBTTagCompound nbt = new NBTTagCompound(); + nbt.setInteger("type", type.ordinal()); + nbt.setInteger("duration", durationTicks); + nbt.setInteger("amplifier", amplifier); + return nbt; + } + + /** + * Reads this effect from NBT. + */ + public void readNBT(NBTTagCompound nbt) { + this.type = EffectType.fromOrdinal(nbt.getInteger("type")); + this.durationTicks = nbt.hasKey("duration") ? nbt.getInteger("duration") : 60; + this.amplifier = nbt.hasKey("amplifier") ? nbt.getInteger("amplifier") : 0; + } + + /** + * Creates an AbilityEffect from NBT. + */ + public static AbilityEffect fromNBT(NBTTagCompound nbt) { + AbilityEffect effect = new AbilityEffect(); + effect.readNBT(nbt); + return effect; + } + + // Getters & Setters + + public EffectType getType() { + return type; + } + + public void setType(EffectType type) { + this.type = type != null ? type : EffectType.NONE; + } + + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + + public int getAmplifier() { + return amplifier; + } + + public void setAmplifier(int amplifier) { + this.amplifier = Math.max(0, Math.min(10, amplifier)); + } + + /** + * Returns true if this effect is configured (not NONE). + */ + public boolean isValid() { + return type != EffectType.NONE; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java index 6338279ba..0611a0274 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java @@ -4,5 +4,5 @@ public enum AbilityPhase { IDLE, // Not executing WINDUP, // Charging up (telegraph visible) ACTIVE, // Executing (damage happens) - RECOVERY // Finishing animation + DAZED // Stunned after interrupt during WINDUP (cannot attack) } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java new file mode 100644 index 000000000..ca4e7471e --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java @@ -0,0 +1,124 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Wrapper that stores either an inline ability (full NBT) or a reference + * to a built-in/custom ability (by name or UUID). + *

+ * References are lazy-resolved via {@link AbilityController#resolveAbility(String)} + * and cached until the controller version changes. + */ +public class AbilitySlot { + + private Ability inlineAbility; + private String referenceId; + + private transient Ability cachedAbility; + private transient int cachedVersion = -1; + + private AbilitySlot() {} + + /** + * Create a slot with an inline (embedded) ability. + */ + public static AbilitySlot inline(Ability ability) { + AbilitySlot slot = new AbilitySlot(); + slot.inlineAbility = ability; + slot.referenceId = null; + return slot; + } + + /** + * Create a slot that references a built-in ability (by name) or custom ability (by UUID). + */ + public static AbilitySlot reference(String referenceId) { + AbilitySlot slot = new AbilitySlot(); + slot.inlineAbility = null; + slot.referenceId = referenceId; + return slot; + } + + public boolean isReference() { + return referenceId != null; + } + + public String getReferenceId() { + return referenceId; + } + + /** + * Get the resolved ability. For inline slots, returns the ability directly. + * For reference slots, lazily resolves from AbilityController and caches. + * Returns null if the reference cannot be resolved (broken ref). + */ + public Ability getAbility() { + if (!isReference()) { + return inlineAbility; + } + + // Lazy resolve with version-based cache invalidation + AbilityController controller = AbilityController.Instance; + if (controller == null) return null; + + int currentVersion = controller.getVersion(); + if (cachedAbility != null && cachedVersion == currentVersion) { + return cachedAbility; + } + + cachedAbility = controller.resolveAbility(referenceId); + cachedVersion = currentVersion; + return cachedAbility; + } + + /** + * Get the inline ability directly (null for reference slots). + */ + public Ability getInlineAbility() { + return inlineAbility; + } + + /** + * Convert this reference slot to an inline slot by copying the resolved ability. + * No-op if already inline. Returns false if resolution fails. + */ + public boolean convertToInline() { + if (!isReference()) return true; + + Ability resolved = getAbility(); + if (resolved == null) return false; + + // Deep copy via NBT round-trip + inlineAbility = AbilityController.Instance.fromNBT(resolved.writeNBT()); + referenceId = null; + cachedAbility = null; + cachedVersion = -1; + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + public NBTTagCompound writeNBT() { + if (isReference()) { + NBTTagCompound nbt = new NBTTagCompound(); + nbt.setString("Reference", referenceId); + return nbt; + } + return inlineAbility != null ? inlineAbility.writeNBT() : new NBTTagCompound(); + } + + public static AbilitySlot fromNBT(NBTTagCompound nbt) { + if (nbt == null) return null; + + if (nbt.hasKey("Reference")) { + return reference(nbt.getString("Reference")); + } + + // Inline ability + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability == null) return null; + return inline(ability); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java new file mode 100644 index 000000000..1c308493c --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java @@ -0,0 +1,61 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines anchor points for ability charging effects. + * Determines where the charging visual (orb, disc, etc.) appears relative to the entity. + */ +public enum AnchorPoint { + FRONT("Front"), // In front of entity face (default) + CENTER("Center"), // Entity center + RIGHT_HAND("Right Hand"), // Right arm/hand position + LEFT_HAND("Left Hand"), // Left arm/hand position + ABOVE_HEAD("Above Head"), // Above the entity + CHEST("Chest"); // Chest level, centered + + private final String displayName; + + AnchorPoint(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + /** + * Get AnchorPoint from ordinal value. + * Returns FRONT if ordinal is out of bounds. + */ + public static AnchorPoint fromOrdinal(int ordinal) { + AnchorPoint[] vals = values(); + if (ordinal < 0 || ordinal >= vals.length) { + return FRONT; + } + return vals[ordinal]; + } + + /** + * Get all display names for GUI dropdowns. + */ + public static String[] getDisplayNames() { + AnchorPoint[] values = values(); + String[] names = new String[values.length]; + for (int i = 0; i < values.length; i++) { + names[i] = values[i].displayName; + } + return names; + } + + @Override + public String toString() { + switch (this) { + case FRONT: return "ability.anchor.front"; + case CENTER: return "ability.anchor.center"; + case RIGHT_HAND: return "ability.anchor.rightHand"; + case LEFT_HAND: return "ability.anchor.leftHand"; + case ABOVE_HEAD: return "ability.anchor.aboveHead"; + case CHEST: return "ability.anchor.chest"; + default: return name(); + } + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java new file mode 100644 index 000000000..904fad0b9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java @@ -0,0 +1,55 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Abstract base class for built-in (non-customizable) abilities. + * Built-in abilities are registered by key, have fixed configurations, + * and cannot be modified through the preset GUI system. + *

+ * Examples: simple utility abilities like Jump, Sprint, etc. + * Third-party mods can extend this class to create their own built-in abilities. + */ +public abstract class BuiltInAbility extends Ability { + + /** + * The registry key used to look up this ability (e.g., "cnpc:jump", "dbc:ki_blast"). + */ + private final String registryKey; + + public BuiltInAbility(String registryKey) { + this.registryKey = registryKey; + this.id = registryKey; + this.name = registryKey; + this.typeId = registryKey; + } + + /** + * Get the registry key for this built-in ability. + */ + public String getRegistryKey() { + return registryKey; + } + + /** + * Built-in abilities do not write type-specific NBT (they have fixed configs). + */ + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + // No-op: built-in abilities have fixed configurations + } + + /** + * Built-in abilities do not read type-specific NBT. + */ + @Override + public void readTypeNBT(NBTTagCompound nbt) { + // No-op: built-in abilities have fixed configurations + } + + /** + * Get the duration of the active phase in ticks. + * Built-in abilities must define their own active duration. + */ + public abstract int getActiveDurationTicks(); +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java index 70b3b290f..1183fe858 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java @@ -13,11 +13,19 @@ public interface Condition { /** * Check if this condition is met. * - * @param npc The NPC that would use the ability - * @param target The current target (may be null for self-targeting abilities) + * @param caster The entity that would use the ability (NPC or Player) + * @param target The current target (may be null for self-targeting abilities or players) * @return true if the condition is satisfied */ - boolean check(EntityNPCInterface npc, EntityLivingBase target); + boolean check(EntityLivingBase caster, EntityLivingBase target); + + /** + * Whether this condition requires a target entity to evaluate. + * Conditions that return true here will be skipped for player ability checks. + */ + default boolean requiresTarget() { + return false; + } /** * Get the condition type ID for serialization. @@ -75,24 +83,27 @@ public interface Condition { // ═══════════════════════════════════════════════════════════════════ /** - * Condition: NPC HP is above a percentage threshold. + * Condition: Caster HP is above a percentage threshold. */ class ConditionHPAbove implements Condition { private float threshold = 0.5f; // 50% - public ConditionHPAbove() {} + public ConditionHPAbove() { + } public ConditionHPAbove(float threshold) { this.threshold = threshold; } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { - return npc.getHealth() / npc.getMaxHealth() > threshold; + public boolean check(EntityLivingBase caster, EntityLivingBase target) { + return caster.getHealth() / caster.getMaxHealth() > threshold; } @Override - public String getTypeId() { return "hp_above"; } + public String getTypeId() { + return "hp_above"; + } @Override public NBTTagCompound writeNBT() { @@ -109,24 +120,27 @@ public interface Condition { } /** - * Condition: NPC HP is below a percentage threshold. + * Condition: Caster HP is below a percentage threshold. */ class ConditionHPBelow implements Condition { private float threshold = 0.5f; - public ConditionHPBelow() {} + public ConditionHPBelow() { + } public ConditionHPBelow(float threshold) { this.threshold = threshold; } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { - return npc.getHealth() / npc.getMaxHealth() < threshold; + public boolean check(EntityLivingBase caster, EntityLivingBase target) { + return caster.getHealth() / caster.getMaxHealth() < threshold; } @Override - public String getTypeId() { return "hp_below"; } + public String getTypeId() { + return "hp_below"; + } @Override public NBTTagCompound writeNBT() { @@ -144,24 +158,33 @@ public interface Condition { /** * Condition: Target HP is above a percentage threshold. + * Requires a target - will be skipped for player ability checks. */ class ConditionTargetHPAbove implements Condition { private float threshold = 0.5f; - public ConditionTargetHPAbove() {} + public ConditionTargetHPAbove() { + } public ConditionTargetHPAbove(float threshold) { this.threshold = threshold; } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { if (target == null) return false; return target.getHealth() / target.getMaxHealth() > threshold; } @Override - public String getTypeId() { return "target_hp_above"; } + public boolean requiresTarget() { + return true; + } + + @Override + public String getTypeId() { + return "target_hp_above"; + } @Override public NBTTagCompound writeNBT() { @@ -179,24 +202,33 @@ public interface Condition { /** * Condition: Target HP is below a percentage threshold. + * Requires a target - will be skipped for player ability checks. */ class ConditionTargetHPBelow implements Condition { private float threshold = 0.5f; - public ConditionTargetHPBelow() {} + public ConditionTargetHPBelow() { + } public ConditionTargetHPBelow(float threshold) { this.threshold = threshold; } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { if (target == null) return false; return target.getHealth() / target.getMaxHealth() < threshold; } @Override - public String getTypeId() { return "target_hp_below"; } + public boolean requiresTarget() { + return true; + } + + @Override + public String getTypeId() { + return "target_hp_below"; + } @Override public NBTTagCompound writeNBT() { @@ -213,14 +245,16 @@ public interface Condition { } /** - * Condition: NPC has been hit X times within a time window. + * Condition: Caster has been hit X times within a time window. * Useful for reactive abilities that trigger after receiving multiple hits. + * Currently only works for NPCs (requires DataAbilities hit tracking). */ class ConditionHitCount implements Condition { private int requiredHits = 3; private int withinTicks = 60; - public ConditionHitCount() {} + public ConditionHitCount() { + } public ConditionHitCount(int requiredHits, int withinTicks) { this.requiredHits = requiredHits; @@ -228,16 +262,22 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { // Check NPC's recent hit count from DataAbilities - if (npc.abilities != null) { - return npc.abilities.getRecentHitCount(withinTicks) >= requiredHits; + if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + if (npc.abilities != null) { + return npc.abilities.getRecentHitCount(withinTicks) >= requiredHits; + } } + // For players, hit count tracking is not yet supported return false; } @Override - public String getTypeId() { return "hit_count"; } + public String getTypeId() { + return "hit_count"; + } @Override public NBTTagCompound writeNBT() { @@ -254,10 +294,20 @@ public interface Condition { withinTicks = nbt.getInteger("withinTicks"); } - public int getRequiredHits() { return requiredHits; } - public void setRequiredHits(int hits) { this.requiredHits = hits; } + public int getRequiredHits() { + return requiredHits; + } - public int getWithinTicks() { return withinTicks; } - public void setWithinTicks(int ticks) { this.withinTicks = ticks; } + public void setRequiredHits(int hits) { + this.requiredHits = hits; + } + + public int getWithinTicks() { + return withinTicks; + } + + public void setWithinTicks(int ticks) { + this.withinTicks = ticks; + } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/LockMovementType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/LockMovementType.java new file mode 100644 index 000000000..1b81a3bb3 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/LockMovementType.java @@ -0,0 +1,83 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines when an ability locks NPC movement and rotation. + */ +public enum LockMovementType { + /** + * No movement lock - NPC can move freely during both phases. + */ + NO, + + /** + * Lock movement during WINDUP phase only. + * NPC can move during ACTIVE phase. + */ + WINDUP, + + /** + * Lock movement during ACTIVE phase only. + * NPC can move during WINDUP phase. + */ + ACTIVE, + + /** + * Lock movement during both WINDUP and ACTIVE phases. + */ + WINDUP_AND_ACTIVE; + + /** + * Check if movement should be locked during WINDUP phase. + */ + public boolean locksWindup() { + return this == WINDUP || this == WINDUP_AND_ACTIVE; + } + + /** + * Check if movement should be locked during ACTIVE phase. + */ + public boolean locksActive() { + return this == ACTIVE || this == WINDUP_AND_ACTIVE; + } + + /** + * Get display name for GUI. + */ + public String getDisplayKey() { + switch (this) { + case NO: + return "gui.no"; + case WINDUP: + return "ability.lockMove.windup"; + case ACTIVE: + return "ability.lockMove.active"; + case WINDUP_AND_ACTIVE: + return "ability.lockMove.both"; + default: + return "gui.no"; + } + } + + /** + * Get all display keys for GUI dropdown. + */ + public static String[] getDisplayKeys() { + return new String[]{ + "gui.no", + "ability.lockMove.windup", + "ability.lockMove.active", + "ability.lockMove.both" + }; + } + + /** + * Get LockMovementType from ordinal, with fallback to WINDUP. + */ + public static LockMovementType fromOrdinal(int ordinal) { + LockMovementType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return WINDUP; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java index a639702e1..9bdbdac6f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java @@ -4,24 +4,34 @@ package kamkeel.npcs.controllers.data.ability; * Defines how an ability selects its target(s). */ public enum TargetingMode { - /** Target the current aggro target */ + /** + * Target the current aggro target + */ AGGRO_TARGET, - /** Target self (for buffs, heals, guards) */ + /** + * Target self (for buffs, heals, guards) + */ SELF, - /** AOE centered on self */ + /** + * AOE centered on self + */ AOE_SELF, - /** AOE centered on target */ - AOE_TARGET, + /** + * AOE centered on target + */ + AOE_TARGET; - /** Random nearby enemy */ - RANDOM_ENEMY, - - /** Lowest HP enemy in range */ - LOWEST_HP, - - /** Highest threat enemy */ - HIGHEST_THREAT + @Override + public String toString() { + switch (this) { + case AGGRO_TARGET: return "ability.target.aggro_target"; + case SELF: return "ability.target.self"; + case AOE_SELF: return "ability.target.aoe_self"; + case AOE_TARGET: return "ability.target.aoe_target"; + default: return name(); + } + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java new file mode 100644 index 000000000..d68693f59 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java @@ -0,0 +1,26 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines which entity types can use an ability. + */ +public enum UserType { + NPC_ONLY, + PLAYER_ONLY, + BOTH; + + public boolean allowsNpc() { + return this == NPC_ONLY || this == BOTH; + } + + public boolean allowsPlayer() { + return this == PLAYER_ONLY || this == BOTH; + } + + public static UserType fromOrdinal(int ordinal) { + UserType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return BOTH; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java new file mode 100644 index 000000000..66983f89b --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java @@ -0,0 +1,54 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import net.minecraft.nbt.NBTTagCompound; + +public class EnergyAnchorData { + public AnchorPoint anchorPoint = AnchorPoint.FRONT; + public float anchorOffsetX = 0; + public float anchorOffsetY = 0; + public float anchorOffsetZ = 0; + + public EnergyAnchorData() { + } + + public EnergyAnchorData(AnchorPoint anchorPoint) { + this.anchorPoint = anchorPoint; + } + + public EnergyAnchorData(AnchorPoint anchorPoint, float anchorOffsetX, float anchorOffsetY, float anchorOffsetZ) { + this.anchorPoint = anchorPoint; + this.anchorOffsetX = anchorOffsetX; + this.anchorOffsetY = anchorOffsetY; + this.anchorOffsetZ = anchorOffsetZ; + } + + public EnergyAnchorData(int anchorPoint) { + this.anchorPoint = AnchorPoint.fromOrdinal(anchorPoint); + } + + public EnergyAnchorData(int anchorPoint, float anchorOffsetX, float anchorOffsetY, float anchorOffsetZ) { + this.anchorPoint = AnchorPoint.fromOrdinal(anchorPoint); + this.anchorOffsetX = anchorOffsetX; + this.anchorOffsetY = anchorOffsetY; + this.anchorOffsetZ = anchorOffsetZ; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setInteger("anchorPoint", anchorPoint.ordinal()); + nbt.setFloat("anchorOffsetX", anchorOffsetX); + nbt.setFloat("anchorOffsetY", anchorOffsetY); + nbt.setFloat("anchorOffsetZ", anchorOffsetZ); + } + + public void readNBT(NBTTagCompound nbt) { + this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromOrdinal(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; + this.anchorOffsetX = nbt.hasKey("anchorOffsetX") ? nbt.getFloat("anchorOffsetX") : 0; + this.anchorOffsetY = nbt.hasKey("anchorOffsetY") ? nbt.getFloat("anchorOffsetY") : 0; + this.anchorOffsetZ = nbt.hasKey("anchorOffsetZ") ? nbt.getFloat("anchorOffsetZ") : 0; + } + + public EnergyAnchorData copy() { + return new EnergyAnchorData(anchorPoint, anchorOffsetX, anchorOffsetY, anchorOffsetZ); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyColorData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyColorData.java new file mode 100644 index 000000000..4bd841742 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyColorData.java @@ -0,0 +1,50 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups visual color properties shared by all energy abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyColorData { + public int innerColor = 0xFFFFFF; + public int outerColor = 0x8888FF; + public boolean outerColorEnabled = true; + public float outerColorWidth = 0.4f; + public float outerColorAlpha = 0.5f; + public float rotationSpeed = 4.0f; + + public EnergyColorData() {} + + public EnergyColorData(int innerColor, int outerColor, boolean outerColorEnabled, + float outerColorWidth, float outerColorAlpha, float rotationSpeed) { + this.innerColor = innerColor; + this.outerColor = outerColor; + this.outerColorEnabled = outerColorEnabled; + this.outerColorWidth = outerColorWidth; + this.outerColorAlpha = outerColorAlpha; + this.rotationSpeed = rotationSpeed; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setFloat("outerColorAlpha", outerColorAlpha); + nbt.setFloat("rotationSpeed", rotationSpeed); + } + + public void readNBT(NBTTagCompound nbt) { + innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; + outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x8888FF; + outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; + outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; + rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 4.0f; + } + + public EnergyColorData copy() { + return new EnergyColorData(innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java new file mode 100644 index 000000000..fa09645a6 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java @@ -0,0 +1,51 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups combat properties shared by energy projectile abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyCombatData { + public float damage = 7.0f; + public float knockback = 1.0f; + public float knockbackUp = 0.1f; + public boolean explosive = false; + public float explosionRadius = 3.0f; + public float explosionDamageFalloff = 0.5f; + + public EnergyCombatData() {} + + public EnergyCombatData(float damage, float knockback, float knockbackUp, + boolean explosive, float explosionRadius, float explosionDamageFalloff) { + this.damage = damage; + this.knockback = knockback; + this.knockbackUp = knockbackUp; + this.explosive = explosive; + this.explosionRadius = explosionRadius; + this.explosionDamageFalloff = explosionDamageFalloff; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("knockbackUp", knockbackUp); + nbt.setBoolean("explosive", explosive); + nbt.setFloat("explosionRadius", explosionRadius); + nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); + } + + public void readNBT(NBTTagCompound nbt) { + damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; + knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; + knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.1f; + explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); + explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f; + explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; + } + + public EnergyCombatData copy() { + return new EnergyCombatData(damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java new file mode 100644 index 000000000..5b90bafce --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java @@ -0,0 +1,41 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups homing/movement properties for energy projectile abilities. + * Used by Orb, Disc, and Beam (not Laser or Sweeper). + */ +public class EnergyHomingData { + public float speed = 0.5f; + public boolean homing = true; + public float homingStrength = 0.15f; + public float homingRange = 20.0f; + + public EnergyHomingData() {} + + public EnergyHomingData(float speed, boolean homing, float homingStrength, float homingRange) { + this.speed = speed; + this.homing = homing; + this.homingStrength = homingStrength; + this.homingRange = homingRange; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setFloat("speed", speed); + nbt.setBoolean("homing", homing); + nbt.setFloat("homingStrength", homingStrength); + nbt.setFloat("homingRange", homingRange); + } + + public void readNBT(NBTTagCompound nbt) { + speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 0.5f; + homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); + homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.15f; + homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 20.0f; + } + + public EnergyHomingData copy() { + return new EnergyHomingData(speed, homing, homingStrength, homingRange); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java new file mode 100644 index 000000000..f8f0122e8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java @@ -0,0 +1,33 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups lifespan/range properties for energy projectile abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyLifespanData { + public float maxDistance = 30.0f; + public int maxLifetime = 200; + + public EnergyLifespanData() {} + + public EnergyLifespanData(float maxDistance, int maxLifetime) { + this.maxDistance = maxDistance; + this.maxLifetime = maxLifetime; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setFloat("maxDistance", maxDistance); + nbt.setInteger("maxLifetime", maxLifetime); + } + + public void readNBT(NBTTagCompound nbt) { + maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 30.0f; + maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; + } + + public EnergyLifespanData copy() { + return new EnergyLifespanData(maxDistance, maxLifetime); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java new file mode 100644 index 000000000..30d4ea6d8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java @@ -0,0 +1,42 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups lightning visual effect properties for energy abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyLightningData { + public boolean lightningEffect = false; + public float lightningDensity = 0.15f; + public float lightningRadius = 0.5f; + public int lightningFadeTime = 6; + + public EnergyLightningData() {} + + public EnergyLightningData(boolean lightningEffect, float lightningDensity, + float lightningRadius, int lightningFadeTime) { + this.lightningEffect = lightningEffect; + this.lightningDensity = lightningDensity; + this.lightningRadius = lightningRadius; + this.lightningFadeTime = lightningFadeTime; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setBoolean("lightningEffect", lightningEffect); + nbt.setFloat("lightningDensity", lightningDensity); + nbt.setFloat("lightningRadius", lightningRadius); + nbt.setInteger("lightningFadeTime", lightningFadeTime); + } + + public void readNBT(NBTTagCompound nbt) { + lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); + lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; + lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; + lightningFadeTime = nbt.hasKey("lightningFadeTime") ? nbt.getInteger("lightningFadeTime") : 6; + } + + public EnergyLightningData copy() { + return new EnergyLightningData(lightningEffect, lightningDensity, lightningRadius, lightningFadeTime); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/ColumnHint.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/ColumnHint.java new file mode 100644 index 000000000..a1b5dd270 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/ColumnHint.java @@ -0,0 +1,7 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +public enum ColumnHint { + FULL, + LEFT, + RIGHT +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java new file mode 100644 index 000000000..28b211c3e --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java @@ -0,0 +1,289 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import noppes.npcs.client.gui.SubGuiColorSelector; +import noppes.npcs.client.gui.select.GuiAnimationSelection; +import noppes.npcs.client.gui.select.GuiSoundSelection; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Declarative field definition for ability GUI rendering. + * Built via static factories and chaining methods. + */ +public class FieldDef { + + private final String label; + private final FieldType type; + private TabTarget tab = TabTarget.TYPE; + private String customTabName = null; + private ColumnHint column = ColumnHint.FULL; + private Supplier getter; + private Consumer setter; + private BooleanSupplier visibleWhen = () -> true; + private BooleanSupplier enabledWhen = () -> true; + private String tooltip = null; + private String hoverText = null; + + // Numeric range + private float min = Float.MIN_VALUE; + private float max = Float.MAX_VALUE; + + // Enum support + private Class> enumClass; + private String[] stringEnumValues; + private Supplier stringEnumGetter; + private Consumer stringEnumSetter; + + // Sub-gui support + private Supplier subGuiFactory; + private Consumer subGuiResultHandler; + + // Button display + private Supplier buttonLabelSupplier; + private Supplier buttonTextColorSupplier; + private Runnable clearAction; + + private FieldDef(String label, FieldType type) { + this.label = label; + this.type = type; + } + + // ═══════════════════════════════════════════════════════════════════ + // STATIC FACTORIES + // ═══════════════════════════════════════════════════════════════════ + + public static FieldDef section(String label) { + return new FieldDef(label, FieldType.SECTION_HEADER); + } + + public static FieldDef floatField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.FLOAT); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept(((Number) v).floatValue()); + return def; + } + + public static FieldDef intField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.INT); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept(((Number) v).intValue()); + return def; + } + + public static FieldDef boolField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.BOOLEAN); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept((Boolean) v); + return def; + } + + @SuppressWarnings("unchecked") + public static > FieldDef enumField(String label, Class enumClass, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.ENUM); + def.enumClass = enumClass; + def.getter = () -> getter.get(); + def.setter = v -> setter.accept((E) v); + return def; + } + + public static FieldDef stringEnumField(String label, String[] values, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.STRING_ENUM); + def.stringEnumValues = values; + def.stringEnumGetter = getter; + def.stringEnumSetter = setter; + return def; + } + + public static FieldDef stringField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.STRING); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept((String) v); + return def; + } + + public static FieldDef labelField(String label, Supplier textSupplier) { + FieldDef def = new FieldDef(label, FieldType.LABEL); + def.getter = () -> textSupplier.get(); + return def; + } + + @SuppressWarnings("unchecked") + public static FieldDef effectsListField(String label, Supplier> getter, Consumer> setter) { + FieldDef def = new FieldDef(label, FieldType.EFFECTS_LIST); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept((List) v); + return def; + } + + public static FieldDef subGuiField(String label, Supplier factory, Consumer resultHandler) { + FieldDef def = new FieldDef(label, FieldType.SUB_GUI); + def.subGuiFactory = factory; + def.subGuiResultHandler = resultHandler; + return def; + } + + // ═══════════════════════════════════════════════════════════════════ + // CONVENIENCE FACTORIES (SUB_GUI wrappers) + // ═══════════════════════════════════════════════════════════════════ + + public static FieldDef colorSubGui(String label, Supplier getter, Consumer setter) { + return subGuiField(label, + () -> new SubGuiColorSelector(getter.get()), + gui -> setter.accept(((SubGuiColorSelector) gui).color & 0x00FFFFFF)) + .buttonLabel(() -> String.format("%06X", getter.get() & 0xFFFFFF)) + .buttonTextColor(() -> getter.get() & 0xFFFFFF); + } + + public static FieldDef soundSubGui(String label, Supplier getter, Consumer setter) { + return subGuiField(label, + () -> new GuiSoundSelection(getter.get()), + gui -> { + GuiSoundSelection s = (GuiSoundSelection) gui; + if (s.selectedResource != null) setter.accept(s.selectedResource.toString()); + }) + .buttonLabel(() -> { + String s = getter.get(); + return s == null || s.isEmpty() ? "gui.none" : s; + }) + .clearable(() -> setter.accept("")); + } + + public static FieldDef animSubGui(String label, + Supplier idGetter, Consumer idSetter, + Supplier nameGetter, Consumer nameSetter) { + return subGuiField(label, + () -> new GuiAnimationSelection(idGetter.get(), nameGetter.get()), + gui -> { + GuiAnimationSelection sel = (GuiAnimationSelection) gui; + if (sel.isBuiltInSelected()) { + nameSetter.accept(sel.selectedBuiltInName); + idSetter.accept(-1); + } else { + idSetter.accept(sel.selectedAnimationId); + nameSetter.accept(""); + } + }) + .buttonLabel(() -> { + String name = nameGetter.get(); + if (name != null && !name.isEmpty()) return name; + int id = idGetter.get(); + return id >= 0 ? "ID: " + id : "gui.none"; + }) + .clearable(() -> { idSetter.accept(-1); nameSetter.accept(""); }); + } + + // ═══════════════════════════════════════════════════════════════════ + // CHAINING METHODS + // ═══════════════════════════════════════════════════════════════════ + + public FieldDef tab(TabTarget tab) { + this.tab = tab; + return this; + } + + public FieldDef customTab(String tabName) { + this.tab = TabTarget.CUSTOM; + this.customTabName = tabName; + return this; + } + + public FieldDef range(float min, float max) { + this.min = min; + this.max = max; + return this; + } + + public FieldDef column(ColumnHint column) { + this.column = column; + return this; + } + + public FieldDef visibleWhen(BooleanSupplier condition) { + this.visibleWhen = condition; + return this; + } + + public FieldDef enabledWhen(BooleanSupplier condition) { + this.enabledWhen = condition; + return this; + } + + public FieldDef tooltip(String tooltip) { + this.tooltip = tooltip; + return this; + } + + public FieldDef hover(String hoverText) { + this.hoverText = hoverText; + return this; + } + + public FieldDef buttonLabel(Supplier supplier) { + this.buttonLabelSupplier = supplier; + return this; + } + + public FieldDef buttonTextColor(Supplier supplier) { + this.buttonTextColorSupplier = supplier; + return this; + } + + public FieldDef clearable(Runnable action) { + this.clearAction = action; + return this; + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS + // ═══════════════════════════════════════════════════════════════════ + + public String getLabel() { return label; } + public FieldType getType() { return type; } + public TabTarget getTab() { return tab; } + public String getCustomTabName() { return customTabName; } + public ColumnHint getColumn() { return column; } + public boolean isVisible() { return visibleWhen.getAsBoolean(); } + public boolean isEnabled() { return enabledWhen.getAsBoolean(); } + public String getTooltip() { return tooltip; } + public String getHoverText() { return hoverText; } + public float getMin() { return min; } + public float getMax() { return max; } + public boolean hasRange() { return min != Float.MIN_VALUE || max != Float.MAX_VALUE; } + public Class> getEnumClass() { return enumClass; } + public String[] getStringEnumValues() { return stringEnumValues; } + public Supplier getSubGuiFactory() { return subGuiFactory; } + public Consumer getSubGuiResultHandler() { return subGuiResultHandler; } + public boolean hasClearAction() { return clearAction != null; } + public Runnable getClearAction() { return clearAction; } + + public Object getValue() { + return getter != null ? getter.get() : null; + } + + public void setValue(Object value) { + if (setter != null) setter.accept(value); + } + + public String getStringEnumValue() { + return stringEnumGetter != null ? stringEnumGetter.get() : null; + } + + public void setStringEnumValue(String value) { + if (stringEnumSetter != null) stringEnumSetter.accept(value); + } + + public String getButtonLabel() { + if (buttonLabelSupplier != null) return buttonLabelSupplier.get(); + return label; + } + + public Integer getButtonTextColor() { + return buttonTextColorSupplier != null ? buttonTextColorSupplier.get() : null; + } + +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDefinitionsBuilder.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDefinitionsBuilder.java new file mode 100644 index 000000000..1ba3f2694 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDefinitionsBuilder.java @@ -0,0 +1,359 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.GuiScrollWindow; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Reusable builder that converts a list of {@link FieldDef} into positioned + * GUI widgets inside a {@link GuiScrollWindow}. + */ +public class FieldDefinitionsBuilder { + + // Layout defaults + private int contentRight = 330; + private int colLLabel = 5; + private int colLField = 75; + private int colLWidth = 70; + private int colRLabel = 178; + private int colRField = 248; + private int colRWidth = 70; + private int rowHeight = 24; + private int labelPadding = 8; + + // Dependencies + private final GuiScreen parent; + private final GuiScrollWindow sw; + private final FontRenderer fontRenderer; + + // ID counters + private int widgetId; + private int clearId; + private int labelId; + + // Starting Y + private int startY = 5; + + // Output maps + private final Map buttonFieldMap = new HashMap<>(); + private final Map textFieldMap = new HashMap<>(); + private final Map clearFieldMap = new HashMap<>(); + + // Effects list metadata: widgetId -> [effectIndex, action] + // action: 0=type, 1=duration(text), 2=amp, 3=delete, 4=add + private final Map effectWidgetMeta = new HashMap<>(); + + public FieldDefinitionsBuilder(GuiScreen parent, GuiScrollWindow sw, FontRenderer fontRenderer) { + this.parent = parent; + this.sw = sw; + this.fontRenderer = fontRenderer; + } + + // ═══════════════════════════════════════════════════════════════════ + // CONFIG CHAINING + // ═══════════════════════════════════════════════════════════════════ + + public FieldDefinitionsBuilder contentRight(int v) { this.contentRight = v; return this; } + public FieldDefinitionsBuilder startIds(int widget, int clear, int label) { + this.widgetId = widget; this.clearId = clear; this.labelId = label; return this; + } + public FieldDefinitionsBuilder startY(int y) { this.startY = y; return this; } + public FieldDefinitionsBuilder rowHeight(int h) { this.rowHeight = h; return this; } + + // ═══════════════════════════════════════════════════════════════════ + // BUILD + // ═══════════════════════════════════════════════════════════════════ + + /** + * Processes all fields and adds widgets to the scroll window. + * @return the final Y position after all fields + */ + public int build(List fields) { + buttonFieldMap.clear(); + textFieldMap.clear(); + clearFieldMap.clear(); + effectWidgetMeta.clear(); + + int y = startY; + + for (int i = 0; i < fields.size(); i++) { + FieldDef def = fields.get(i); + + // Section header + if (def.getType() == FieldType.SECTION_HEADER) { + y += 3; + String sectionText = StatCollector.translateToLocal(def.getLabel()); + if (def.getTooltip() != null && !def.getTooltip().isEmpty()) { + sectionText += " (" + StatCollector.translateToLocal(def.getTooltip()) + ")"; + } + sw.addLabel(new GuiNpcLabel(labelId++, sectionText, colLLabel, y + 2, 0xFFFF55)); + y += 15; + continue; + } + + // Effects list renders its own multi-row block + if (def.getType() == FieldType.EFFECTS_LIST) { + y = renderEffectsList(def, y); + continue; + } + + // Two-column pair: LEFT followed by RIGHT + if (def.getColumn() == ColumnHint.LEFT + && i + 1 < fields.size() + && fields.get(i + 1).getColumn() == ColumnHint.RIGHT + && fields.get(i + 1).getType() != FieldType.SECTION_HEADER) { + renderFieldAt(def, colLLabel, colLField, colLWidth, y); + + i++; + FieldDef rightDef = fields.get(i); + renderFieldAt(rightDef, colRLabel, colRField, colRWidth, y); + + y += rowHeight; + } else { + // Full-width: compute fieldX from label pixel width + int fieldW = getFullFieldWidth(def); + String translated = StatCollector.translateToLocal(def.getLabel()); + int labelW = fontRenderer.getStringWidth(translated); + int fieldX = colLLabel + labelW + labelPadding; + + renderFieldAt(def, colLLabel, fieldX, fieldW, y); + y += rowHeight; + } + } + + return y; + } + + // ═══════════════════════════════════════════════════════════════════ + // FIELD RENDERING + // ═══════════════════════════════════════════════════════════════════ + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void renderFieldAt(FieldDef def, int labelX, int fieldX, int fieldW, int y) { + sw.addLabel(new GuiNpcLabel(labelId++, def.getLabel(), labelX, y + 5, 0xFFFFFF)); + + String hover = def.getHoverText() != null ? def.getHoverText() : def.getTooltip(); + + switch (def.getType()) { + case FLOAT: { + float fVal = def.getValue() instanceof Number ? ((Number) def.getValue()).floatValue() : 0f; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(fVal)); + tf.setFloatsOnly(); + if (def.hasRange()) tf.setMinMaxDefaultFloat(def.getMin(), def.getMax(), fVal); + if (!def.isEnabled()) tf.setEnabled(false); + if (hover != null) tf.setHoverText(hover); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case INT: { + int iVal = def.getValue() instanceof Number ? ((Number) def.getValue()).intValue() : 0; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(iVal)); + tf.setIntegersOnly(); + if (def.hasRange()) tf.setMinMaxDefault((int) def.getMin(), (int) def.getMax(), iVal); + if (!def.isEnabled()) tf.setEnabled(false); + if (hover != null) tf.setHoverText(hover); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case STRING: { + String sVal = def.getValue() != null ? def.getValue().toString() : ""; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, sVal); + if (!def.isEnabled()) tf.setEnabled(false); + if (hover != null) tf.setHoverText(hover); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case LABEL: { + String text = def.getValue() != null ? def.getValue().toString() : ""; + sw.addLabel(new GuiNpcLabel(labelId++, text, fieldX, y + 5, 0xAAAAAA)); + widgetId++; + clearId++; + break; + } + case BOOLEAN: { + boolean bVal = def.getValue() instanceof Boolean ? (Boolean) def.getValue() : false; + GuiNpcButtonYesNo btn = new GuiNpcButtonYesNo(widgetId, fieldX, y, fieldW, 20, bVal); + if ("gui.enabled".equals(def.getLabel()) && def.getTab() == TabTarget.GENERAL) { + btn.setTextColor(bVal ? 0x00FF00 : 0xFF0000); + } + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case ENUM: { + Class> enumClass = def.getEnumClass(); + if (enumClass != null) { + Enum[] constants = enumClass.getEnumConstants(); + String[] names = new String[constants.length]; + for (int i = 0; i < constants.length; i++) names[i] = constants[i].toString(); + int selected = def.getValue() instanceof Enum ? ((Enum) def.getValue()).ordinal() : 0; + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, fieldW, 20, names, selected); + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + } + widgetId++; + clearId++; + break; + } + case STRING_ENUM: { + String[] values = def.getStringEnumValues(); + if (values != null && values.length > 0) { + String curVal = def.getStringEnumValue(); + int selected = 0; + for (int i = 0; i < values.length; i++) { + if (values[i].equals(curVal)) { selected = i; break; } + } + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, fieldW, 20, values, selected); + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + } + widgetId++; + clearId++; + break; + } + case SUB_GUI: { + String btnText = def.getButtonLabel(); + int btnW; + if (def.hasClearAction()) { + btnW = contentRight - 22 - fieldX; + GuiNpcButton clearBtn = new GuiNpcButton(clearId, contentRight - 20, y, 20, 20, "X"); + sw.addButton(clearBtn); + clearFieldMap.put(clearId, def); + } else { + btnW = contentRight - fieldX; + } + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, btnW, 20, btnText); + Integer textColor = def.getButtonTextColor(); + if (textColor != null) btn.setTextColor(textColor); + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // EFFECTS LIST RENDERING + // ═══════════════════════════════════════════════════════════════════ + + @SuppressWarnings("unchecked") + private int renderEffectsList(FieldDef def, int y) { + // Section header + y += 3; + sw.addLabel(new GuiNpcLabel(labelId++, def.getLabel(), colLLabel, y + 2, 0xFFFF55)); + y += 15; + + List effects = (List) def.getValue(); + if (effects == null) effects = new ArrayList<>(); + + String[] typeNames = AbilityEffect.EffectType.getLangKeys(); + String[] ampValues = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}; + + for (int e = 0; e < effects.size() && e < 5; e++) { + AbilityEffect effect = effects.get(e); + + // Type selector button + int typeIdx = effect.getType().ordinal(); + GuiNpcButton typeBtn = new GuiNpcButton(widgetId, colLLabel, y, 100, 20, typeNames, typeIdx); + sw.addButton(typeBtn); + buttonFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 0}); + widgetId++; + + // Duration text field + GuiNpcTextField durField = new GuiNpcTextField(widgetId, parent, fontRenderer, + colLLabel + 104, y, 50, 20, String.valueOf(effect.getDurationTicks())); + durField.setIntegersOnly(); + durField.setMinMaxDefault(1, 12000, 60); + sw.addTextField(durField); + textFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 1}); + widgetId++; + + // Amplifier selector (0-10) + GuiNpcButton ampBtn = new GuiNpcButton(widgetId, colLLabel + 158, y, 40, 20, ampValues, effect.getAmplifier()); + sw.addButton(ampBtn); + buttonFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 2}); + widgetId++; + + // Delete button + GuiNpcButton delBtn = new GuiNpcButton(clearId, colLLabel + 202, y, 20, 20, "X"); + sw.addButton(delBtn); + clearFieldMap.put(clearId, def); + effectWidgetMeta.put(clearId, new int[]{e, 3}); + clearId++; + + y += rowHeight; + } + + // Add button (if < 5 effects) + if (effects.size() < 5) { + GuiNpcButton addBtn = new GuiNpcButton(widgetId, colLLabel, y, 50, 20, "gui.add"); + sw.addButton(addBtn); + buttonFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{effects.size(), 4}); + widgetId++; + clearId++; + y += rowHeight; + } + + return y; + } + + private int getFullFieldWidth(FieldDef def) { + switch (def.getType()) { + case FLOAT: + case INT: return 60; + case STRING: return 200; + case BOOLEAN: return 50; + case LABEL: return 60; + case ENUM: + case STRING_ENUM: return 120; + case SUB_GUI: return def.hasClearAction() ? 175 : 200; + default: return 80; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS + // ═══════════════════════════════════════════════════════════════════ + + public Map getButtonFieldMap() { return buttonFieldMap; } + public Map getTextFieldMap() { return textFieldMap; } + public Map getClearFieldMap() { return clearFieldMap; } + public Map getEffectWidgetMeta() { return effectWidgetMeta; } + public int getNextLabelId() { return labelId; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldType.java new file mode 100644 index 000000000..019b24bdf --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldType.java @@ -0,0 +1,14 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +public enum FieldType { + FLOAT, + INT, + BOOLEAN, + ENUM, + STRING_ENUM, + SUB_GUI, + STRING, + SECTION_HEADER, + LABEL, + EFFECTS_LIST +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabDef.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabDef.java new file mode 100644 index 000000000..64d1501fd --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabDef.java @@ -0,0 +1,28 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +/** + * Simple data class representing a tab in the ability config GUI. + * Built automatically from the FieldDef list. + */ +public class TabDef { + + public final String name; + public final TabTarget target; + public int index; + + public TabDef(String name, TabTarget target, int index) { + this.name = name; + this.target = target; + this.index = index; + } + + /** + * Returns a unique key combining target and custom name for deduplication. + */ + public String getKey() { + if (target == TabTarget.CUSTOM && name != null) { + return "CUSTOM:" + name; + } + return target.name(); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabTarget.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabTarget.java new file mode 100644 index 000000000..978f15224 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabTarget.java @@ -0,0 +1,9 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +public enum TabTarget { + GENERAL, + TYPE, + TARGET, + EFFECTS, + CUSTOM +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java deleted file mode 100644 index dee9a249a..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java +++ /dev/null @@ -1,24 +0,0 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; - -/** - * Types of telegraph shapes for visual attack warnings. - */ -public enum TelegraphType { - /** Filled circle (uses radius) */ - CIRCLE, - - /** Donut shape (uses radius and innerRadius) */ - RING, - - /** Rectangle (uses length, width) */ - LINE, - - /** Fan/wedge shape (uses length, angle) */ - CONE, - - /** Single point indicator */ - POINT, - - /** No telegraph */ - NONE -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java deleted file mode 100644 index f094a4f0d..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ /dev/null @@ -1,288 +0,0 @@ -package kamkeel.npcs.controllers.data.ability.type; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; - -import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityBeam; -import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; -import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityLivingBase; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; -import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Beam ability: Continuous line attack that damages all targets in a line. - * Can sweep across an arc or remain fixed. - */ -public class AbilityBeam extends Ability { - - // Type-specific parameters - private float beamLength = 10.0f; - private float beamWidth = 1.0f; - private float damage = 5.0f; - private int damageInterval = 5; - private boolean piercing = true; - private float sweepAngle = 0.0f; - private float sweepSpeed = 2.0f; - private boolean sweepBackAndForth = true; - private boolean lockOnTarget = false; - - // Runtime state (transient) - private transient float currentSweepAngle = 0; - private transient boolean sweepingRight = true; - private transient int ticksSinceDamage = 0; - private transient Set hitThisTick = new HashSet<>(); - - public AbilityBeam() { - this.typeId = "ability.cnpc.beam"; - this.name = "Beam"; - this.targetingMode = TargetingMode.AGGRO_TARGET; - this.maxRange = 15.0f; - this.minRange = 0.0f; - this.lockMovement = true; - this.cooldownTicks = 200; - this.windUpTicks = 60; - this.activeTicks = 80; - this.recoveryTicks = 20; - // No telegraph for beam - it's a continuous line attack - this.telegraphType = TelegraphType.NONE; - this.showTelegraph = false; - } - - @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityBeam(this, callback); - } - - @Override - public boolean isTargetingModeLocked() { return true; } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; - } - - @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - // Initialize beam - currentSweepAngle = -sweepAngle / 2; - sweepingRight = true; - ticksSinceDamage = damageInterval; // Deal damage immediately - hitThisTick.clear(); - } - - @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; - - hitThisTick.clear(); - ticksSinceDamage++; - - // Update sweep angle - if (sweepAngle > 0) { - if (sweepBackAndForth) { - if (sweepingRight) { - currentSweepAngle += sweepSpeed; - if (currentSweepAngle >= sweepAngle / 2) { - sweepingRight = false; - } - } else { - currentSweepAngle -= sweepSpeed; - if (currentSweepAngle <= -sweepAngle / 2) { - sweepingRight = true; - } - } - } else { - currentSweepAngle += sweepSpeed; - if (currentSweepAngle >= sweepAngle) { - currentSweepAngle = 0; - } - } - } - - // Calculate beam direction - float baseYaw = npc.rotationYaw; - - // If lock on target, point at target - if (lockOnTarget && target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; - baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); - } - - float beamYaw = baseYaw + currentSweepAngle; - float yawRad = (float) Math.toRadians(beamYaw); - - // Calculate beam start position - double startX = npc.posX; - double startY = npc.posY + npc.getEyeHeight() * 0.8; - double startZ = npc.posZ; - - double dirX = -Math.sin(yawRad); - double dirZ = Math.cos(yawRad); - - // Check for entities along the beam - if (ticksSinceDamage >= damageInterval) { - ticksSinceDamage = 0; - - // Sample points along the beam - int samples = (int) (beamLength / 0.5); - for (int i = 1; i <= samples; i++) { - double progress = (double) i / samples; - double checkX = startX + dirX * beamLength * progress; - double checkY = startY; - double checkZ = startZ + dirZ * beamLength * progress; - - // Find entities at this point - AxisAlignedBB checkBox = AxisAlignedBB.getBoundingBox( - checkX - beamWidth, checkY - 1, checkZ - beamWidth, - checkX + beamWidth, checkY + 2, checkZ + beamWidth - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, checkBox); - - for (Entity entity : entities) { - if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; - if (hitThisTick.contains(entity.getEntityId())) continue; - - EntityLivingBase livingEntity = (EntityLivingBase) entity; - - // Check if actually in beam path - if (isInBeamPath(livingEntity, startX, startY, startZ, dirX, dirZ)) { - hitThisTick.add(entity.getEntityId()); - // Apply damage with scripted event support (no knockback for beam) - applyAbilityDamage(npc, livingEntity, damage, 0); - - if (!piercing) { - return; // Stop at first hit - } - } - } - } - } - - // Spawn beam particles - spawnBeamParticles(world, startX, startY, startZ, dirX, dirZ); - } - - private boolean isInBeamPath(EntityLivingBase entity, double startX, double startY, double startZ, - double dirX, double dirZ) { - // Project entity position onto beam line - double entityX = entity.posX - startX; - double entityZ = entity.posZ - startZ; - - // Dot product to find projection distance - double projectionDist = entityX * dirX + entityZ * dirZ; - - // Check if within beam length - if (projectionDist < 0 || projectionDist > beamLength) { - return false; - } - - // Calculate perpendicular distance - double projX = dirX * projectionDist; - double projZ = dirZ * projectionDist; - double perpX = entityX - projX; - double perpZ = entityZ - projZ; - double perpDist = Math.sqrt(perpX * perpX + perpZ * perpZ); - - // Check if within beam width - return perpDist <= beamWidth + entity.width / 2; - } - - private void spawnBeamParticles(World world, double startX, double startY, double startZ, - double dirX, double dirZ) { - // Spawn particles along beam line - for (int i = 0; i < beamLength; i++) { - double px = startX + dirX * i; - double pz = startZ + dirZ * i; - world.spawnParticle("flame", px, startY, pz, 0, 0, 0); - } - } - - @Override - public void reset() { - super.reset(); - currentSweepAngle = 0; - sweepingRight = true; - ticksSinceDamage = 0; - hitThisTick.clear(); - } - - @Override - public float getTelegraphRadius() { - return beamLength; - } - - @Override - public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("beamLength", beamLength); - nbt.setFloat("beamWidth", beamWidth); - nbt.setFloat("damage", damage); - nbt.setInteger("damageInterval", damageInterval); - nbt.setBoolean("piercing", piercing); - nbt.setFloat("sweepAngle", sweepAngle); - nbt.setFloat("sweepSpeed", sweepSpeed); - nbt.setBoolean("sweepBackAndForth", sweepBackAndForth); - nbt.setBoolean("lockOnTarget", lockOnTarget); - } - - @Override - public void readTypeNBT(NBTTagCompound nbt) { - this.beamLength = nbt.hasKey("beamLength") ? nbt.getFloat("beamLength") : 10.0f; - this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 1.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 5.0f; - this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 5; - this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); - this.sweepAngle = nbt.hasKey("sweepAngle") ? nbt.getFloat("sweepAngle") : 0.0f; - this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 2.0f; - this.sweepBackAndForth = !nbt.hasKey("sweepBackAndForth") || nbt.getBoolean("sweepBackAndForth"); - this.lockOnTarget = nbt.hasKey("lockOnTarget") && nbt.getBoolean("lockOnTarget"); - } - - // Getters & Setters - public float getBeamLength() { return beamLength; } - public void setBeamLength(float beamLength) { this.beamLength = beamLength; } - - public float getBeamWidth() { return beamWidth; } - public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } - - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } - - public int getDamageInterval() { return damageInterval; } - public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; } - - public boolean isPiercing() { return piercing; } - public void setPiercing(boolean piercing) { this.piercing = piercing; } - - public float getSweepAngle() { return sweepAngle; } - public void setSweepAngle(float sweepAngle) { this.sweepAngle = sweepAngle; } - - public float getSweepSpeed() { return sweepSpeed; } - public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; } - - public boolean isSweepBackAndForth() { return sweepBackAndForth; } - public void setSweepBackAndForth(boolean sweepBackAndForth) { this.sweepBackAndForth = sweepBackAndForth; } - - public boolean isLockOnTarget() { return lockOnTarget; } - public void setLockOnTarget(boolean lockOnTarget) { this.lockOnTarget = lockOnTarget; } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java new file mode 100644 index 000000000..62865721f --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java @@ -0,0 +1,493 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.util.ValueUtil; + +import java.util.Arrays; +import java.util.List; + +public class AbilityBeamDual extends Ability { + private float beamWidth = 0.4f; + private float headSize = 0.6f; + private boolean dualFire= true; + private int dualFireDelay = 0; + + // Data classes + private EnergyColorData[] colorData = new EnergyColorData[]{ + new EnergyColorData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f), + new EnergyColorData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 6.0f), + }; + private EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f); + private EnergyLightningData[] lightningData = new EnergyLightningData[]{ + new EnergyLightningData(), new EnergyLightningData() + }; + private EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200); + private EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ + new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND) + }; + + // Transient state for beam entity (used during windup charging) + private transient EntityAbilityBeam beamEntity = null; + private transient EntityAbilityBeam beamEntity2 = null; + + public AbilityBeamDual() { + this.typeId = "ability.cnpc.beam_dual"; + this.name = "Beam Dual"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 20.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 80; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_BeamDual_Windup"; + this.activeAnimationName = "Ability_BeamDual_Active"; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start firing the beam that was spawned during windup + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.startFiring(target); + } + + if (dualFire) { + if (beamEntity2 != null && !beamEntity2.isDead && dualFireDelay <= 0) { + beamEntity2.startFiring(target); + } + } else { + beamEntity2.setDead(); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn beam in charging mode on first tick of windup + if (tick == 1) { + float offsetDist = 1.0f; + + // Create beam in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0], offsetDist); + beamEntity = new EntityAbilityBeam( + world, caster, target, + spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, + beamWidth, headSize, + colorData[0], combatData, homingData, lightningData[0], lifespanData, + lockMovement.locksActive()); + + Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1], offsetDist); + beamEntity2 = new EntityAbilityBeam( + world, caster, target, + spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, + beamWidth, headSize, + colorData[1], combatData, homingData, lightningData[1], lifespanData, + lockMovement.locksActive()); + + beamEntity.setSiblingUUID(beamEntity2.getPersistentID()); + beamEntity2.setSiblingUUID(beamEntity.getPersistentID()); + + beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist); + beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist); + + beamEntity.setEffects(this.effects); + if (dualFire) beamEntity2.setEffects(this.effects); + + world.spawnEntityInWorld(beamEntity); + world.spawnEntityInWorld(beamEntity2); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (dualFire) { + if ((beamEntity == null || beamEntity.isDead) && (beamEntity2 == null || beamEntity2.isDead)) { + beamEntity = null; + beamEntity2 = null; + signalCompletion(); + } + + if (tick == dualFireDelay && beamEntity2 != null && !beamEntity2.isDead) { + beamEntity2.startFiring(target); + } + } else { + if (beamEntity == null || beamEntity.isDead) { + beamEntity = null; + signalCompletion(); + } + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void cleanup() { + // Despawn beam entity if still alive + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.setDead(); + } + + if (beamEntity2 != null && !beamEntity2.isDead) { + beamEntity2.setDead(); + } + + beamEntity = null; + beamEntity2 = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(headSize * 2.0f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return headSize * 2.0f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("headSize", headSize); + nbt.setBoolean("dualFire", dualFire); + nbt.setInteger("dualFireDelay", dualFireDelay); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + + NBTTagCompound beam1 = new NBTTagCompound(); + NBTTagCompound beam2 = new NBTTagCompound(); + + anchorData[0].writeNBT(beam1); + anchorData[1].writeNBT(beam2); + colorData[0].writeNBT(beam1); + colorData[1].writeNBT(beam2); + lightningData[0].writeNBT(beam1); + lightningData[1].writeNBT(beam2); + + nbt.setTag("Beam_1", beam1); + nbt.setTag("Beam_2", beam2); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + NBTTagCompound beam1 = nbt.hasKey("Beam_1") ? nbt.getCompoundTag("Beam_1") : new NBTTagCompound(); + NBTTagCompound beam2 = nbt.hasKey("Beam_2") ? nbt.getCompoundTag("Beam_2") : new NBTTagCompound(); + + this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; + this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; + this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); + this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lifespanData.readNBT(nbt); + + anchorData[0].readNBT(beam1); + anchorData[1].readNBT(beam2); + colorData[0].readNBT(beam1); + colorData[1].readNBT(beam2); + lightningData[0].readNBT(beam1); + lightningData[1].readNBT(beam2); + } + + // Getters & Setters - Standalone fields + public float getBeamWidth() { return beamWidth; } + public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public float getHeadSize() { return headSize; } + public void setHeadSize(float headSize) { this.headSize = headSize; } + public boolean isDualFire() { return dualFire; } + public void setDualFire(boolean dualFire) { this.dualFire = dualFire; } + public int getDualFireDelay() { return dualFireDelay; } + public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; } + + private EnergyColorData getColorData(int beam) { + beam = ValueUtil.clamp(beam, 0, 1); + return colorData[beam]; + } + private EnergyLightningData getLightningData(int beam) { + beam = ValueUtil.clamp(beam, 0, 1); + return lightningData[beam]; + } + private EnergyAnchorData getAnchorData(int beam) { + beam = ValueUtil.clamp(beam, 0, 1); + return anchorData[beam]; + } + + // Getters & Setters - Color data + public int getInnerColor(int beam) { return getColorData(beam).innerColor; } + public void setInnerColor(int beam, int innerColor) { getColorData(beam).innerColor = innerColor; } + public int getOuterColor(int beam) { return getColorData(beam).outerColor; } + public void setOuterColor(int beam, int outerColor) { getColorData(beam).outerColor = outerColor; } + public boolean isOuterColorEnabled(int beam) { return getColorData(beam).outerColorEnabled; } + public void setOuterColorEnabled(int beam, boolean enabled) { getColorData(beam).outerColorEnabled = enabled; } + public float getOuterColorWidth(int beam) { return getColorData(beam).outerColorWidth; } + public void setOuterColorWidth(int beam, float width) { getColorData(beam).outerColorWidth = width; } + public float getOuterColorAlpha(int beam) { return getColorData(beam).outerColorAlpha; } + public void setOuterColorAlpha(int beam, float alpha) { getColorData(beam).outerColorAlpha = alpha; } + public float getRotationSpeed(int beam) { return getColorData(beam).rotationSpeed; } + public void setRotationSpeed(int beam, float speed) { getColorData(beam).rotationSpeed = speed; } + + + public int getInnerColor() { return getInnerColor(0); } + public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } + public int getOuterColor() { return getOuterColor(0); } + public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } + public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } + public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } + public float getOuterColorWidth() { return getOuterColorWidth(0); } + public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } + public float getOuterColorAlpha() { return getOuterColorAlpha(0); } + public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } + public float getRotationSpeed() { return getRotationSpeed(0); } + public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } + + // Getters & Setters - Combat data + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { this.combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { this.combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } + + // Getters & Setters - Homing data + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { this.homingData.speed = speed; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { this.homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; } + + // Getters & Setters - Lightning data + public boolean hasLightningEffect(int beam) { return getLightningData(beam).lightningEffect; } + public void setLightningEffect(int beam, boolean lightningEffect) { getLightningData(beam).lightningEffect = lightningEffect; } + public float getLightningDensity(int beam) { return getLightningData(beam).lightningDensity; } + public void setLightningDensity(int beam, float lightningDensity) { getLightningData(beam).lightningDensity = lightningDensity; } + public float getLightningRadius(int beam) { return getLightningData(beam).lightningRadius; } + public void setLightningRadius(int beam, float lightningRadius) { getLightningData(beam).lightningRadius = lightningRadius; } + + public boolean hasLightningEffect() { return hasLightningEffect(0); } + public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } + public float getLightningDensity() { return getLightningDensity(0); } + public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } + public float getLightningRadius() { return getLightningRadius(0); } + public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } + + + // Getters & Setters - Lifespan data + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } + + // Getters & Setters - Anchor point + public AnchorPoint getAnchorPointEnum(int beam) { return getAnchorData(beam).anchorPoint; } + public void setAnchorPointEnum(int beam, AnchorPoint anchorPoint) { getAnchorData(beam).anchorPoint = anchorPoint; } + public float getAnchorOffsetX(int beam) { return getAnchorData(beam).anchorOffsetX; } + public void setAnchorOffsetX(int beam, float x) { getAnchorData(beam).anchorOffsetX = x; } + public float getAnchorOffsetY(int beam) { return getAnchorData(beam).anchorOffsetY; } + public void setAnchorOffsetY(int beam, float y) { getAnchorData(beam).anchorOffsetY = y; } + public float getAnchorOffsetZ(int beam) { return getAnchorData(beam).anchorOffsetZ; } + public void setAnchorOffsetZ(int beam, float z) { getAnchorData(beam).anchorOffsetZ = z; } + + public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } + public float getAnchorOffsetX() { return getAnchorOffsetX(0); } + public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } + public float getAnchorOffsetY() { return getAnchorOffsetY(0); } + public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } + public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } + public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } + + //@Override + public int getAnchorPoint(int beam) { return getAnchorData(beam).anchorPoint.ordinal(); } + public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } + + //@Override + public void setAnchorPoint(int beam, int point) { getAnchorData(beam).anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.section("ability.section.beam"), + FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth).column(ColumnHint.LEFT), + FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.dual"), + FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire), + FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay) + .visibleWhen(this::isDualFire), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming).column(ColumnHint.LEFT), + FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange) + .visibleWhen(this::isHoming).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab - Beam 1 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.beam1").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(0), v -> setInnerColor(0, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v)) + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(0), v -> setOuterColor(0, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v)) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(0), v -> setRotationSpeed(0, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(0), v -> setLightningEffect(0, v)) + .customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(0), v -> setLightningDensity(0, v)) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(0), v -> setLightningRadius(0, v)) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.RIGHT), + // Visual tab - Beam 2 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.beam2").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(1), v -> setInnerColor(1, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v)) + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(1), v -> setOuterColor(1, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v)) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(1), v -> setRotationSpeed(1, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(1), v -> setLightningEffect(1, v)) + .customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(1), v -> setLightningDensity(1, v)) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(1), v -> setLightningRadius(1, v)) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.RIGHT) + ); + } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj); + beam.setupPreview(npc, beamWidth, headSize, colorData[0], lightningData[0], anchorData[0], windUpTicks, 1.0f); + return beam; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 41e288c5b..d345fb992 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -3,22 +3,25 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCharge; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; +import noppes.npcs.api.ability.type.IAbilityCharge; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -26,11 +29,11 @@ import java.util.Set; /** * Charge ability: Rush attack where NPC charges in a line, damaging all targets hit. */ -public class AbilityCharge extends Ability { +public class AbilityCharge extends Ability implements IAbilityCharge { // Type-specific parameters private float chargeSpeed = 0.8f; - private float damage = 15.0f; + private float damage = 8.0f; private float knockback = 3.0f; private float hitWidth = 1.5f; @@ -46,11 +49,9 @@ public class AbilityCharge extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 20.0f; this.minRange = 4.0f; - this.lockMovement = false; // Movement IS the ability - this.cooldownTicks = 80; + this.lockMovement = LockMovementType.WINDUP; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 40; - this.recoveryTicks = 20; // LINE telegraph showing charge path this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; @@ -59,20 +60,18 @@ public class AbilityCharge extends Ability { } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityCharge(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -84,104 +83,106 @@ public class AbilityCharge extends Ability { * Called on the first tick of windup - lock direction here so it matches telegraph. */ @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (tick == 1) { // Lock direction on first windup tick (same time telegraph is created) - lockChargeDirection(npc, target); + lockChargeDirection(caster, target); } - // Keep NPC facing the locked direction during windup - enforceLockedRotation(npc); + // Keep caster facing the locked direction during windup + enforceLockedRotation(caster); } /** * Locks the charge direction based on current target position. * Called once at windup start - direction won't change even if target moves. */ - private void lockChargeDirection(EntityNPCInterface npc, EntityLivingBase target) { + private void lockChargeDirection(EntityLivingBase caster, EntityLivingBase target) { if (target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dz * dz); if (len > 0) { chargeDirection = Vec3.createVectorHelper(dx / len, 0, dz / len); } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); + float yaw = (float) Math.toRadians(caster.rotationYaw); chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); } } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); + float yaw = (float) Math.toRadians(caster.rotationYaw); chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); } lockedYaw = (float) Math.toDegrees(Math.atan2(-chargeDirection.xCoord, chargeDirection.zCoord)); } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Initialize charge - direction was already locked during windup - startX = npc.posX; - startY = npc.posY; - startZ = npc.posZ; + startX = caster.posX; + startY = caster.posY; + startZ = caster.posZ; hitEntities.clear(); // If direction wasn't set during windup (shouldn't happen), set it now if (chargeDirection == null) { - lockChargeDirection(npc, target); + lockChargeDirection(caster, target); } - enforceLockedRotation(npc); + enforceLockedRotation(caster); } - private void enforceLockedRotation(EntityNPCInterface npc) { - npc.rotationYaw = lockedYaw; - npc.rotationYawHead = lockedYaw; - npc.prevRotationYaw = lockedYaw; - npc.prevRotationYawHead = lockedYaw; - npc.renderYawOffset = lockedYaw; - npc.prevRenderYawOffset = lockedYaw; + private void enforceLockedRotation(EntityLivingBase caster) { + caster.rotationYaw = lockedYaw; + caster.rotationYawHead = lockedYaw; + caster.prevRotationYaw = lockedYaw; + caster.prevRotationYawHead = lockedYaw; + caster.renderYawOffset = lockedYaw; + caster.prevRenderYawOffset = lockedYaw; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (chargeDirection == null) return; // Enforce rotation every tick - enforceLockedRotation(npc); + enforceLockedRotation(caster); // Calculate distance traveled double distanceTraveled = Math.sqrt( - Math.pow(npc.posX - startX, 2) + - Math.pow(npc.posZ - startZ, 2) + Math.pow(caster.posX - startX, 2) + + Math.pow(caster.posZ - startZ, 2) ); // Check if reached max distance if (distanceTraveled >= maxRange) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; + signalCompletion(); return; } - if (isChargeBlocked(npc)) { - stopMomentum(npc); + if (isChargeBlocked(caster)) { + stopMomentum(caster); + signalCompletion(); return; } - // Move NPC - npc.motionX = chargeDirection.xCoord * chargeSpeed; - npc.motionY = 0; - npc.motionZ = chargeDirection.zCoord * chargeSpeed; - npc.velocityChanged = true; + // Move caster + caster.motionX = chargeDirection.xCoord * chargeSpeed; + caster.motionY = 0; + caster.motionZ = chargeDirection.zCoord * chargeSpeed; + caster.velocityChanged = true; // Server-side collision damage if (!world.isRemote) { - AxisAlignedBB hitBox = npc.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth); + AxisAlignedBB hitBox = caster.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); for (Entity entity : entities) { if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; + if (entity == caster) continue; if (hitEntities.contains(entity.getEntityId())) continue; EntityLivingBase livingEntity = (EntityLivingBase) entity; @@ -190,7 +191,7 @@ public class AbilityCharge extends Ability { hitEntities.add(entity.getEntityId()); // Apply damage with scripted event support - boolean wasHit = applyAbilityDamageWithDirection(npc, livingEntity, damage, knockback, + boolean wasHit = applyAbilityDamageWithDirection(caster, livingEntity, damage, knockback, chargeDirection.xCoord, chargeDirection.zCoord); // Play impact sound if hit wasn't cancelled @@ -202,28 +203,28 @@ public class AbilityCharge extends Ability { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - stopMomentum(npc); - super.onComplete(npc, target); + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + stopMomentum(caster); + super.onComplete(caster, target); } @Override - public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) { - stopMomentum(npc); - super.onInterrupt(npc, source, damage); + public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { + stopMomentum(caster); + super.onInterrupt(caster, source, damage); } - private void stopMomentum(EntityNPCInterface npc) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + private void stopMomentum(EntityLivingBase caster) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; } - private boolean isChargeBlocked(EntityNPCInterface npc) { + private boolean isChargeBlocked(EntityLivingBase caster) { double nextX = chargeDirection.xCoord * chargeSpeed; double nextZ = chargeDirection.zCoord * chargeSpeed; - AxisAlignedBB nextBox = npc.boundingBox.copy().offset(nextX, 0, nextZ); - return !npc.worldObj.getCollidingBoundingBoxes(npc, nextBox).isEmpty(); + AxisAlignedBB nextBox = caster.boundingBox.copy().offset(nextX, 0, nextZ); + return !caster.worldObj.getCollidingBoundingBoxes(caster, nextBox).isEmpty(); } @Override @@ -244,12 +245,12 @@ public class AbilityCharge extends Ability { } /** - * Creates a LINE telegraph from NPC position towards target. - * Telegraph follows NPC during windup (so NPC can reposition before charging). + * Creates a LINE telegraph from caster position towards target. + * Telegraph follows caster during windup (so caster can reposition before charging). * Direction is locked at creation based on target position. */ @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { return null; } @@ -257,11 +258,11 @@ public class AbilityCharge extends Ability { // Calculate direction to target at this moment (locked) float yaw; if (target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; yaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); } else { - yaw = npc.rotationYaw; + yaw = caster.rotationYaw; } // Create LINE telegraph @@ -272,16 +273,79 @@ public class AbilityCharge extends Ability { telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); - // Position at NPC ground level, direction towards target - double groundY = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ); - TelegraphInstance instance = new TelegraphInstance(telegraph, npc.posX, groundY, npc.posZ, yaw); - instance.setCasterEntityId(npc.getEntityId()); - // Telegraph follows NPC during windup - allows NPC to reposition - instance.setEntityIdToFollow(npc.getEntityId()); + // Position at caster ground level, direction towards target + double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, yaw); + instance.setCasterEntityId(caster.getEntityId()); + // Telegraph follows caster during windup - allows caster to reposition + instance.setEntityIdToFollow(caster.getEntityId()); return instance; } + // ==================== PREVIEW MODE ==================== + + private transient double previewDirX, previewDirZ; + private transient double previewStartX, previewStartZ; + private transient boolean previewCharging = false; + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + previewStartX = npc.posX; + previewStartZ = npc.posZ; + previewCharging = false; + + // Calculate direction toward fake target + if (previewTarget != null) { + double dx = previewTarget.posX - npc.posX; + double dz = previewTarget.posZ - npc.posZ; + double len = Math.sqrt(dx * dx + dz * dz); + if (len > 0) { + previewDirX = dx / len; + previewDirZ = dz / len; + } else { + float yaw = (float) Math.toRadians(npc.rotationYaw); + previewDirX = -Math.sin(yaw); + previewDirZ = Math.cos(yaw); + } + } else { + float yaw = (float) Math.toRadians(npc.rotationYaw); + previewDirX = -Math.sin(yaw); + previewDirZ = Math.cos(yaw); + } + previewCharging = true; + } + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + if (!previewCharging) return; + + // Check distance traveled + double distTraveled = Math.sqrt( + Math.pow(npc.posX - previewStartX, 2) + + Math.pow(npc.posZ - previewStartZ, 2) + ); + + if (distTraveled >= maxRange) { + previewCharging = false; + return; + } + + npc.prevPosX = npc.posX; + npc.prevPosY = npc.posY; + npc.prevPosZ = npc.posZ; + + npc.posX += previewDirX * chargeSpeed; + npc.posZ += previewDirZ * chargeSpeed; + } + + @Override + public int getPreviewActiveDuration() { + return (int) Math.ceil(maxRange / chargeSpeed) + 5; + } + @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("chargeSpeed", chargeSpeed); @@ -293,7 +357,7 @@ public class AbilityCharge extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { this.chargeSpeed = nbt.hasKey("chargeSpeed") ? nbt.getFloat("chargeSpeed") : 0.8f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 3.0f; if (nbt.hasKey("hitWidth")) { this.hitWidth = nbt.getFloat("hitWidth"); @@ -305,15 +369,46 @@ public class AbilityCharge extends Ability { } // Getters & Setters - public float getChargeSpeed() { return chargeSpeed; } - public void setChargeSpeed(float chargeSpeed) { this.chargeSpeed = chargeSpeed; } + public float getChargeSpeed() { + return chargeSpeed; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public void setChargeSpeed(float chargeSpeed) { + this.chargeSpeed = chargeSpeed; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public float getDamage() { + return damage; + } - public float getHitWidth() { return hitWidth; } - public void setHitWidth(float hitWidth) { this.hitWidth = hitWidth; } + public void setDamage(float damage) { + this.damage = damage; + } + + public float getKnockback() { + return knockback; + } + + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + public float getHitWidth() { + return hitWidth; + } + + public void setHitWidth(float hitWidth) { + this.hitWidth = hitWidth; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("ability.chargeSpeed", this::getChargeSpeed, this::setChargeSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.LEFT), + FieldDef.floatField("ability.hitWidth", this::getHitWidth, this::setHitWidth).column(ColumnHint.RIGHT), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index b258e6127..4b8e5c0ee 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -1,22 +1,23 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCutter; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityCutter; + +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -26,24 +27,31 @@ import java.util.Set; * Deals damage to all entities in a cone/arc in front of the caster. * Can sweep outward over time or hit instantly. */ -public class AbilityCutter extends Ability { +public class AbilityCutter extends Ability implements IAbilityCutter { public enum SweepMode { SWIPE, - SPIN + SPIN; + + @Override + public String toString() { + switch (this) { + case SWIPE: return "ability.sweep.swipe"; + case SPIN: return "ability.sweep.spin"; + default: return name(); + } + } } private float arcAngle = 90.0f; private float range = 6.0f; - private float damage = 12.0f; + private float damage = 7.0f; private float knockback = 1.5f; private SweepMode sweepMode = SweepMode.SWIPE; private float sweepSpeed = 6.0f; + private int spinDurationTicks = 60; // Duration for SPIN mode - private int stunDuration = 0; - private int poisonDurationSeconds = 0; - private int poisonLevel = 0; private boolean piercing = true; private float innerRadius = 0.0f; @@ -56,88 +64,97 @@ public class AbilityCutter extends Ability { this.name = "Cutter"; this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 8.0f; - this.lockMovement = true; - this.cooldownTicks = 80; + this.lockMovement = LockMovementType.NO; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 15; - this.recoveryTicks = 15; this.telegraphType = TelegraphType.CONE; this.windUpSound = "random.bow"; this.activeSound = "random.break"; + this.windUpAnimationName = "Ability_Cutter_Windup"; + this.activeAnimationName = "Ability_Cutter_Active"; } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityCutter(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF }; + return new TargetingMode[]{TargetingMode.AOE_SELF}; } @Override - public float getTelegraphRadius() { return range; } + public float getTelegraphRadius() { + return range; + } @Override - public float getTelegraphLength() { return range; } + public float getTelegraphLength() { + return range; + } @Override - public float getTelegraphAngle() { return arcAngle; } + public float getTelegraphAngle() { + return arcAngle; + } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { hitEntities.clear(); currentRotation = -arcAngle / 2.0f; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; switch (sweepMode) { case SWIPE: if (currentRotation > arcAngle / 2.0f) { + signalCompletion(); // Swipe arc complete return; } - performSweepDamage(npc, world, innerRadius, range, currentRotation); + performSweepDamage(caster, world, innerRadius, range, currentRotation); currentRotation += sweepSpeed; break; case SPIN: + if (tick >= spinDurationTicks) { + signalCompletion(); // Spin duration complete + return; + } currentRotation = (currentRotation + sweepSpeed) % 360.0f; hitEntities.clear(); - performSweepDamage(npc, world, innerRadius, range, currentRotation); + performSweepDamage(caster, world, innerRadius, range, currentRotation); break; } } - private void performSweepDamage(EntityNPCInterface npc, World world, float minDist, float maxDist, float angleOffset) { - float casterYaw = npc.rotationYaw + angleOffset; + private void performSweepDamage(EntityLivingBase caster, World world, float minDist, float maxDist, float angleOffset) { + float casterYaw = caster.rotationYaw + angleOffset; AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( - npc.posX - maxDist, npc.posY - 1, npc.posZ - maxDist, - npc.posX + maxDist, npc.posY + 3, npc.posZ + maxDist + caster.posX - maxDist, caster.posY - 1, caster.posZ - maxDist, + caster.posX + maxDist, caster.posY + 3, caster.posZ + maxDist ); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (hitEntities.contains(entity.getEntityId())) continue; // If not piercing, stop after hitting one entity this sweep if (!piercing && !hitEntities.isEmpty()) break; - double dx = entity.posX - npc.posX; - double dz = entity.posZ - npc.posZ; + double dx = entity.posX - caster.posX; + double dz = entity.posZ - caster.posZ; double dist = Math.sqrt(dx * dx + dz * dz); if (dist < minDist || dist > maxDist) continue; @@ -145,21 +162,15 @@ public class AbilityCutter extends Ability { hitEntities.add(entity.getEntityId()); - float distFactor = 1.0f - ((float)dist / maxDist) * 0.3f; + float distFactor = 1.0f - ((float) dist / maxDist) * 0.3f; float actualDamage = damage * distFactor; // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, actualDamage, knockback); + boolean wasHit = applyAbilityDamage(caster, entity, actualDamage, knockback); - // Only apply effects if hit wasn't cancelled + // Apply effects if hit wasn't cancelled if (wasHit) { - if (stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - if (poisonDurationSeconds > 0) { - entity.addPotionEffect(new PotionEffect(Potion.poison.id, poisonDurationSeconds * 20, poisonLevel)); - } + applyEffects(entity); } } } @@ -177,13 +188,13 @@ public class AbilityCutter extends Ability { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { hitEntities.clear(); currentRotation = 0.0f; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { hitEntities.clear(); currentRotation = 0.0f; } @@ -196,9 +207,7 @@ public class AbilityCutter extends Ability { nbt.setFloat("knockback", knockback); nbt.setString("sweepMode", sweepMode.name()); nbt.setFloat("sweepSpeed", sweepSpeed); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("poisonDurationSeconds", poisonDurationSeconds); - nbt.setInteger("poisonLevel", poisonLevel); + nbt.setInteger("spinDurationTicks", spinDurationTicks); nbt.setBoolean("piercing", piercing); nbt.setFloat("innerRadius", innerRadius); } @@ -207,7 +216,7 @@ public class AbilityCutter extends Ability { public void readTypeNBT(NBTTagCompound nbt) { this.arcAngle = nbt.hasKey("arcAngle") ? nbt.getFloat("arcAngle") : 90.0f; this.range = nbt.hasKey("range") ? nbt.getFloat("range") : 6.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 12.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f; try { this.sweepMode = SweepMode.valueOf(nbt.getString("sweepMode")); @@ -215,46 +224,117 @@ public class AbilityCutter extends Ability { this.sweepMode = SweepMode.SWIPE; } this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 6.0f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.poisonDurationSeconds = nbt.hasKey("poisonDurationSeconds") ? nbt.getInteger("poisonDurationSeconds") : 0; - this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : 0; + this.spinDurationTicks = nbt.hasKey("spinDurationTicks") ? nbt.getInteger("spinDurationTicks") : 60; this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f; } // Getters & Setters - public float getArcAngle() { return arcAngle; } - public void setArcAngle(float arcAngle) { this.arcAngle = arcAngle; } + public float getArcAngle() { + return arcAngle; + } - public float getRange() { return range; } - public void setRange(float range) { this.range = range; } + public void setArcAngle(float arcAngle) { + this.arcAngle = arcAngle; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getRange() { + return range; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setRange(float range) { + this.range = range; + } - public SweepMode getSweepMode() { return sweepMode; } - public void setSweepMode(SweepMode sweepMode) { this.sweepMode = sweepMode; } + public float getDamage() { + return damage; + } - public float getSweepSpeed() { return sweepSpeed; } - public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; } + public void setDamage(float damage) { + this.damage = damage; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public float getKnockback() { + return knockback; + } - public int getPoisonDurationSeconds() { return poisonDurationSeconds; } - public void setPoisonDurationSeconds(int poisonDurationSeconds) { this.poisonDurationSeconds = poisonDurationSeconds; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public int getPoisonLevel() { return poisonLevel; } - public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; } + public SweepMode getSweepModeEnum() { + return sweepMode; + } - public boolean isPiercing() { return piercing; } - public void setPiercing(boolean piercing) { this.piercing = piercing; } + public void setSweepModeEnum(SweepMode sweepMode) { + this.sweepMode = sweepMode; + } - public float getInnerRadius() { return innerRadius; } - public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; } + @Override + public int getSweepMode() { + return sweepMode.ordinal(); + } - public float getCurrentRotation() { return currentRotation; } + @Override + public void setSweepMode(int mode) { + SweepMode[] values = SweepMode.values(); + this.sweepMode = mode >= 0 && mode < values.length ? values[mode] : SweepMode.SWIPE; + } + + public float getSweepSpeed() { + return sweepSpeed; + } + + public void setSweepSpeed(float sweepSpeed) { + this.sweepSpeed = sweepSpeed; + } + + public int getSpinDurationTicks() { + return spinDurationTicks; + } + + public void setSpinDurationTicks(int spinDurationTicks) { + this.spinDurationTicks = Math.max(1, spinDurationTicks); + } + + public boolean isPiercing() { + return piercing; + } + + public void setPiercing(boolean piercing) { + this.piercing = piercing; + } + + public float getInnerRadius() { + return innerRadius; + } + + public void setInnerRadius(float innerRadius) { + this.innerRadius = innerRadius; + } + + public float getCurrentRotation() { + return currentRotation; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("gui.range", this::getRange, this::setRange).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.section("ability.section.sweep"), + FieldDef.enumField("ability.sweepMode", SweepMode.class, this::getSweepModeEnum, this::setSweepModeEnum) + .hover("ability.hover.sweepMode"), + FieldDef.floatField("ability.arcAngle", this::getArcAngle, this::setArcAngle).column(ColumnHint.LEFT), + FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed), + FieldDef.intField("ability.spinDuration", this::getSpinDurationTicks, this::setSpinDurationTicks) + .range(1, 1000) + .visibleWhen(() -> this.getSweepModeEnum() == SweepMode.SPIN), + FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing) + .hover("ability.hover.piercing"), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index db34b0bb8..af0b1eb16 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -2,33 +2,48 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDash; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; +import noppes.npcs.api.ability.type.IAbilityDash; + +import java.util.Arrays; +import java.util.List; import java.util.Random; /** * Dash ability: Quick evasive sidestep with NO telegraph. * Defensive repositioning move to evade attacks. */ -public class AbilityDash extends Ability { +public class AbilityDash extends Ability implements IAbilityDash { /** * Dash behavior mode. */ public enum DashMode { AGGRESSIVE, - DEFENSIVE + DEFENSIVE; + + @Override + public String toString() { + switch (this) { + case AGGRESSIVE: return "ability.dash.aggressive"; + case DEFENSIVE: return "ability.dash.defensive"; + default: return name(); + } + } } /** @@ -87,11 +102,9 @@ public class AbilityDash extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 20.0f; this.minRange = 0.0f; - this.lockMovement = false; - this.cooldownTicks = 40; + this.lockMovement = LockMovementType.NO; + this.cooldownTicks = 0; this.windUpTicks = 5; - this.activeTicks = 8; - this.recoveryTicks = 10; // No telegraph for dash - it's a quick evasive move this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -100,21 +113,18 @@ public class AbilityDash extends Ability { } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityDash(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -123,10 +133,10 @@ public class AbilityDash extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - startX = npc.posX; - startY = npc.posY; - startZ = npc.posZ; + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + startX = caster.posX; + startY = caster.posY; + startZ = caster.posZ; // Choose random direction based on mode DashDirection[] directions = dashMode == DashMode.AGGRESSIVE @@ -137,11 +147,11 @@ public class AbilityDash extends Ability { // Calculate dash direction relative to target float baseYaw; if (target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); } else { - baseYaw = npc.rotationYaw; + baseYaw = caster.rotationYaw; } // Apply direction offset @@ -157,69 +167,130 @@ public class AbilityDash extends Ability { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (dashDirection == null) return; // Calculate distance traveled double distanceTraveled = Math.sqrt( - Math.pow(npc.posX - startX, 2) + - Math.pow(npc.posZ - startZ, 2) + Math.pow(caster.posX - startX, 2) + + Math.pow(caster.posZ - startZ, 2) ); // Check if reached max distance if (distanceTraveled >= dashDistance) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; + signalCompletion(); return; } - if (isDashBlocked(npc)) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + if (isDashBlocked(caster)) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; + signalCompletion(); return; } - // Move NPC - npc.motionX = dashDirection.xCoord * dashSpeed; - npc.motionY = 0; - npc.motionZ = dashDirection.zCoord * dashSpeed; - npc.velocityChanged = true; + // Move caster + caster.motionX = dashDirection.xCoord * dashSpeed; + caster.motionY = 0; + caster.motionZ = dashDirection.zCoord * dashSpeed; + caster.velocityChanged = true; // Trail particles - world.spawnParticle("smoke", npc.posX, npc.posY + 0.5, npc.posZ, 0, 0, 0); + world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0); } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; + } + + @Override + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; + } + + @Override + public void cleanup() { dashDirection = null; chosenDirection = null; } - @Override - public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; - dashDirection = null; - chosenDirection = null; - } - - private boolean isDashBlocked(EntityNPCInterface npc) { + private boolean isDashBlocked(EntityLivingBase caster) { + if (dashDirection == null) return true; double nextX = dashDirection.xCoord * dashSpeed; double nextZ = dashDirection.zCoord * dashSpeed; - return !npc.worldObj.getCollidingBoundingBoxes(npc, npc.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); + return !caster.worldObj.getCollidingBoundingBoxes(caster, caster.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); + } + + // ==================== PREVIEW MODE ==================== + + private transient double previewDirX, previewDirZ; + private transient double previewStartX, previewStartZ; + private transient boolean previewDashing = false; + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + previewStartX = npc.posX; + previewStartZ = npc.posZ; + previewDashing = false; + + // Pick a random direction based on mode + DashDirection[] directions = dashMode == DashMode.AGGRESSIVE + ? AGGRESSIVE_DIRECTIONS + : DEFENSIVE_DIRECTIONS; + DashDirection dir = directions[RANDOM.nextInt(directions.length)]; + + float baseYaw; + if (previewTarget != null) { + double dx = previewTarget.posX - npc.posX; + double dz = previewTarget.posZ - npc.posZ; + baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); + } else { + baseYaw = npc.rotationYaw; + } + + float dashYaw = baseYaw + dir.getAngleOffset(); + float yawRad = (float) Math.toRadians(dashYaw); + previewDirX = -Math.sin(yawRad); + previewDirZ = Math.cos(yawRad); + previewDashing = true; } @Override - public void reset() { - super.reset(); - dashDirection = null; - chosenDirection = null; + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + if (!previewDashing) return; + + double distTraveled = Math.sqrt( + Math.pow(npc.posX - previewStartX, 2) + + Math.pow(npc.posZ - previewStartZ, 2) + ); + + if (distTraveled >= dashDistance) { + previewDashing = false; + return; + } + + npc.prevPosX = npc.posX; + npc.prevPosY = npc.posY; + npc.prevPosZ = npc.posZ; + + npc.posX += previewDirX * dashSpeed; + npc.posZ += previewDirZ * dashSpeed; + } + + @Override + public int getPreviewActiveDuration() { + return (int) Math.ceil(dashDistance / dashSpeed) + 5; } @Override @@ -246,14 +317,52 @@ public class AbilityDash extends Ability { } // Getters & Setters - public DashMode getDashMode() { return dashMode; } - public void setDashMode(DashMode dashMode) { this.dashMode = dashMode; } + public DashMode getDashModeEnum() { + return dashMode; + } - public float getDashDistance() { return dashDistance; } - public void setDashDistance(float dashDistance) { this.dashDistance = dashDistance; } + public void setDashModeEnum(DashMode dashMode) { + this.dashMode = dashMode; + } - public float getDashSpeed() { return dashSpeed; } - public void setDashSpeed(float dashSpeed) { this.dashSpeed = dashSpeed; } + @Override + public int getDashMode() { + return dashMode.ordinal(); + } - public DashDirection getChosenDirection() { return chosenDirection; } + @Override + public void setDashMode(int mode) { + DashMode[] values = DashMode.values(); + this.dashMode = mode >= 0 && mode < values.length ? values[mode] : DashMode.AGGRESSIVE; + } + + public float getDashDistance() { + return dashDistance; + } + + public void setDashDistance(float dashDistance) { + this.dashDistance = dashDistance; + } + + public float getDashSpeed() { + return dashSpeed; + } + + public void setDashSpeed(float dashSpeed) { + this.dashSpeed = dashSpeed; + } + + public DashDirection getChosenDirection() { + return chosenDirection; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.enumField("ability.dashMode", DashMode.class, this::getDashModeEnum, this::setDashModeEnum) + .hover("ability.hover.dashMode"), + FieldDef.floatField("ability.dashDistance", this::getDashDistance, this::setDashDistance).column(ColumnHint.LEFT), + FieldDef.floatField("ability.dashSpeed", this::getDashSpeed, this::setDashSpeed).column(ColumnHint.RIGHT) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java new file mode 100644 index 000000000..1e066ff69 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -0,0 +1,332 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import noppes.npcs.api.ability.type.IAbilityDisc; + +import java.util.Arrays; +import java.util.List; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Disc ability: Spawns a flat spinning disc projectile. + * Has optional boomerang behavior to return to owner. + */ +public class AbilityDisc extends Ability implements IAbilityDisc { + + // Disc geometry + private float discRadius = 1.0f; + private float discThickness = 0.2f; + + // Boomerang properties + private boolean boomerang = false; + private int boomerangDelay = 40; + + // Energy data classes + private EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f); + private EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f); + private EnergyLightningData lightningData = new EnergyLightningData(); + private EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200); + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); + + // Transient state for disc entity (used during windup charging) + private transient EntityAbilityDisc discEntity = null; + + public AbilityDisc() { + this.typeId = "ability.cnpc.disc"; + this.name = "Disc"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 30.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 60; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_Disc_Windup"; + this.activeAnimationName = "Ability_Disc_Active"; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start moving the disc that was spawned during windup + if (discEntity != null && !discEntity.isDead) { + discEntity.startMoving(target); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn disc in charging mode on first tick of windup + if (tick == 1) { + // Create disc in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); + discEntity = new EntityAbilityDisc( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + discRadius, discThickness, + colorData, combatData, homingData, lightningData, lifespanData, + boomerang, boomerangDelay); + discEntity.setupCharging(anchorData, windUpTicks); + + discEntity.setEffects(this.effects); + world.spawnEntityInWorld(discEntity); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (discEntity == null || discEntity.isDead) { + discEntity = null; + signalCompletion(); + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void cleanup() { + // Despawn disc entity if still alive + if (discEntity != null && !discEntity.isDead) { + discEntity.setDead(); + } + discEntity = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(discRadius * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return discRadius * 1.5f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("discRadius", discRadius); + nbt.setFloat("discThickness", discThickness); + nbt.setBoolean("boomerang", boomerang); + nbt.setInteger("boomerangDelay", boomerangDelay); + anchorData.writeNBT(nbt); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; + this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; + this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); + this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; + anchorData.readNBT(nbt); + colorData.readNBT(nbt); + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); + } + + // Getters & Setters + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { homingData.speed = speed; } + public float getDiscRadius() { return discRadius; } + public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } + public float getDiscThickness() { return discThickness; } + public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; } + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; } + public boolean isBoomerang() { return boomerang; } + public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; } + public int getBoomerangDelay() { return boomerangDelay; } + public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; } + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { colorData.outerColor = outerColor; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return colorData.outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { colorData.outerColorAlpha = outerColorAlpha; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; } + public float getRotationSpeed() { return colorData.rotationSpeed; } + public void setRotationSpeed(float rotationSpeed) { colorData.rotationSpeed = rotationSpeed; } + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { lightningData.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningData.lightningDensity; } + public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningData.lightningRadius; } + public void setLightningRadius(float lightningRadius) { lightningData.lightningRadius = lightningRadius; } + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } + + @Override + public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); } + + @Override + public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj); + disc.setupPreview(npc, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks); + return disc; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.section("ability.section.disc"), + FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius).column(ColumnHint.LEFT), + FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming), + FieldDef.section("ability.section.boomerang"), + FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang) + .hover("ability.hover.boomerang"), + FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay) + .visibleWhen(this::isBoomerang), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.colors").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").customTab("ability.tab.visual"), + FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) + ); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java new file mode 100644 index 000000000..c4f2b880d --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java @@ -0,0 +1,505 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.util.ValueUtil; + +import java.util.Arrays; +import java.util.List; + +public class AbilityDiscDual extends Ability { + + // Disc geometry + private float discRadius = 1.0f; + private float discThickness = 0.2f; + + // Boomerang properties + private boolean boomerang = false; + private int boomerangDelay = 40; + + // Dual properties + private boolean dualFire= true; + private int dualFireDelay = 10; + + // Energy data classes + private EnergyColorData[] colorData = new EnergyColorData[]{ + new EnergyColorData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f), + new EnergyColorData(0xFFFFFF, 0x8800FF, true, 0.4f, 0.5f, 5.0f) + }; + private EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f); + public final EnergyLightningData[] lightningData = new EnergyLightningData[]{ + new EnergyLightningData(), new EnergyLightningData() + }; + private EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200); + private EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ + new EnergyAnchorData(AnchorPoint.LEFT_HAND), new EnergyAnchorData(AnchorPoint.RIGHT_HAND) + }; + + // Transient state for disc entity (used during windup charging) + private transient EntityAbilityDisc discEntity = null; + private transient EntityAbilityDisc discEntity2 = null; + + public AbilityDiscDual() { + this.typeId = "ability.cnpc.disc_dual"; + this.name = "Disc Dual"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 30.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 60; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_DiscDual_Windup"; + this.activeAnimationName = "Ability_DiscDual_Active"; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start moving the orb that was spawned during windup + if (discEntity != null && !discEntity.isDead) { + discEntity.startMoving(target); + } + + if (dualFire) { + if (discEntity2 != null && !discEntity2.isDead && dualFireDelay <= 0) { + discEntity2.startMoving(target); + } + } else { + discEntity2.setDead(); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn disc in charging mode on first tick of windup + if (tick == 1) { + // Create disc in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]); + discEntity = new EntityAbilityDisc( + world, caster, target, + spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, + discRadius, discThickness, + colorData[0], combatData, homingData, lightningData[0], lifespanData, + boomerang, boomerangDelay); + + Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]); + discEntity2 = new EntityAbilityDisc( + world, caster, target, + spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, + discRadius, discThickness, + colorData[1], combatData, homingData, lightningData[1], lifespanData, + boomerang, boomerangDelay); + + discEntity.setSiblingUUID(discEntity2.getPersistentID()); + discEntity2.setSiblingUUID(discEntity.getPersistentID()); + + discEntity.setupCharging(anchorData[0], windUpTicks); + discEntity2.setupCharging(anchorData[1], windUpTicks); + + discEntity.setEffects(this.effects); + + if (dualFire) { + discEntity2.setEffects(this.effects); + } + + world.spawnEntityInWorld(discEntity); + world.spawnEntityInWorld(discEntity2); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (dualFire) { + if ((discEntity == null || discEntity.isDead) && (discEntity2 == null || discEntity2.isDead)) { + discEntity = null; + discEntity2 = null; + signalCompletion(); + } + + if (tick == dualFireDelay && discEntity2 != null && !discEntity2.isDead) { + discEntity2.startMoving(target); + } + } else { + if (discEntity == null || discEntity.isDead) { + discEntity = null; + signalCompletion(); + } + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void cleanup() { + // Despawn disc entity if still alive + if (discEntity != null && !discEntity.isDead) { + discEntity.setDead(); + } + + if (discEntity2 != null && !discEntity2.isDead) { + discEntity2.setDead(); + } + + discEntity = null; + discEntity2 = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(discRadius * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return discRadius * 1.5f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("discRadius", discRadius); + nbt.setFloat("discThickness", discThickness); + nbt.setBoolean("boomerang", boomerang); + nbt.setInteger("boomerangDelay", boomerangDelay); + nbt.setBoolean("dualFire", dualFire); + nbt.setInteger("dualFireDelay", dualFireDelay); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + + NBTTagCompound disc1 = new NBTTagCompound(); + NBTTagCompound disc2 = new NBTTagCompound(); + + anchorData[0].writeNBT(disc1); + anchorData[1].writeNBT(disc2); + colorData[0].writeNBT(disc1); + colorData[1].writeNBT(disc2); + lightningData[0].writeNBT(disc1); + lightningData[1].writeNBT(disc2); + + nbt.setTag("Disc_1", disc1); + nbt.setTag("Disc_2", disc2); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + NBTTagCompound disc1 = nbt.hasKey("Disc_1") ? nbt.getCompoundTag("Disc_1") : new NBTTagCompound(); + NBTTagCompound disc2 = nbt.hasKey("Disc_2") ? nbt.getCompoundTag("Disc_2") : new NBTTagCompound(); + + this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; + this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; + this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); + this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; + this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); + this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lifespanData.readNBT(nbt); + + anchorData[0].readNBT(disc1); + anchorData[1].readNBT(disc2); + colorData[0].readNBT(disc1); + colorData[1].readNBT(disc2); + lightningData[0].readNBT(disc1); + lightningData[1].readNBT(disc2); + } + + // Getters & Setters + private EnergyColorData getColorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return colorData[orb]; + } + private EnergyLightningData getLightningData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return lightningData[orb]; + } + private EnergyAnchorData getAnchorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return anchorData[orb]; + } + + public boolean isDualFire() { return dualFire; } + public void setDualFire(boolean dualFire) { this.dualFire = dualFire; } + public int getDualFireDelay() { return dualFireDelay; } + public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; } + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { homingData.speed = speed; } + public float getDiscRadius() { return discRadius; } + public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } + public float getDiscThickness() { return discThickness; } + public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; } + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; } + public boolean isBoomerang() { return boomerang; } + public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; } + public int getBoomerangDelay() { return boomerangDelay; } + public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; } + + + public int getInnerColor(int orb) { return getColorData(orb).innerColor; } + public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; } + public int getOuterColor(int orb) { return getColorData(orb).outerColor; } + public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; } + public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; } + public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; } + public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; } + public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; } + public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; } + public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; } + public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; } + public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; } + + public int getInnerColor() { return getInnerColor(0); } + public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } + public int getOuterColor() { return getOuterColor(0); } + public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } + public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } + public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } + public float getOuterColorWidth() { return getOuterColorWidth(0); } + public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } + public float getOuterColorAlpha() { return getOuterColorAlpha(0); } + public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } + public float getRotationSpeed() { return getRotationSpeed(0); } + public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } + + + public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; } + public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; } + public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; } + public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; } + public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; } + public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; } + + public boolean hasLightningEffect() { return hasLightningEffect(0); } + public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } + public float getLightningDensity() { return getLightningDensity(0); } + public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } + public float getLightningRadius() { return getLightningRadius(0); } + public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } + + + public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; } + public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; } + public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; } + public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; } + public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; } + public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; } + public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; } + public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; } + + public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } + public float getAnchorOffsetX() { return getAnchorOffsetX(0); } + public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } + public float getAnchorOffsetY() { return getAnchorOffsetY(0); } + public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } + public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } + public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } + + //@Override + public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); } + public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } + + //@Override + public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.section("ability.section.disc"), + FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius).column(ColumnHint.LEFT), + FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.dual"), + FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire), + FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay) + .visibleWhen(this::isDualFire), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming), + FieldDef.section("ability.section.boomerang"), + FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang) + .hover("ability.hover.boomerang"), + FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay) + .visibleWhen(this::isBoomerang), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab - Disc 1 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.disc1").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(0), v -> setInnerColor(0, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v)) + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(0), v -> setOuterColor(0, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v)) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(0), v -> setRotationSpeed(0, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(0), v -> setLightningEffect(0, v)) + .customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(0), v -> setLightningDensity(0, v)) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(0), v -> setLightningRadius(0, v)) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.RIGHT), + // Visual tab - Disc 2 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.disc2").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(1), v -> setInnerColor(1, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v)) + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(1), v -> setOuterColor(1, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v)) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(1), v -> setRotationSpeed(1, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(1), v -> setLightningEffect(1, v)) + .customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(1), v -> setLightningDensity(1, v)) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(1), v -> setLightningRadius(1, v)) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.RIGHT) + ); + } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj); + disc.setupPreview(npc, discRadius, discThickness, colorData[0], lightningData[0], anchorData[0], windUpTicks); + return disc; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java new file mode 100644 index 000000000..3f9e2da7e --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -0,0 +1,332 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import noppes.npcs.api.ability.type.IAbilityEnergyBeam; + +import java.util.Arrays; +import java.util.List; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Energy Beam ability: A homing head with a trailing path. + * The beam stays attached to the origin and curves as it homes in on target. + * Cannot cross over itself (self-intersection prevention). + */ +public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { + + // Shape properties (standalone) + private float beamWidth = 0.4f; + private float headSize = 0.6f; + + // Data classes + private EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f); + private EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f); + private EnergyLightningData lightningData = new EnergyLightningData(); + private EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200); + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); + + // Transient state for beam entity (used during windup charging) + private transient EntityAbilityBeam beamEntity = null; + + public AbilityEnergyBeam() { + this.typeId = "ability.cnpc.beam"; + this.name = "Beam"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 20.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 40; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_Beam_Windup"; + this.activeAnimationName = "Ability_Beam_Active"; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start firing the beam that was spawned during windup + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.startFiring(target); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn beam in charging mode on first tick of windup + if (tick == 1) { + float offsetDist = 1.0f; + + // Create beam in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData, offsetDist); + beamEntity = new EntityAbilityBeam( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + beamWidth, headSize, + colorData, combatData, homingData, lightningData, lifespanData, + lockMovement.locksActive()); + beamEntity.setupCharging(anchorData, windUpTicks, offsetDist); + + beamEntity.setEffects(this.effects); + world.spawnEntityInWorld(beamEntity); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (beamEntity == null || beamEntity.isDead) { + beamEntity = null; + signalCompletion(); + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void cleanup() { + // Despawn beam entity if still alive + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.setDead(); + } + beamEntity = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(headSize * 2.0f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return headSize * 2.0f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("headSize", headSize); + anchorData.writeNBT(nbt); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; + this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; + anchorData.readNBT(nbt); + colorData.readNBT(nbt); + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); + } + + // Getters & Setters - Standalone fields + public float getBeamWidth() { return beamWidth; } + public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public float getHeadSize() { return headSize; } + public void setHeadSize(float headSize) { this.headSize = headSize; } + + // Getters & Setters - Color data + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { this.colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { this.colorData.outerColor = outerColor; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.colorData.outerColorEnabled = outerColorEnabled; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.colorData.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return colorData.outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { this.colorData.outerColorAlpha = outerColorAlpha; } + public float getRotationSpeed() { return colorData.rotationSpeed; } + public void setRotationSpeed(float rotationSpeed) { this.colorData.rotationSpeed = rotationSpeed; } + + // Getters & Setters - Combat data + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { this.combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { this.combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } + + // Getters & Setters - Homing data + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { this.homingData.speed = speed; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { this.homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; } + + // Getters & Setters - Lightning data + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningData.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningData.lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningData.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningData.lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningData.lightningRadius = lightningRadius; } + + // Getters & Setters - Lifespan data + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } + + // Getters & Setters - Anchor point + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } + + @Override + public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); } + + @Override + public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj); + beam.setupPreview(npc, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, 1.0f); + return beam; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.section("ability.section.beam"), + FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth).column(ColumnHint.LEFT), + FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming).column(ColumnHint.LEFT), + FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange) + .visibleWhen(this::isHoming).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.colors").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").customTab("ability.tab.visual"), + FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) + ); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 2ef0a446c..b790e33d0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -1,35 +1,40 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityGuard; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.ability.UserType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityGuard; + +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; +import java.util.List; import java.util.Random; /** * Guard ability: Defensive stance that reduces incoming damage. * Can optionally counter-attack when hit. */ -public class AbilityGuard extends Ability { +public class AbilityGuard extends Ability implements IAbilityGuard { private static final Random RANDOM = new Random(); // Type-specific parameters + private int durationTicks = 60; private float damageReduction = 0.5f; private boolean canCounter = false; private CounterType counterType = CounterType.FLAT; - private float counterValue = 10.0f; + private float counterValue = 6.0f; private float counterChance = 0.3f; private String counterSound = "random.wood_click"; private int counterAnimationId = -1; @@ -41,67 +46,77 @@ public class AbilityGuard extends Ability { public enum CounterType { FLAT, - PERCENT + PERCENT; + + @Override + public String toString() { + switch (this) { + case FLAT: return "ability.counter.flat"; + case PERCENT: return "ability.counter.percent"; + default: return name(); + } + } } public AbilityGuard() { this.typeId = "ability.cnpc.guard"; this.name = "Guard"; this.targetingMode = TargetingMode.SELF; - this.lockMovement = true; - this.cooldownTicks = 120; + this.lockMovement = LockMovementType.ACTIVE; + this.cooldownTicks = 0; this.windUpTicks = 10; - this.activeTicks = 60; - this.recoveryTicks = 20; this.interruptible = false; // Hard to interrupt while guarding // No telegraph for guard - it's a defensive stance this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; this.windUpSound = "random.anvil_use"; this.activeSound = "random.anvil_land"; + this.allowedBy = UserType.NPC_ONLY; } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityGuard(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.SELF }; + return new TargetingMode[]{TargetingMode.SELF}; } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { lastAttacker = null; counterTriggered = false; lastDamageTaken = 0.0f; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Counter attack logic if triggered if (canCounter && counterTriggered && lastAttacker != null && !world.isRemote) { - performCounter(npc, world); + performCounter(caster, world); + } + + // Check if guard duration has ended + if (tick >= durationTicks) { + signalCompletion(); } } /** - * Called externally when the NPC takes damage while guarding. + * Called externally when the caster takes damage while guarding. * This should be called from the damage handling code. * * @param attacker The entity that attacked - * @param damage The damage amount (after reduction) + * @param damage The damage amount (after reduction) */ - public void onDamageTaken(EntityNPCInterface npc, EntityLivingBase attacker, DamageSource source, float damage) { + public void onDamageTaken(EntityLivingBase caster, EntityLivingBase attacker, DamageSource source, float damage) { if (!canCounter || attacker == null) return; if (!isDirectHit(source)) return; if (RANDOM.nextFloat() >= counterChance) return; @@ -109,32 +124,21 @@ public class AbilityGuard extends Ability { lastAttacker = attacker; lastDamageTaken = damage; counterTriggered = true; - - if (interruptible) { - performCounter(npc, npc.worldObj); - npc.abilities.interruptCurrentAbility(source, damage); - } + // Counter will be performed in onActiveTick - don't self-interrupt } - private void performCounter(EntityNPCInterface npc, World world) { + private void performCounter(EntityLivingBase caster, World world) { float counterDamage = counterType == CounterType.PERCENT ? lastDamageTaken * (counterValue / 100.0f) : counterValue; - boolean wasHit = applyAbilityDamage(npc, lastAttacker, counterDamage, 0.5f); + boolean wasHit = applyAbilityDamage(caster, lastAttacker, counterDamage, 0.5f); if (wasHit && counterSound != null && !counterSound.isEmpty()) { - world.playSoundAtEntity(npc, counterSound, 1.0f, 1.2f); + world.playSoundAtEntity(caster, counterSound, 1.0f, 1.2f); } - if (counterAnimationId >= 0) { - if (noppes.npcs.controllers.AnimationController.Instance != null) { - noppes.npcs.controllers.data.Animation animation = - noppes.npcs.controllers.AnimationController.Instance.animations.get(counterAnimationId); - if (animation != null) { - npc.display.animationData.setEnabled(true); - npc.display.animationData.setAnimation(animation); - npc.display.animationData.updateClient(); - } - } + // Use shared animation utility - only available for NPCs + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).abilities.playAbilityAnimation(counterAnimationId); } counterTriggered = false; @@ -160,7 +164,7 @@ public class AbilityGuard extends Ability { } /** - * Check if the NPC is currently in guard stance. + * Check if the caster is currently in guard stance. */ public boolean isGuarding() { return isExecuting() && getPhase() == kamkeel.npcs.controllers.data.ability.AbilityPhase.ACTIVE; @@ -181,6 +185,7 @@ public class AbilityGuard extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("damageReduction", damageReduction); nbt.setBoolean("canCounter", canCounter); nbt.setString("counterType", counterType.name()); @@ -192,6 +197,7 @@ public class AbilityGuard extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; this.damageReduction = nbt.hasKey("damageReduction") ? nbt.getFloat("damageReduction") : 0.5f; this.canCounter = nbt.hasKey("canCounter") && nbt.getBoolean("canCounter"); try { @@ -204,7 +210,7 @@ public class AbilityGuard extends Ability { } else if (nbt.hasKey("counterDamage")) { this.counterValue = nbt.getFloat("counterDamage"); } else { - this.counterValue = 10.0f; + this.counterValue = 6.0f; } this.counterChance = nbt.hasKey("counterChance") ? nbt.getFloat("counterChance") : 0.3f; this.counterSound = nbt.hasKey("counterSound") ? nbt.getString("counterSound") : "random.wood_click"; @@ -212,24 +218,104 @@ public class AbilityGuard extends Ability { } // Getters & Setters - public float getDamageReduction() { return damageReduction; } - public void setDamageReduction(float damageReduction) { this.damageReduction = damageReduction; } + public int getDurationTicks() { + return durationTicks; + } - public boolean isCanCounter() { return canCounter; } - public void setCanCounter(boolean canCounter) { this.canCounter = canCounter; } + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } - public CounterType getCounterType() { return counterType; } - public void setCounterType(CounterType counterType) { this.counterType = counterType; } + public float getDamageReduction() { + return damageReduction; + } - public float getCounterValue() { return counterValue; } - public void setCounterValue(float counterValue) { this.counterValue = counterValue; } + public void setDamageReduction(float damageReduction) { + this.damageReduction = damageReduction; + } - public float getCounterChance() { return counterChance; } - public void setCounterChance(float counterChance) { this.counterChance = counterChance; } + public boolean isCanCounter() { + return canCounter; + } - public String getCounterSound() { return counterSound; } - public void setCounterSound(String counterSound) { this.counterSound = counterSound; } + @Override + public boolean canCounter() { + return canCounter; + } - public int getCounterAnimationId() { return counterAnimationId; } - public void setCounterAnimationId(int counterAnimationId) { this.counterAnimationId = counterAnimationId; } + public void setCanCounter(boolean canCounter) { + this.canCounter = canCounter; + } + + public CounterType getCounterTypeEnum() { + return counterType; + } + + public void setCounterTypeEnum(CounterType counterType) { + this.counterType = counterType; + } + + @Override + public int getCounterType() { + return counterType.ordinal(); + } + + @Override + public void setCounterType(int type) { + CounterType[] values = CounterType.values(); + this.counterType = type >= 0 && type < values.length ? values[type] : CounterType.FLAT; + } + + public float getCounterValue() { + return counterValue; + } + + public void setCounterValue(float counterValue) { + this.counterValue = counterValue; + } + + public float getCounterChance() { + return counterChance; + } + + public void setCounterChance(float counterChance) { + this.counterChance = counterChance; + } + + public String getCounterSound() { + return counterSound; + } + + public void setCounterSound(String counterSound) { + this.counterSound = counterSound; + } + + public int getCounterAnimationId() { + return counterAnimationId; + } + + public void setCounterAnimationId(int counterAnimationId) { + this.counterAnimationId = counterAnimationId; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 1000), + FieldDef.floatField("ability.damageReduction", this::getDamageReduction, this::setDamageReduction), + FieldDef.section("ability.section.counter"), + FieldDef.boolField("gui.enabled", this::isCanCounter, this::setCanCounter).hover("ability.hover.canCounter"), + FieldDef.enumField("gui.type", CounterType.class, this::getCounterTypeEnum, this::setCounterTypeEnum) + .hover("ability.hover.counterType").visibleWhen(this::isCanCounter), + FieldDef.floatField("gui.value", this::getCounterValue, this::setCounterValue) + .visibleWhen(this::isCanCounter).column(ColumnHint.LEFT), + FieldDef.floatField("gui.chance", this::getCounterChance, this::setCounterChance) + .visibleWhen(this::isCanCounter).column(ColumnHint.RIGHT), + FieldDef.stringField("gui.sound", this::getCounterSound, this::setCounterSound) + .visibleWhen(this::isCanCounter), + FieldDef.intField("gui.animation", this::getCounterAnimationId, this::setCounterAnimationId) + .visibleWhen(this::isCanCounter), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index d211e7516..01cf2f1be 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -1,15 +1,10 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHazard; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; @@ -19,6 +14,13 @@ import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityHazard; + +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Random; @@ -28,28 +30,50 @@ import java.util.Set; * Hazard ability: Creates persistent ground effect zones. * Deals damage and/or applies debuffs to entities in the zone over time. */ -public class AbilityHazard extends Ability { +public class AbilityHazard extends Ability implements IAbilityHazard { public enum HazardShape { CIRCLE, RING, - CONE + CONE; + + @Override + public String toString() { + switch (this) { + case CIRCLE: return "ability.shape.circle"; + case RING: return "ability.shape.ring"; + case CONE: return "ability.shape.cone"; + default: return name(); + } + } } public enum PlacementMode { AT_CASTER, AT_TARGET, FOLLOW_CASTER, - FOLLOW_TARGET + FOLLOW_TARGET; + + @Override + public String toString() { + switch (this) { + case AT_CASTER: return "ability.placement.atCaster"; + case AT_TARGET: return "ability.placement.atTarget"; + case FOLLOW_CASTER: return "ability.placement.followCaster"; + case FOLLOW_TARGET: return "ability.placement.followTarget"; + default: return name(); + } + } } + private int durationTicks = 100; private float radius = 4.0f; private float innerRadius = 0.0f; private float coneAngle = 45.0f; private HazardShape shape = HazardShape.CIRCLE; private PlacementMode placement = PlacementMode.AT_TARGET; - private float damagePerTick = 1.0f; + private float damagePerSecond = 1.0f; private int damageInterval = 20; private boolean ignoreInvulnFrames = false; @@ -81,38 +105,35 @@ public class AbilityHazard extends Ability { this.name = "Hazard"; this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 15.0f; - this.lockMovement = true; - this.cooldownTicks = 200; + this.lockMovement = LockMovementType.WINDUP; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 100; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityHazard(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override - public float getTelegraphRadius() { return radius; } + public float getTelegraphRadius() { + return radius; + } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { - TelegraphInstance instance = super.createTelegraph(npc, target); + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + TelegraphInstance instance = super.createTelegraph(caster, target); if (instance == null) return null; // Control telegraph following based on placement mode @@ -121,9 +142,9 @@ public class AbilityHazard extends Ability { case FOLLOW_CASTER: // Telegraph stays at caster position, no following instance.setEntityIdToFollow(-1); - instance.setX(npc.posX); - instance.setY(npc.posY); - instance.setZ(npc.posZ); + instance.setX(caster.posX); + instance.setY(caster.posY); + instance.setZ(caster.posZ); break; case AT_TARGET: // Telegraph follows target during windup, locks on execute @@ -141,43 +162,15 @@ public class AbilityHazard extends Ability { return instance; } - /** - * Calculates offset position near the given coordinates. - * Used to place hazard near target rather than exactly on them. - */ - private double[] calculateOffsetPosition(double baseX, double baseY, double baseZ) { - if (maxOffset <= 0) { - return new double[]{baseX, baseY, baseZ}; - } - - double offsetDist; - double offsetAngle; - - if (randomOffset) { - // Random offset within min-max range - offsetDist = minOffset + RANDOM.nextDouble() * (maxOffset - minOffset); - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } else { - // Fixed offset (use max, random angle) - offsetDist = maxOffset; - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } - - double offsetX = Math.cos(offsetAngle) * offsetDist; - double offsetZ = Math.sin(offsetAngle) * offsetDist; - - return new double[]{baseX + offsetX, baseY, baseZ + offsetZ}; - } - @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // AT_CASTER locks immediately, AT_TARGET follows during windup and locks on execute if (tick == 0) { switch (placement) { case AT_CASTER: - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; positionLocked = true; break; case AT_TARGET: @@ -193,7 +186,7 @@ public class AbilityHazard extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Lock position from telegraph (which was following target) or calculate it now TelegraphInstance telegraph = getTelegraphInstance(); @@ -201,33 +194,39 @@ public class AbilityHazard extends Ability { case AT_CASTER: // Already locked during windup if (!positionLocked) { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; case AT_TARGET: // Use telegraph position if available, apply offset if (telegraph != null) { - double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ()); + double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(), + minOffset, maxOffset, randomOffset, RANDOM); zoneX = pos[0]; zoneY = pos[1]; zoneZ = pos[2]; + // Update telegraph to show actual hazard position + telegraph.setX(zoneX); + telegraph.setY(zoneY); + telegraph.setZ(zoneZ); } else if (target != null) { - double[] pos = calculateOffsetPosition(target.posX, target.posY, target.posZ); + double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ, + minOffset, maxOffset, randomOffset, RANDOM); zoneX = pos[0]; zoneY = pos[1]; zoneZ = pos[2]; } else { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; case FOLLOW_CASTER: - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; break; case FOLLOW_TARGET: if (target != null) { @@ -235,9 +234,9 @@ public class AbilityHazard extends Ability { zoneY = target.posY; zoneZ = target.posZ; } else { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; } @@ -246,17 +245,23 @@ public class AbilityHazard extends Ability { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; + // Check if hazard duration has ended + if (tick >= durationTicks) { + signalCompletion(); + return; + } + damagedThisTick.clear(); ticksSinceDamage++; switch (placement) { case FOLLOW_CASTER: - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; break; case FOLLOW_TARGET: if (target != null) { @@ -281,16 +286,16 @@ public class AbilityHazard extends Ability { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); for (EntityLivingBase entity : entities) { - if (entity == npc && !affectsCaster) continue; + if (entity == caster && !affectsCaster) continue; if (damagedThisTick.contains(entity.getEntityId())) continue; - if (!isInZone(entity, npc)) continue; + if (!isInZone(entity, caster)) continue; - if (damagePerTick > 0) { + if (damagePerSecond > 0) { if (ignoreInvulnFrames) { entity.hurtResistantTime = 0; } - // Apply damage with scripted event support (no knockback for hazard ticks) - boolean wasHit = applyAbilityDamage(npc, entity, damagePerTick, 0); + // Apply damage with scripted event support (no knockback for hazard) + boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0); if (!wasHit) continue; // Skip debuffs if hit was cancelled } @@ -300,7 +305,7 @@ public class AbilityHazard extends Ability { } } - private boolean isInZone(EntityLivingBase entity, EntityNPCInterface npc) { + private boolean isInZone(EntityLivingBase entity, EntityLivingBase caster) { double dx = entity.posX - zoneX; double dz = entity.posZ - zoneZ; double dist = Math.sqrt(dx * dx + dz * dz); @@ -312,7 +317,7 @@ public class AbilityHazard extends Ability { return dist >= innerRadius && dist <= radius; case CONE: if (dist > radius) return false; - float casterYaw = npc.rotationYaw; + float casterYaw = caster.rotationYaw; double angleToEntity = Math.toDegrees(Math.atan2(-dx, dz)); double angleDiff = Math.abs(normalizeAngle(angleToEntity - casterYaw)); return angleDiff <= coneAngle / 2; @@ -346,25 +351,26 @@ public class AbilityHazard extends Ability { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { damagedThisTick.clear(); positionLocked = false; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { damagedThisTick.clear(); positionLocked = false; } @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("radius", radius); nbt.setFloat("innerRadius", innerRadius); nbt.setFloat("coneAngle", coneAngle); nbt.setString("shape", shape.name()); nbt.setString("placement", placement.name()); - nbt.setFloat("damagePerTick", damagePerTick); + nbt.setFloat("damagePerSecond", damagePerSecond); nbt.setInteger("damageInterval", damageInterval); nbt.setBoolean("ignoreInvulnFrames", ignoreInvulnFrames); nbt.setInteger("slownessLevel", slownessLevel); @@ -383,6 +389,7 @@ public class AbilityHazard extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 100; this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 4.0f; this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f; this.coneAngle = nbt.hasKey("coneAngle") ? nbt.getFloat("coneAngle") : 45.0f; @@ -396,7 +403,14 @@ public class AbilityHazard extends Ability { } catch (Exception e) { this.placement = PlacementMode.AT_TARGET; } - this.damagePerTick = nbt.hasKey("damagePerTick") ? nbt.getFloat("damagePerTick") : 1.0f; + // Support legacy "damagePerTick" key for backwards compatibility + if (nbt.hasKey("damagePerSecond")) { + this.damagePerSecond = nbt.getFloat("damagePerSecond"); + } else if (nbt.hasKey("damagePerTick")) { + this.damagePerSecond = nbt.getFloat("damagePerTick"); + } else { + this.damagePerSecond = 1.0f; + } this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 20; this.ignoreInvulnFrames = nbt.hasKey("ignoreInvulnFrames") && nbt.getBoolean("ignoreInvulnFrames"); this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : -1; @@ -414,68 +428,230 @@ public class AbilityHazard extends Ability { } // Getters & Setters - public float getRadius() { return radius; } - public void setRadius(float radius) { this.radius = radius; } + public int getDurationTicks() { + return durationTicks; + } - public float getInnerRadius() { return innerRadius; } - public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; } + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } - public float getConeAngle() { return coneAngle; } - public void setConeAngle(float coneAngle) { this.coneAngle = coneAngle; } + public float getRadius() { + return radius; + } - public HazardShape getShape() { return shape; } - public void setShape(HazardShape shape) { this.shape = shape; } + public void setRadius(float radius) { + this.radius = radius; + } - public PlacementMode getPlacement() { return placement; } - public void setPlacement(PlacementMode placement) { this.placement = placement; } + public float getInnerRadius() { + return innerRadius; + } - public float getDamagePerTick() { return damagePerTick; } - public void setDamagePerTick(float damagePerTick) { this.damagePerTick = damagePerTick; } + public void setInnerRadius(float innerRadius) { + this.innerRadius = innerRadius; + } - public int getDamageInterval() { return damageInterval; } - public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; } + public float getConeAngle() { + return coneAngle; + } - public boolean isIgnoreInvulnFrames() { return ignoreInvulnFrames; } - public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { this.ignoreInvulnFrames = ignoreInvulnFrames; } + public void setConeAngle(float coneAngle) { + this.coneAngle = coneAngle; + } - public int getSlownessLevel() { return slownessLevel; } - public void setSlownessLevel(int slownessLevel) { this.slownessLevel = slownessLevel; } + public HazardShape getShapeEnum() { + return shape; + } - public int getWeaknessLevel() { return weaknessLevel; } - public void setWeaknessLevel(int weaknessLevel) { this.weaknessLevel = weaknessLevel; } + public void setShapeEnum(HazardShape shape) { + this.shape = shape; + } - public int getPoisonLevel() { return poisonLevel; } - public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; } + @Override + public int getShape() { + return shape.ordinal(); + } - public int getWitherLevel() { return witherLevel; } - public void setWitherLevel(int witherLevel) { this.witherLevel = witherLevel; } + @Override + public void setShape(int shape) { + HazardShape[] values = HazardShape.values(); + this.shape = shape >= 0 && shape < values.length ? values[shape] : HazardShape.CIRCLE; + } - public int getBlindnessLevel() { return blindnessLevel; } - public void setBlindnessLevel(int blindnessLevel) { this.blindnessLevel = blindnessLevel; } + public PlacementMode getPlacementEnum() { + return placement; + } - public int getDebuffDuration() { return debuffDuration; } - public void setDebuffDuration(int debuffDuration) { this.debuffDuration = debuffDuration; } + public void setPlacementEnum(PlacementMode placement) { + this.placement = placement; + } - public boolean isAffectsCaster() { return affectsCaster; } - public void setAffectsCaster(boolean affectsCaster) { this.affectsCaster = affectsCaster; } + @Override + public int getPlacement() { + return placement.ordinal(); + } - public float getHeightAbove() { return heightAbove; } - public void setHeightAbove(float heightAbove) { this.heightAbove = heightAbove; } + @Override + public void setPlacement(int placement) { + PlacementMode[] values = PlacementMode.values(); + this.placement = placement >= 0 && placement < values.length ? values[placement] : PlacementMode.AT_CASTER; + } - public float getHeightBelow() { return heightBelow; } - public void setHeightBelow(float heightBelow) { this.heightBelow = heightBelow; } + public float getDamagePerSecond() { + return damagePerSecond; + } - public float getMinOffset() { return minOffset; } - public void setMinOffset(float minOffset) { this.minOffset = minOffset; } + public void setDamagePerSecond(float damagePerSecond) { + this.damagePerSecond = damagePerSecond; + } - public float getMaxOffset() { return maxOffset; } - public void setMaxOffset(float maxOffset) { this.maxOffset = maxOffset; } + public int getDamageInterval() { + return damageInterval; + } - public boolean isRandomOffset() { return randomOffset; } - public void setRandomOffset(boolean randomOffset) { this.randomOffset = randomOffset; } + public void setDamageInterval(int damageInterval) { + this.damageInterval = damageInterval; + } + + public boolean isIgnoreInvulnFrames() { + return ignoreInvulnFrames; + } + + public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { + this.ignoreInvulnFrames = ignoreInvulnFrames; + } + + public int getSlownessLevel() { + return slownessLevel; + } + + public void setSlownessLevel(int slownessLevel) { + this.slownessLevel = slownessLevel; + } + + public int getWeaknessLevel() { + return weaknessLevel; + } + + public void setWeaknessLevel(int weaknessLevel) { + this.weaknessLevel = weaknessLevel; + } + + public int getPoisonLevel() { + return poisonLevel; + } + + public void setPoisonLevel(int poisonLevel) { + this.poisonLevel = poisonLevel; + } + + public int getWitherLevel() { + return witherLevel; + } + + public void setWitherLevel(int witherLevel) { + this.witherLevel = witherLevel; + } + + public int getBlindnessLevel() { + return blindnessLevel; + } + + public void setBlindnessLevel(int blindnessLevel) { + this.blindnessLevel = blindnessLevel; + } + + public int getDebuffDuration() { + return debuffDuration; + } + + public void setDebuffDuration(int debuffDuration) { + this.debuffDuration = debuffDuration; + } + + public boolean isAffectsCaster() { + return affectsCaster; + } + + public void setAffectsCaster(boolean affectsCaster) { + this.affectsCaster = affectsCaster; + } + + public float getHeightAbove() { + return heightAbove; + } + + public void setHeightAbove(float heightAbove) { + this.heightAbove = heightAbove; + } + + public float getHeightBelow() { + return heightBelow; + } + + public void setHeightBelow(float heightBelow) { + this.heightBelow = heightBelow; + } + + public float getMinOffset() { + return minOffset; + } + + public void setMinOffset(float minOffset) { + this.minOffset = minOffset; + } + + public float getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(float maxOffset) { + this.maxOffset = maxOffset; + } + + public boolean isRandomOffset() { + return randomOffset; + } + + public void setRandomOffset(boolean randomOffset) { + this.randomOffset = randomOffset; + } // Runtime getters - public double getZoneX() { return zoneX; } - public double getZoneY() { return zoneY; } - public double getZoneZ() { return zoneZ; } + public double getZoneX() { + return zoneX; + } + + public double getZoneY() { + return zoneY; + } + + public double getZoneZ() { + return zoneZ; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000), + FieldDef.enumField("ability.shape", HazardShape.class, this::getShapeEnum, this::setShapeEnum) + .hover("ability.hover.shape"), + FieldDef.enumField("ability.placement", PlacementMode.class, this::getPlacementEnum, this::setPlacementEnum) + .hover("ability.hover.placement"), + FieldDef.section("ability.section.area"), + FieldDef.floatField("gui.radius", this::getRadius, this::setRadius).column(ColumnHint.LEFT), + FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.damage"), + FieldDef.floatField("ability.damagePerSecond", this::getDamagePerSecond, this::setDamagePerSecond).column(ColumnHint.LEFT), + FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.debuffs"), + FieldDef.intField("ability.slownessLevel", this::getSlownessLevel, this::setSlownessLevel).column(ColumnHint.LEFT), + FieldDef.intField("ability.debuffDuration", this::getDebuffDuration, this::setDebuffDuration).column(ColumnHint.RIGHT), + FieldDef.intField("ability.poisonLevel", this::getPoisonLevel, this::setPoisonLevel), + FieldDef.boolField("ability.affectsCaster", this::isAffectsCaster, this::setAffectsCaster) + .hover("ability.hover.affectsCaster"), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 4a393682b..1d9be8553 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -1,30 +1,33 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeal; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; +import noppes.npcs.api.ability.type.IAbilityHeal; + import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** * Heal ability: Restore health to self or allies. * Can heal a fixed amount or percentage of max health. */ -public class AbilityHeal extends Ability { +public class AbilityHeal extends Ability implements IAbilityHeal { // Type-specific parameters + private int durationTicks = 60; private float healAmount = 10.0f; private float healPercent = 0.0f; private boolean healSelf = true; @@ -33,88 +36,141 @@ public class AbilityHeal extends Ability { private boolean instantHeal = true; // Runtime state - private transient List healedAllies = new ArrayList<>(); + private transient List healedAllies; + + private List getHealedAllies() { + if (healedAllies == null) { + healedAllies = new ArrayList<>(); + } + return healedAllies; + } public AbilityHeal() { this.typeId = "ability.cnpc.heal"; this.name = "Heal"; this.targetingMode = TargetingMode.SELF; - this.lockMovement = true; - this.cooldownTicks = 200; + this.lockMovement = LockMovementType.WINDUP; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 10; - this.recoveryTicks = 10; // No telegraph for heal - it's a self/ally buff this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityHeal(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.SELF }; + return new TargetingMode[]{TargetingMode.SELF}; } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) return; - healedAllies.clear(); + getHealedAllies().clear(); + + // Always find allies if we're healing them (needed for both instant and HoT) + if (healAllies && healRadius > 0) { + findAlliesInRadius(caster, world); + } if (instantHeal) { + // Instant heal - apply all healing now if (healSelf) { - healEntity(npc); - spawnHealParticles(world, npc); + healEntity(caster); + spawnHealParticles(world, caster); } - if (healAllies && healRadius > 0) { - healAlliesInRadius(npc, world); + for (EntityLivingBase ally : getHealedAllies()) { + healEntity(ally); + spawnHealParticles(world, ally); } + + // Instant heal completes immediately + signalCompletion(); } + // If not instant, HoT healing is applied in onActiveTick } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote || instantHeal) return; - // Heal over time - distribute heal across active ticks + // Heal over time - distribute heal across duration ticks if (tick % 10 == 0) { // Calculate tick-based heal for fixed amount - float tickHeal = (healAmount / (float) activeTicks) * 10; + float tickHeal = (healAmount / (float) durationTicks) * 10; if (healSelf) { float selfTickHeal = tickHeal; // Add percentage-based heal portion if (healPercent > 0) { - selfTickHeal += (npc.getMaxHealth() * healPercent / (float) activeTicks) * 10; + selfTickHeal += (caster.getMaxHealth() * healPercent / (float) durationTicks) * 10; + } + caster.heal(selfTickHeal); + if (tick % 20 == 0) { + spawnHealParticles(world, caster); } - npc.heal(selfTickHeal); } if (healAllies && healRadius > 0) { - for (EntityLivingBase ally : healedAllies) { + for (EntityLivingBase ally : getHealedAllies()) { if (!ally.isDead) { float allyTickHeal = tickHeal; // Add percentage-based heal portion for each ally if (healPercent > 0) { - allyTickHeal += (ally.getMaxHealth() * healPercent / (float) activeTicks) * 10; + allyTickHeal += (ally.getMaxHealth() * healPercent / (float) durationTicks) * 10; } ally.heal(allyTickHeal); + if (tick % 20 == 0) { + spawnHealParticles(world, ally); + } } } } } + + // Check if heal duration has ended + if (tick >= durationTicks) { + signalCompletion(); + } + } + + /** + * Find all allies in radius and add them to healedAllies list. + */ + private void findAlliesInRadius(EntityLivingBase caster, World world) { + AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox( + caster.posX - healRadius, caster.posY - 2, caster.posZ - healRadius, + caster.posX + healRadius, caster.posY + 3, caster.posZ + healRadius + ); + + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, aabb); + + for (Entity entity : entities) { + if (!(entity instanceof EntityLivingBase)) continue; + if (entity == caster) continue; + + EntityLivingBase living = (EntityLivingBase) entity; + + // Only heal other NPCs (allies) + if (living instanceof EntityNPCInterface) { + float dist = caster.getDistanceToEntity(living); + if (dist <= healRadius) { + getHealedAllies().add(living); + } + } + } } private void healEntity(EntityLivingBase entity) { @@ -125,33 +181,6 @@ public class AbilityHeal extends Ability { entity.heal(totalHeal); } - private void healAlliesInRadius(EntityNPCInterface npc, World world) { - AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox( - npc.posX - healRadius, npc.posY - 2, npc.posZ - healRadius, - npc.posX + healRadius, npc.posY + 3, npc.posZ + healRadius - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, aabb); - - for (Entity entity : entities) { - if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; - - EntityLivingBase living = (EntityLivingBase) entity; - - // Only heal other NPCs (allies) - if (living instanceof EntityNPCInterface) { - float dist = npc.getDistanceToEntity(living); - if (dist <= healRadius) { - healEntity(living); - healedAllies.add(living); - spawnHealParticles(world, living); - } - } - } - } - private void spawnHealParticles(World world, EntityLivingBase entity) { for (int i = 0; i < 10; i++) { double offsetX = (world.rand.nextDouble() - 0.5) * entity.width; @@ -166,9 +195,8 @@ public class AbilityHeal extends Ability { } @Override - public void reset() { - super.reset(); - healedAllies.clear(); + public void cleanup() { + getHealedAllies().clear(); } @Override @@ -178,6 +206,7 @@ public class AbilityHeal extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("healAmount", healAmount); nbt.setFloat("healPercent", healPercent); nbt.setBoolean("healSelf", healSelf); @@ -188,6 +217,7 @@ public class AbilityHeal extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; this.healAmount = nbt.hasKey("healAmount") ? nbt.getFloat("healAmount") : 10.0f; this.healPercent = nbt.hasKey("healPercent") ? nbt.getFloat("healPercent") : 0.0f; this.healSelf = !nbt.hasKey("healSelf") || nbt.getBoolean("healSelf"); @@ -197,21 +227,77 @@ public class AbilityHeal extends Ability { } // Getters & Setters - public float getHealAmount() { return healAmount; } - public void setHealAmount(float healAmount) { this.healAmount = healAmount; } + public int getDurationTicks() { + return durationTicks; + } - public float getHealPercent() { return healPercent; } - public void setHealPercent(float healPercent) { this.healPercent = healPercent; } + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } - public boolean isHealSelf() { return healSelf; } - public void setHealSelf(boolean healSelf) { this.healSelf = healSelf; } + public float getHealAmount() { + return healAmount; + } - public boolean isHealAllies() { return healAllies; } - public void setHealAllies(boolean healAllies) { this.healAllies = healAllies; } + public void setHealAmount(float healAmount) { + this.healAmount = healAmount; + } - public float getHealRadius() { return healRadius; } - public void setHealRadius(float healRadius) { this.healRadius = healRadius; } + public float getHealPercent() { + return healPercent; + } - public boolean isInstantHeal() { return instantHeal; } - public void setInstantHeal(boolean instantHeal) { this.instantHeal = instantHeal; } + public void setHealPercent(float healPercent) { + this.healPercent = healPercent; + } + + public boolean isHealSelf() { + return healSelf; + } + + public void setHealSelf(boolean healSelf) { + this.healSelf = healSelf; + } + + public boolean isHealAllies() { + return healAllies; + } + + public void setHealAllies(boolean healAllies) { + this.healAllies = healAllies; + } + + public float getHealRadius() { + return healRadius; + } + + public void setHealRadius(float healRadius) { + this.healRadius = healRadius; + } + + public boolean isInstantHeal() { + return instantHeal; + } + + public void setInstantHeal(boolean instantHeal) { + this.instantHeal = instantHeal; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.boolField("ability.instantHeal", this::isInstantHeal, this::setInstantHeal) + .hover("ability.hover.instant"), + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) + .range(1, 1000).visibleWhen(() -> !this.isInstantHeal()), + FieldDef.section("ability.section.healing"), + FieldDef.floatField("ability.healAmount", this::getHealAmount, this::setHealAmount).column(ColumnHint.LEFT), + FieldDef.floatField("ability.healPercent", this::getHealPercent, this::setHealPercent).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.healRadius", this::getHealRadius, this::setHealRadius), + FieldDef.boolField("ability.healSelf", this::isHealSelf, this::setHealSelf) + .hover("ability.hover.healSelf").column(ColumnHint.LEFT), + FieldDef.boolField("ability.healAllies", this::isHealAllies, this::setHealAllies) + .hover("ability.hover.healAllies").column(ColumnHint.RIGHT) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 288af63d7..15ab3e04c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -1,32 +1,29 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeavyHit; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.world.World; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; +import noppes.npcs.api.ability.type.IAbilityHeavyHit; import noppes.npcs.entity.EntityNPCInterface; +import java.util.Arrays; +import java.util.List; + /** * Heavy Hit ability: Single-target melee attack with optional stun. * Deals high damage to one target and can stun them. */ -public class AbilityHeavyHit extends Ability { +public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { - private float damage = 15.0f; + private float damage = 8.0f; private float knockback = 2.0f; - private int slownessLevel = 1; - private int weaknessLevel = 0; - private int potionDurationSeconds = 2; public AbilityHeavyHit() { this.typeId = "ability.cnpc.heavy_hit"; @@ -34,89 +31,90 @@ public class AbilityHeavyHit extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 3.0f; this.minRange = 0.0f; - this.lockMovement = true; - this.cooldownTicks = 80; + this.lockMovement = LockMovementType.NO; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 5; - this.recoveryTicks = 20; this.telegraphType = TelegraphType.POINT; this.showTelegraph = false; this.windUpSound = "random.anvil_use"; this.activeSound = "random.anvil_land"; + this.windUpAnimationName = "Ability_HeavyHit_Windup"; + this.activeAnimationName = "Ability_HeavyHit_Active"; } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityHeavyHit(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote || target == null) return; + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote || target == null) { + signalCompletion(); + return; + } // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, target, damage, knockback); + boolean wasHit = applyAbilityDamage(caster, target, damage, knockback); - if (wasHit && potionDurationSeconds > 0) { - int durationTicks = potionDurationSeconds * 20; - if (slownessLevel >= 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, durationTicks, slownessLevel)); - } - if (weaknessLevel >= 0) { - target.addPotionEffect(new PotionEffect(Potion.weakness.id, durationTicks, weaknessLevel)); - } + // Apply effects if hit wasn't cancelled + if (wasHit) { + applyEffects(target); } } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // Heavy Hit is instant, no active tick needed + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Just enough delay to see the active animation + if (tick == 10) + signalCompletion(); } @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("damage", damage); nbt.setFloat("knockback", knockback); - nbt.setInteger("slownessLevel", slownessLevel); - nbt.setInteger("weaknessLevel", weaknessLevel); - nbt.setInteger("potionDurationSeconds", potionDurationSeconds); } @Override public void readTypeNBT(NBTTagCompound nbt) { - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 2.0f; - this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : 1; - this.weaknessLevel = nbt.hasKey("weaknessLevel") ? nbt.getInteger("weaknessLevel") : 0; - this.potionDurationSeconds = nbt.hasKey("potionDurationSeconds") ? nbt.getInteger("potionDurationSeconds") : 2; } // Getters & Setters - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getDamage() { + return damage; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setDamage(float damage) { + this.damage = damage; + } - public int getSlownessLevel() { return slownessLevel; } - public void setSlownessLevel(int slownessLevel) { this.slownessLevel = slownessLevel; } + public float getKnockback() { + return knockback; + } - public int getWeaknessLevel() { return weaknessLevel; } - public void setWeaknessLevel(int weaknessLevel) { this.weaknessLevel = weaknessLevel; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public int getPotionDurationSeconds() { return potionDurationSeconds; } - public void setPotionDurationSeconds(int potionDurationSeconds) { this.potionDurationSeconds = potionDurationSeconds; } + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.RIGHT), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java new file mode 100644 index 000000000..dac6cb932 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -0,0 +1,288 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.DamageSource; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.api.ability.type.IAbilityLaserShot; + +import java.util.Arrays; +import java.util.List; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Laser Shot ability: Fast expanding thin line that pierces through targets. + * Travels in a straight line from origin, damaging all entities it passes through. + */ +public class AbilityLaserShot extends Ability implements IAbilityLaserShot { + + // Laser properties + private float laserWidth = 0.3f; + private float expansionSpeed = 3.0f; + private int lingerTicks = 8; + + // Data classes + private final EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f); + private final EnergyCombatData combatData = new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f); + private final EnergyLightningData lightningData = new EnergyLightningData(); + private final EnergyLifespanData lifespanData = new EnergyLifespanData(40.0f, 100); + private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); + + // Transient state for laser entity (used for movement locking) + private transient EntityAbilityLaser laserEntity = null; + + public AbilityLaserShot() { + this.typeId = "ability.cnpc.laser_shot"; + this.name = "Laser Shot"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 35.0f; + this.minRange = 3.0f; + this.cooldownTicks = 0; + this.windUpTicks = 15; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.LINE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_Laser_Windup"; + this.activeAnimationName = "Ability_Laser_Active"; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + public boolean isTargetingModeLocked() { + return false; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public float getTelegraphLength() { + return lifespanData.maxDistance; + } + + @Override + public float getTelegraphWidth() { + return laserWidth * 2.0f; // Make telegraph slightly wider for visibility + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + if (tick == 1) { + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); + laserEntity = new EntityAbilityLaser( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + laserWidth, + colorData, combatData, lightningData, lifespanData, + expansionSpeed, lingerTicks + ); + + laserEntity.setupCharging(anchorData, windUpTicks); + + laserEntity.setEffects(this.effects); + world.spawnEntityInWorld(laserEntity); + } + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + } + + laserEntity.startMoving(target); + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (laserEntity == null || laserEntity.isDead) { + laserEntity = null; + signalCompletion(); + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + } + + @Override + public void cleanup() { + // Despawn laser entity if still alive + if (laserEntity != null && !laserEntity.isDead) { + laserEntity.setDead(); + } + laserEntity = null; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("laserWidth", laserWidth); + nbt.setFloat("expansionSpeed", expansionSpeed); + nbt.setInteger("lingerTicks", lingerTicks); + anchorData.writeNBT(nbt); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.laserWidth = nbt.hasKey("laserWidth") ? nbt.getFloat("laserWidth") : 0.3f; + this.expansionSpeed = nbt.hasKey("expansionSpeed") ? nbt.getFloat("expansionSpeed") : 3.0f; + this.lingerTicks = nbt.hasKey("lingerTicks") ? nbt.getInteger("lingerTicks") : 8; + anchorData.readNBT(nbt); + colorData.readNBT(nbt); + combatData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); + } + + // Getters & Setters - Standalone fields + public float getLaserWidth() { return laserWidth; } + public void setLaserWidth(float laserWidth) { this.laserWidth = laserWidth; } + public float getExpansionSpeed() { return expansionSpeed; } + public void setExpansionSpeed(float expansionSpeed) { this.expansionSpeed = expansionSpeed; } + public int getLingerTicks() { return lingerTicks; } + public void setLingerTicks(int lingerTicks) { this.lingerTicks = lingerTicks; } + + // Getters & Setters - Lifespan data + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } + + // Getters & Setters - Combat data + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { this.combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { this.combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } + // Getters & Setters - Color data + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { this.colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { this.colorData.outerColor = outerColor; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.colorData.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return colorData.outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { this.colorData.outerColorAlpha = outerColorAlpha; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.colorData.outerColorEnabled = outerColorEnabled; } + + // Getters & Setters - Lightning data + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningData.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningData.lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningData.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningData.lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningData.lightningRadius = lightningRadius; } + + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } + + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } + public int getAnchorPoint() { + return anchorData.anchorPoint.ordinal(); + } + public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityLaser laser = new EntityAbilityLaser(npc.worldObj); + laser.setupPreview(npc, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance); + return laser; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 60) : 60; + } + + @Override + public boolean spawnPreviewDuringWindup() { + return false; // Laser has no charging, spawns at active phase + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockbackUp", this::getKnockbackUp, this::setKnockbackUp).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.beam"), + FieldDef.floatField("ability.laserWidth", this::getLaserWidth, this::setLaserWidth).column(ColumnHint.LEFT), + FieldDef.floatField("ability.expansionSpeed", this::getExpansionSpeed, this::setExpansionSpeed).column(ColumnHint.RIGHT), + FieldDef.intField("ability.lingerTicks", this::getLingerTicks, this::setLingerTicks).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.section("ability.section.colors").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) + ); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index cf1555898..41abc96fc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -2,55 +2,49 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; -import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; +import net.minecraft.util.Vec3; import net.minecraft.world.World; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import noppes.npcs.api.ability.type.IAbilityOrb; + +import java.util.Arrays; +import java.util.List; import noppes.npcs.entity.EntityNPCInterface; -import java.util.List; - /** - * Orb ability: Homing projectile sphere that tracks target. - * Can be explosive on impact and/or apply debuffs. + * Orb ability: Spawns a homing projectile sphere that tracks target. + * The EntityAbilityOrb handles all movement, collision, and damage logic. + * This ability just configures and spawns the orb entity. */ -public class AbilityOrb extends Ability { +public class AbilityOrb extends Ability implements IAbilityOrb { - private float orbSpeed = 0.5f; + // Ability-specific properties private float orbSize = 1.0f; - private float damage = 15.0f; - private float knockback = 1.0f; - private float maxDistance = 30.0f; - private int maxLifetime = 200; - private boolean homing = true; - private float homingStrength = 0.15f; - private float homingRange = 20.0f; + // Data classes for energy properties + private final EnergyColorData colorData = new EnergyColorData(); + private final EnergyCombatData combatData = new EnergyCombatData(); + private final EnergyHomingData homingData = new EnergyHomingData(); + private final EnergyLightningData lightningData = new EnergyLightningData(); + private final EnergyLifespanData lifespanData = new EnergyLifespanData(); + private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); - private boolean explosive = false; - private float explosionRadius = 3.0f; - private float explosionDamageFalloff = 0.5f; - - private int stunDuration = 0; - private int slowDuration = 0; - private int slowLevel = 0; - - // Runtime state - private transient double orbX, orbY, orbZ; - private transient double velocityX, velocityY, velocityZ; - private transient double startX, startY, startZ; - private transient boolean hasHit = false; - private transient int ticksAlive = 0; + // Transient state for orb entity (used during windup charging) + private transient EntityAbilityOrb orbEntity = null; public AbilityOrb() { this.typeId = "ability.cnpc.orb"; @@ -58,310 +52,405 @@ public class AbilityOrb extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 25.0f; this.minRange = 5.0f; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 200; - this.recoveryTicks = 10; - this.telegraphType = TelegraphType.POINT; - this.showTelegraph = false; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animation + this.windUpAnimationName = "Ability_Orb_Windup"; + this.activeAnimationName = "Ability_Orb_Active"; } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityOrb(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - startX = orbX = npc.posX; - startY = orbY = npc.posY + npc.getEyeHeight() * 0.8; - startZ = orbZ = npc.posZ; - hasHit = false; - ticksAlive = 0; - - if (target != null) { - double dx = target.posX - orbX; - double dy = (target.posY + target.height * 0.5) - orbY; - double dz = target.posZ - orbZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0) { - velocityX = (dx / len) * orbSpeed; - velocityY = (dy / len) * orbSpeed; - velocityZ = (dz / len) * orbSpeed; - } - } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); - float pitch = (float) Math.toRadians(npc.rotationPitch); - velocityX = -Math.sin(yaw) * Math.cos(pitch) * orbSpeed; - velocityY = -Math.sin(pitch) * orbSpeed; - velocityZ = Math.cos(yaw) * Math.cos(pitch) * orbSpeed; - } - } - - @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (hasHit) return; - - ticksAlive++; - - if (ticksAlive >= maxLifetime) { - hasHit = true; + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); return; } - double distTraveled = Math.sqrt( - Math.pow(orbX - startX, 2) + - Math.pow(orbY - startY, 2) + - Math.pow(orbZ - startZ, 2) - ); - if (distTraveled >= maxDistance) { - hasHit = true; - return; + // Start moving the orb that was spawned during windup + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.startMoving(target); } - if (homing && target != null && target.isEntityAlive()) { - double targetX = target.posX; - double targetY = target.posY + target.height * 0.5; - double targetZ = target.posZ; - - double distToTarget = Math.sqrt( - Math.pow(targetX - orbX, 2) + - Math.pow(targetY - orbY, 2) + - Math.pow(targetZ - orbZ, 2) - ); - - if (distToTarget <= homingRange) { - double dx = targetX - orbX; - double dy = targetY - orbY; - double dz = targetZ - orbZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0) { - double desiredVX = (dx / len) * orbSpeed; - double desiredVY = (dy / len) * orbSpeed; - double desiredVZ = (dz / len) * orbSpeed; - - velocityX += (desiredVX - velocityX) * homingStrength; - velocityY += (desiredVY - velocityY) * homingStrength; - velocityZ += (desiredVZ - velocityZ) * homingStrength; - - double vLen = Math.sqrt(velocityX * velocityX + velocityY * velocityY + velocityZ * velocityZ); - if (vLen > 0) { - velocityX = (velocityX / vLen) * orbSpeed; - velocityY = (velocityY / vLen) * orbSpeed; - velocityZ = (velocityZ / vLen) * orbSpeed; - } - } - } - } - - orbX += velocityX; - orbY += velocityY; - orbZ += velocityZ; - - if (!world.isRemote) { - checkCollision(npc, world); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } - private void checkCollision(EntityNPCInterface npc, World world) { - AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( - orbX - orbSize, orbY - orbSize, orbZ - orbSize, - orbX + orbSize, orbY + orbSize, orbZ + orbSize - ); + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + // Spawn orb in charging mode on first tick of windup + if (tick == 1) { + // Create orb in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); + orbEntity = new EntityAbilityOrb( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, + colorData, combatData, homingData, lightningData, lifespanData); + orbEntity.setupCharging(anchorData, windUpTicks); - for (EntityLivingBase entity : entities) { - if (entity == npc) continue; - - hasHit = true; - - if (explosive) { - doExplosion(npc, world); - } else { - // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback); - if (wasHit) { - applyEffects(entity); - } - } - break; - } - - if (!hasHit && world.getBlock((int)orbX, (int)orbY, (int)orbZ).getMaterial().isSolid()) { - hasHit = true; - if (explosive) { - doExplosion(npc, world); - } - } - } - - private void doExplosion(EntityNPCInterface npc, World world) { - AxisAlignedBB explosionBox = AxisAlignedBB.getBoundingBox( - orbX - explosionRadius, orbY - explosionRadius, orbZ - explosionRadius, - orbX + explosionRadius, orbY + explosionRadius, orbZ + explosionRadius - ); - - @SuppressWarnings("unchecked") - List blastTargets = world.getEntitiesWithinAABB(EntityLivingBase.class, explosionBox); - - for (EntityLivingBase blastTarget : blastTargets) { - if (blastTarget == npc) continue; - - double dist = Math.sqrt( - Math.pow(blastTarget.posX - orbX, 2) + - Math.pow(blastTarget.posY - orbY, 2) + - Math.pow(blastTarget.posZ - orbZ, 2) - ); - - if (dist <= explosionRadius) { - float falloff = 1.0f - (float)(dist / explosionRadius) * explosionDamageFalloff; - // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, blastTarget, damage * falloff, knockback * falloff); - if (wasHit) { - applyEffects(blastTarget); - } - } - } - - world.playSoundEffect(orbX, orbY, orbZ, "random.explode", 1.0f, 1.0f); - } - - private void applyKnockback(EntityLivingBase target, float strength, float upward) { - double dx = target.posX - orbX; - double dz = target.posZ - orbZ; - double dist = Math.sqrt(dx * dx + dz * dz); - if (dist > 0) { - target.motionX += (dx / dist) * strength; - target.motionZ += (dz / dist) * strength; - } - target.motionY += upward; - target.velocityChanged = true; - } - - private void applyEffects(EntityLivingBase target) { - if (stunDuration > 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - target.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - if (slowDuration > 0 && slowLevel > 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, slowDuration, slowLevel)); + orbEntity.setEffects(this.effects); + world.spawnEntityInWorld(orbEntity); } } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - hasHit = false; - ticksAlive = 0; + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (orbEntity == null || orbEntity.isDead) { + orbEntity = null; + signalCompletion(); + } } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - hasHit = false; - ticksAlive = 0; + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + // Nothing to clean up - entity manages itself + } + + @Override + public void cleanup() { + // Despawn orb entity if still alive + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.setDead(); + } + orbEntity = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create small circle telegraph at target position + Telegraph telegraph = Telegraph.circle(orbSize * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return orbSize * 1.5f; } @Override public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("orbSpeed", orbSpeed); nbt.setFloat("orbSize", orbSize); - nbt.setFloat("damage", damage); - nbt.setFloat("knockback", knockback); - nbt.setFloat("maxDistance", maxDistance); - nbt.setInteger("maxLifetime", maxLifetime); - nbt.setBoolean("homing", homing); - nbt.setFloat("homingStrength", homingStrength); - nbt.setFloat("homingRange", homingRange); - nbt.setBoolean("explosive", explosive); - nbt.setFloat("explosionRadius", explosionRadius); - nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("slowDuration", slowDuration); - nbt.setInteger("slowLevel", slowLevel); + anchorData.writeNBT(nbt); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + // Backward compat: old key was "orbSpeed" + nbt.setFloat("orbSpeed", homingData.speed); } @Override public void readTypeNBT(NBTTagCompound nbt) { - this.orbSpeed = nbt.hasKey("orbSpeed") ? nbt.getFloat("orbSpeed") : 0.5f; this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; - this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 30.0f; - this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; - this.homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); - this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.15f; - this.homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 20.0f; - this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); - this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f; - this.explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; - this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; + anchorData.readNBT(nbt); + colorData.readNBT(nbt); + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); + // Backward compat: old key was "orbSpeed" + if (nbt.hasKey("orbSpeed")) { + homingData.speed = nbt.getFloat("orbSpeed"); + } } // Getters & Setters - public float getOrbSpeed() { return orbSpeed; } - public void setOrbSpeed(float orbSpeed) { this.orbSpeed = orbSpeed; } + public float getOrbSpeed() { + return homingData.speed; + } - public float getOrbSize() { return orbSize; } - public void setOrbSize(float orbSize) { this.orbSize = orbSize; } + public void setOrbSpeed(float orbSpeed) { + homingData.speed = orbSpeed; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getOrbSize() { + return orbSize; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setOrbSize(float orbSize) { + this.orbSize = orbSize; + } - public float getMaxDistance() { return maxDistance; } - public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } + public float getMaxDistance() { + return lifespanData.maxDistance; + } - public int getMaxLifetime() { return maxLifetime; } - public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } + public void setMaxDistance(float maxDistance) { + lifespanData.maxDistance = maxDistance; + } - public boolean isHoming() { return homing; } - public void setHoming(boolean homing) { this.homing = homing; } + public int getMaxLifetime() { + return lifespanData.maxLifetime; + } - public float getHomingStrength() { return homingStrength; } - public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + public void setMaxLifetime(int maxLifetime) { + lifespanData.maxLifetime = maxLifetime; + } - public float getHomingRange() { return homingRange; } - public void setHomingRange(float homingRange) { this.homingRange = homingRange; } + public float getDamage() { + return combatData.damage; + } - public boolean isExplosive() { return explosive; } - public void setExplosive(boolean explosive) { this.explosive = explosive; } + public void setDamage(float damage) { + combatData.damage = damage; + } - public float getExplosionRadius() { return explosionRadius; } - public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public float getKnockback() { + return combatData.knockback; + } - public float getExplosionDamageFalloff() { return explosionDamageFalloff; } - public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } + public void setKnockback(float knockback) { + combatData.knockback = knockback; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public float getKnockbackUp() { + return combatData.knockbackUp; + } - public int getSlowDuration() { return slowDuration; } - public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } + public void setKnockbackUp(float knockbackUp) { + combatData.knockbackUp = knockbackUp; + } - public int getSlowLevel() { return slowLevel; } - public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } + public boolean isHoming() { + return homingData.homing; + } - // Runtime getters for rendering - public double getOrbX() { return orbX; } - public double getOrbY() { return orbY; } - public double getOrbZ() { return orbZ; } - public boolean hasHit() { return hasHit; } + public void setHoming(boolean homing) { + homingData.homing = homing; + } + + public float getHomingStrength() { + return homingData.homingStrength; + } + + public void setHomingStrength(float homingStrength) { + homingData.homingStrength = homingStrength; + } + + public float getHomingRange() { + return homingData.homingRange; + } + + public void setHomingRange(float homingRange) { + homingData.homingRange = homingRange; + } + + public boolean isExplosive() { + return combatData.explosive; + } + + public void setExplosive(boolean explosive) { + combatData.explosive = explosive; + } + + public float getExplosionRadius() { + return combatData.explosionRadius; + } + + public void setExplosionRadius(float explosionRadius) { + combatData.explosionRadius = explosionRadius; + } + + public float getExplosionDamageFalloff() { + return combatData.explosionDamageFalloff; + } + + public void setExplosionDamageFalloff(float explosionDamageFalloff) { + combatData.explosionDamageFalloff = explosionDamageFalloff; + } + + public int getInnerColor() { + return colorData.innerColor; + } + + public void setInnerColor(int innerColor) { + colorData.innerColor = innerColor; + } + + public int getOuterColor() { + return colorData.outerColor; + } + + public void setOuterColor(int outerColor) { + colorData.outerColor = outerColor; + } + + public float getOuterColorWidth() { + return colorData.outerColorWidth; + } + + public void setOuterColorWidth(float outerColorWidth) { + colorData.outerColorWidth = outerColorWidth; + } + + public float getOuterColorAlpha() { + return colorData.outerColorAlpha; + } + + public void setOuterColorAlpha(float outerColorAlpha) { + colorData.outerColorAlpha = outerColorAlpha; + } + + public boolean isOuterColorEnabled() { + return colorData.outerColorEnabled; + } + + public void setOuterColorEnabled(boolean outerColorEnabled) { + colorData.outerColorEnabled = outerColorEnabled; + } + + public float getRotationSpeed() { + return colorData.rotationSpeed; + } + + public void setRotationSpeed(float rotationSpeed) { + colorData.rotationSpeed = rotationSpeed; + } + + public boolean hasLightningEffect() { + return lightningData.lightningEffect; + } + + public void setLightningEffect(boolean lightningEffect) { + lightningData.lightningEffect = lightningEffect; + } + + public float getLightningDensity() { + return lightningData.lightningDensity; + } + + public void setLightningDensity(float lightningDensity) { + lightningData.lightningDensity = lightningDensity; + } + + public float getLightningRadius() { + return lightningData.lightningRadius; + } + + public void setLightningRadius(float lightningRadius) { + lightningData.lightningRadius = lightningRadius; + } + + public int getLightningFadeTime() { + return lightningData.lightningFadeTime; + } + + public void setLightningFadeTime(int lightningFadeTime) { + lightningData.lightningFadeTime = lightningFadeTime; + } + + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } + + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } + + public int getAnchorPoint() { + return anchorData.anchorPoint.ordinal(); + } + + @Override + public void setAnchorPoint(int point) { + this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); + } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); + orb.setupPreview(npc, orbSize, colorData, lightningData, anchorData, windUpTicks); + return orb; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.colors").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").customTab("ability.tab.visual"), + FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed).customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java new file mode 100644 index 000000000..cb9ace86a --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java @@ -0,0 +1,565 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.util.ValueUtil; + +import java.util.Arrays; +import java.util.List; + +public class AbilityOrbDual extends Ability { + // Ability-specific properties + private float orbSize = 1.0f; + private boolean dualFire= true; + private int dualFireDelay = 20; + + // Data classes for energy properties + public final EnergyColorData[] colorData = new EnergyColorData[]{ + new EnergyColorData(), new EnergyColorData() + }; + public final EnergyCombatData combatData = new EnergyCombatData(); + public final EnergyHomingData homingData = new EnergyHomingData(); + public final EnergyLightningData[] lightningData = new EnergyLightningData[]{ + new EnergyLightningData(), new EnergyLightningData() + }; + public final EnergyLifespanData lifespanData = new EnergyLifespanData(); + private EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ + new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND) + }; + + // Transient state for orb entity (used during windup charging) + private transient EntityAbilityOrb orbEntity = null; + private transient EntityAbilityOrb orbEntity2 = null; + + public AbilityOrbDual() { + this.typeId = "ability.cnpc.orb_dual"; + this.name = "Orb Dual"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 25.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 30; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animation + this.windUpAnimationName = "Ability_BeamDual_Windup"; + this.activeAnimationName = "Ability_BeamDual_Active"; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start moving the orb that was spawned during windup + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.startMoving(target); + } + + if (dualFire) { + if (orbEntity2 != null && !orbEntity2.isDead && dualFireDelay <= 0) { + orbEntity2.startMoving(target); + } + } else { + orbEntity2.setDead(); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn orb in charging mode on first tick of windup + if (tick == 1) { + // Create orb in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]); + orbEntity = new EntityAbilityOrb( + world, caster, target, + spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, orbSize, + colorData[0], combatData, homingData, lightningData[0], lifespanData); + + Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]); + orbEntity2 = new EntityAbilityOrb( + world, caster, target, + spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, orbSize, + colorData[1], combatData, homingData, lightningData[1], lifespanData); + + orbEntity.setSiblingUUID(orbEntity2.getPersistentID()); + orbEntity2.setSiblingUUID(orbEntity.getPersistentID()); + + orbEntity.setupCharging(anchorData[0], windUpTicks); + orbEntity2.setupCharging(anchorData[1], windUpTicks); + + orbEntity.setEffects(this.effects); + + if (dualFire) { + orbEntity2.setEffects(this.effects); + } + + world.spawnEntityInWorld(orbEntity); + world.spawnEntityInWorld(orbEntity2); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (dualFire) { + if ((orbEntity == null || orbEntity.isDead) && (orbEntity2 == null || orbEntity2.isDead)) { + orbEntity = null; + orbEntity2 = null; + signalCompletion(); + } + + if (tick == dualFireDelay && orbEntity2 != null && !orbEntity2.isDead) { + orbEntity2.startMoving(target); + } + } else { + if (orbEntity == null || orbEntity.isDead) { + orbEntity = null; + signalCompletion(); + } + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + // Nothing to clean up - entity manages itself + } + + @Override + public void cleanup() { + // Despawn orb entity if still alive + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.setDead(); + } + + if (orbEntity2 != null && !orbEntity2.isDead) { + orbEntity2.setDead(); + } + + orbEntity = null; + orbEntity2 = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create small circle telegraph at target position + Telegraph telegraph = Telegraph.circle(orbSize * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return orbSize * 1.5f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("orbSize", orbSize); + nbt.setBoolean("dualFire", dualFire); + nbt.setInteger("dualFireDelay", dualFireDelay); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + // Backward compat: old key was "orbSpeed" + nbt.setFloat("orbSpeed", homingData.speed); + + NBTTagCompound orb1 = new NBTTagCompound(); + NBTTagCompound orb2 = new NBTTagCompound(); + + anchorData[0].writeNBT(orb1); + anchorData[1].writeNBT(orb2); + colorData[0].writeNBT(orb1); + colorData[1].writeNBT(orb2); + lightningData[0].writeNBT(orb1); + lightningData[1].writeNBT(orb2); + + nbt.setTag("Orb_1", orb1); + nbt.setTag("Orb_2", orb2); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + NBTTagCompound orb1 = nbt.hasKey("Orb_1") ? nbt.getCompoundTag("Orb_1") : new NBTTagCompound(); + NBTTagCompound orb2 = nbt.hasKey("Orb_2") ? nbt.getCompoundTag("Orb_2") : new NBTTagCompound(); + + this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; + this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); + this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lifespanData.readNBT(nbt); + // Backward compat: old key was "orbSpeed" + if (nbt.hasKey("orbSpeed")) { + homingData.speed = nbt.getFloat("orbSpeed"); + } + + anchorData[0].readNBT(orb1); + anchorData[1].readNBT(orb2); + colorData[0].readNBT(orb1); + colorData[1].readNBT(orb2); + lightningData[0].readNBT(orb1); + lightningData[1].readNBT(orb2); + } + + // Getters & Setters + private EnergyColorData getColorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return colorData[orb]; + } + private EnergyLightningData getLightningData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return lightningData[orb]; + } + private EnergyAnchorData getAnchorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return anchorData[orb]; + } + + public float getOrbSpeed() { + return homingData.speed; + } + + public void setOrbSpeed(float orbSpeed) { + homingData.speed = orbSpeed; + } + + public float getOrbSize() { + return orbSize; + } + + public void setOrbSize(float orbSize) { + this.orbSize = orbSize; + } + + public boolean isDualFire() { + return dualFire; + } + + public void setDualFire(boolean dualFire) { + this.dualFire = dualFire; + } + + public int getDualFireDelay() { + return dualFireDelay; + } + + public void setDualFireDelay(int dualFireDelay) { + this.dualFireDelay = dualFireDelay; + } + + public float getMaxDistance() { + return lifespanData.maxDistance; + } + + public void setMaxDistance(float maxDistance) { + lifespanData.maxDistance = maxDistance; + } + + public int getMaxLifetime() { + return lifespanData.maxLifetime; + } + + public void setMaxLifetime(int maxLifetime) { + lifespanData.maxLifetime = maxLifetime; + } + + public float getDamage() { + return combatData.damage; + } + + public void setDamage(float damage) { + combatData.damage = damage; + } + + public float getKnockback() { + return combatData.knockback; + } + + public void setKnockback(float knockback) { + combatData.knockback = knockback; + } + + public float getKnockbackUp() { + return combatData.knockbackUp; + } + + public void setKnockbackUp(float knockbackUp) { + combatData.knockbackUp = knockbackUp; + } + + public boolean isHoming() { + return homingData.homing; + } + + public void setHoming(boolean homing) { + homingData.homing = homing; + } + + public float getHomingStrength() { + return homingData.homingStrength; + } + + public void setHomingStrength(float homingStrength) { + homingData.homingStrength = homingStrength; + } + + public float getHomingRange() { + return homingData.homingRange; + } + + public void setHomingRange(float homingRange) { + homingData.homingRange = homingRange; + } + + public boolean isExplosive() { + return combatData.explosive; + } + + public void setExplosive(boolean explosive) { + combatData.explosive = explosive; + } + + public float getExplosionRadius() { + return combatData.explosionRadius; + } + + public void setExplosionRadius(float explosionRadius) { + combatData.explosionRadius = explosionRadius; + } + + public float getExplosionDamageFalloff() { + return combatData.explosionDamageFalloff; + } + + public void setExplosionDamageFalloff(float explosionDamageFalloff) { + combatData.explosionDamageFalloff = explosionDamageFalloff; + } + + + public int getInnerColor(int orb) { return getColorData(orb).innerColor; } + public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; } + public int getOuterColor(int orb) { return getColorData(orb).outerColor; } + public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; } + public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; } + public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; } + public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; } + public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; } + public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; } + public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; } + public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; } + public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; } + + public int getInnerColor() { return getInnerColor(0); } + public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } + public int getOuterColor() { return getOuterColor(0); } + public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } + public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } + public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } + public float getOuterColorWidth() { return getOuterColorWidth(0); } + public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } + public float getOuterColorAlpha() { return getOuterColorAlpha(0); } + public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } + public float getRotationSpeed() { return getRotationSpeed(0); } + public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } + + + public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; } + public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; } + public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; } + public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; } + public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; } + public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; } + + public boolean hasLightningEffect() { return hasLightningEffect(0); } + public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } + public float getLightningDensity() { return getLightningDensity(0); } + public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } + public float getLightningRadius() { return getLightningRadius(0); } + public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } + + + public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; } + public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; } + public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; } + public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; } + public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; } + public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; } + public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; } + public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; } + + public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } + public float getAnchorOffsetX() { return getAnchorOffsetX(0); } + public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } + public float getAnchorOffsetY() { return getAnchorOffsetY(0); } + public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } + public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } + public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } + + //@Override + public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); } + public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } + + //@Override + public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.dual"), + FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire), + FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay) + .visibleWhen(this::isDualFire), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab - Orb 1 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.orb1").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(0), v -> setInnerColor(0, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v)) + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(0), v -> setOuterColor(0, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v)) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(0)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(0), v -> setRotationSpeed(0, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(0), v -> setLightningEffect(0, v)) + .customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(0), v -> setLightningDensity(0, v)) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(0), v -> setLightningRadius(0, v)) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0)).column(ColumnHint.RIGHT), + // Visual tab - Orb 2 + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.orb2").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", + () -> getInnerColor(1), v -> setInnerColor(1, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", + () -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v)) + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", + () -> getOuterColor(1), v -> setOuterColor(1, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)), + FieldDef.floatField("ability.outerWidth", + () -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v)) + .customTab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", + () -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v)) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(() -> isOuterColorEnabled(1)).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.rotationSpeed", + () -> getRotationSpeed(1), v -> setRotationSpeed(1, v)) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", + () -> hasLightningEffect(1), v -> setLightningEffect(1, v)) + .customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", + () -> getLightningDensity(1), v -> setLightningDensity(1, v)) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", + () -> getLightningRadius(1), v -> setLightningRadius(1, v)) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(1)).column(ColumnHint.RIGHT) + ); + } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); + orb.setupPreview(npc, orbSize, colorData[0], lightningData[0], anchorData[0], windUpTicks); + return orb; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 4d2143b36..79b2b6c9f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -1,30 +1,32 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityProjectile; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityProjectile; + +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; import java.util.List; /** * Projectile ability: Ranged attack that deals damage to target. * Currently instant damage - can be enhanced with custom projectile entities. */ -public class AbilityProjectile extends Ability { +public class AbilityProjectile extends Ability implements IAbilityProjectile { // Type-specific parameters - private float damage = 8.0f; + private float damage = 6.0f; private float speed = 1.5f; private float knockback = 0.5f; private String projectileType = "fireball"; @@ -39,40 +41,37 @@ public class AbilityProjectile extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.minRange = 5.0f; this.maxRange = 20.0f; - this.cooldownTicks = 60; + this.cooldownTicks = 0; this.windUpTicks = 15; - this.activeTicks = 5; - this.recoveryTicks = 10; + this.lockMovement = LockMovementType.NO; // No telegraph for projectile - it's a ranged attack this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityProjectile(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote || target == null) return; // Calculate direction to target - double dx = target.posX - npc.posX; - double dy = (target.posY + target.height / 2) - (npc.posY + npc.getEyeHeight()); - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight()); + double dz = target.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { @@ -83,10 +82,10 @@ public class AbilityProjectile extends Ability { // Deal instant damage with scripted event support // TODO: Use custom EntityAbilityProjectile for actual tracking - applyAbilityDamageWithDirection(npc, target, damage, knockback, dx, dz); + applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); // Play sound - world.playSoundAtEntity(npc, "random.bow", 1.0f, 0.8f); + world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); // Handle splash damage for explosive projectiles if (explosive && explosionRadius > 0) { @@ -95,13 +94,13 @@ public class AbilityProjectile extends Ability { target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius)); for (Entity entity : entities) { - if (entity instanceof EntityLivingBase && entity != npc) { + if (entity instanceof EntityLivingBase && entity != caster) { EntityLivingBase living = (EntityLivingBase) entity; float dist = target.getDistanceToEntity(living); if (dist < explosionRadius) { float falloff = 1.0f - (dist / explosionRadius); // Apply splash damage with scripted event support (no knockback) - applyAbilityDamage(npc, living, damage * falloff * 0.5f, 0); + applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0); } } } @@ -118,13 +117,16 @@ public class AbilityProjectile extends Ability { } // Spawn projectile particles (visual trail) - spawnProjectileParticles(world, npc, target); + spawnProjectileParticles(world, caster, target); + + // Projectile is instant + signalCompletion(); } - private void spawnProjectileParticles(World world, EntityNPCInterface npc, EntityLivingBase target) { - double startX = npc.posX; - double startY = npc.posY + npc.getEyeHeight(); - double startZ = npc.posZ; + private void spawnProjectileParticles(World world, EntityLivingBase caster, EntityLivingBase target) { + double startX = caster.posX; + double startY = caster.posY + caster.getEyeHeight(); + double startZ = caster.posZ; double endX = target.posX; double endY = target.posY + target.height / 2; double endZ = target.posZ; @@ -153,7 +155,7 @@ public class AbilityProjectile extends Ability { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Projectile is instant, nothing to do per-tick } @@ -176,7 +178,7 @@ public class AbilityProjectile extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; this.speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 1.5f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; this.projectileType = nbt.hasKey("projectileType") ? nbt.getString("projectileType") : "fireball"; @@ -187,27 +189,84 @@ public class AbilityProjectile extends Ability { } // Getters & Setters - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getDamage() { + return damage; + } - public float getSpeed() { return speed; } - public void setSpeed(float speed) { this.speed = speed; } + public void setDamage(float damage) { + this.damage = damage; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public float getSpeed() { + return speed; + } - public String getProjectileType() { return projectileType; } - public void setProjectileType(String projectileType) { this.projectileType = projectileType; } + public void setSpeed(float speed) { + this.speed = speed; + } - public boolean isExplosive() { return explosive; } - public void setExplosive(boolean explosive) { this.explosive = explosive; } + public float getKnockback() { + return knockback; + } - public float getExplosionRadius() { return explosionRadius; } - public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public boolean isHoming() { return homing; } - public void setHoming(boolean homing) { this.homing = homing; } + public String getProjectileType() { + return projectileType; + } - public float getHomingStrength() { return homingStrength; } - public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + public void setProjectileType(String projectileType) { + this.projectileType = projectileType; + } + + public boolean isExplosive() { + return explosive; + } + + public void setExplosive(boolean explosive) { + this.explosive = explosive; + } + + public float getExplosionRadius() { + return explosionRadius; + } + + public void setExplosionRadius(float explosionRadius) { + this.explosionRadius = explosionRadius; + } + + public boolean isHoming() { + return homing; + } + + public void setHoming(boolean homing) { + this.homing = homing; + } + + public float getHomingStrength() { + return homingStrength; + } + + public void setHomingStrength(float homingStrength) { + this.homingStrength = homingStrength; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.stringEnumField("ability.projectileType", new String[]{"fireball", "arrow", "magic"}, this::getProjectileType, this::setProjectileType), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 340b92826..447585280 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -1,103 +1,91 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityShockwave; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; +import noppes.npcs.api.ability.type.IAbilityShockwave; + +import java.util.Arrays; import java.util.List; /** * Shockwave ability: Pushes targets away from the caster with damage. * Opposite of Vortex - instant knockback rather than gradual pull. */ -public class AbilityShockwave extends Ability { +public class AbilityShockwave extends Ability implements IAbilityShockwave { private float pushRadius = 8.0f; private float pushStrength = 1.5f; - private float damage = 10.0f; - private int stunDuration = 0; + private float damage = 8.0f; private int maxTargets = 10; - // Runtime state - private transient boolean executed = false; - public AbilityShockwave() { this.typeId = "ability.cnpc.shockwave"; this.name = "Shockwave"; this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 15.0f; - this.lockMovement = true; - this.cooldownTicks = 100; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.cooldownTicks = 0; this.windUpTicks = 25; - this.activeTicks = 5; - this.recoveryTicks = 20; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "random.explode"; this.activeSound = "random.explode"; } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityShockwave(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF }; + return new TargetingMode[]{TargetingMode.AOE_SELF}; } @Override - public float getTelegraphRadius() { return pushRadius; } - - @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - executed = false; + public float getTelegraphRadius() { + return pushRadius; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (executed) return; - executed = true; + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + // Shockwave is instant - apply effect immediately after windup // Get all entities in radius - AxisAlignedBB box = npc.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); + AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); int count = 0; for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (entity.isDead) continue; - double dist = npc.getDistanceToEntity(entity); + double dist = caster.getDistanceToEntity(entity); if (dist > pushRadius) continue; count++; if (count > maxTargets) break; // Calculate push direction (away from caster) - double dx = entity.posX - npc.posX; - double dz = entity.posZ - npc.posZ; + double dx = entity.posX - caster.posX; + double dz = entity.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dz * dz); if (len > 0) { @@ -111,27 +99,24 @@ public class AbilityShockwave extends Ability { } // Scale knockback by distance (closer = stronger) - float distFactor = 1.0f - (float)(dist / pushRadius) * 0.5f; + float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; float finalPush = pushStrength * distFactor; // Apply damage with custom knockback direction - boolean wasHit = applyAbilityDamageWithDirection(npc, entity, damage * distFactor, finalPush, dx, dz); + boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz); - // Apply stun if hit connected - if (wasHit && stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); + // Apply effects if hit connected + if (wasHit) { + applyEffects(entity); } } + + // Shockwave completes instantly + signalCompletion(); } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - executed = false; - } - - @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - executed = false; + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Shockwave is instant - nothing to do per-tick } @Override @@ -139,7 +124,6 @@ public class AbilityShockwave extends Ability { nbt.setFloat("pushRadius", pushRadius); nbt.setFloat("pushStrength", pushStrength); nbt.setFloat("damage", damage); - nbt.setInteger("stunDuration", stunDuration); nbt.setInteger("maxTargets", maxTargets); } @@ -147,25 +131,52 @@ public class AbilityShockwave extends Ability { public void readTypeNBT(NBTTagCompound nbt) { this.pushRadius = nbt.hasKey("pushRadius") ? nbt.getFloat("pushRadius") : 8.0f; this.pushStrength = nbt.hasKey("pushStrength") ? nbt.getFloat("pushStrength") : 1.5f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 10; } // Getters & Setters - public float getPushRadius() { return pushRadius; } - public void setPushRadius(float pushRadius) { this.pushRadius = pushRadius; } + public float getPushRadius() { + return pushRadius; + } - public float getPushStrength() { return pushStrength; } - public void setPushStrength(float pushStrength) { this.pushStrength = pushStrength; } + public void setPushRadius(float pushRadius) { + this.pushRadius = pushRadius; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getPushStrength() { + return pushStrength; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public void setPushStrength(float pushStrength) { + this.pushStrength = pushStrength; + } - public int getMaxTargets() { return maxTargets; } - public void setMaxTargets(int maxTargets) { this.maxTargets = maxTargets; } + public float getDamage() { + return damage; + } + public void setDamage(float damage) { + this.damage = damage; + } + + public int getMaxTargets() { + return maxTargets; + } + + public void setMaxTargets(int maxTargets) { + this.maxTargets = maxTargets; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.section("ability.section.push"), + FieldDef.floatField("gui.radius", this::getPushRadius, this::setPushRadius).column(ColumnHint.LEFT), + FieldDef.floatField("gui.strength", this::getPushStrength, this::setPushStrength).column(ColumnHint.RIGHT), + FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 321b9058f..b3ba2a76d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -3,36 +3,41 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; -import net.minecraft.util.MathHelper; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySlam; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilitySlam; +import somehussar.gui.annotationHandling.GuiEditable; + +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; +import java.util.Arrays; import java.util.List; /** * Slam ability: NPC leaps toward target in an arc and slams down, dealing AOE damage on landing. - * + *

* Phases: * - WINDUP: NPC prepares to leap, telegraph shows landing zone * - ACTIVE: NPC is in the air, traveling in arc toward destination * - Landing: When NPC hits ground, deal AOE damage * - RECOVERY: Landing recovery animation */ -public class AbilitySlam extends Ability { +@GuiEditable +public class AbilitySlam extends Ability implements IAbilitySlam { // Type-specific config - private float damage = 20.0f; + private float damage = 10.0f; private float radius = 5.0f; private float knockbackStrength = 1.5f; private float leapSpeed = 1.0f; @@ -50,24 +55,21 @@ public class AbilitySlam extends Ability { this.name = "Slam"; this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target this.windUpTicks = 30; - this.activeTicks = 60; // Max time in air + landing - this.recoveryTicks = 20; - this.cooldownTicks = 100; - this.lockMovement = true; + this.cooldownTicks = 0; + this.lockMovement = LockMovementType.WINDUP; this.minRange = 2.0f; this.maxRange = 15.0f; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "mob.irongolem.throw"; - this.activeSound = "random.explode"; + this.activeSound = ""; + // Default built-in animations + this.windUpAnimationName = "Ability_Slam_Windup"; + this.activeAnimationName = "Ability_Slam_Active"; } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilitySlam(this, callback); + public boolean hasTypeSettings() { + return true; } /** @@ -80,7 +82,7 @@ public class AbilitySlam extends Ability { @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF, TargetingMode.AOE_TARGET }; + return new TargetingMode[]{TargetingMode.AOE_SELF, TargetingMode.AOE_TARGET}; } @Override @@ -89,13 +91,13 @@ public class AbilitySlam extends Ability { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Update target position during windup if (targetingMode == TargetingMode.AOE_SELF) { - // AOE_SELF: slam at NPC's current position - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + // AOE_SELF: slam at caster's current position + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { // AOE_TARGET: telegraph follows target via setEntityIdToFollow targetX = target.posX; @@ -105,13 +107,13 @@ public class AbilitySlam extends Ability { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Lock in the destination at moment of launch if (targetingMode == TargetingMode.AOE_SELF) { // AOE_SELF: slam in place (just jump up) - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { // AOE_TARGET: leap to target's current position targetX = target.posX; @@ -119,9 +121,9 @@ public class AbilitySlam extends Ability { targetZ = target.posZ; } else { // Fallback: slam in place - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } hasLaunched = false; @@ -129,31 +131,33 @@ public class AbilitySlam extends Ability { airTicks = 0; // Reset fall distance to prevent fall damage during slam - npc.fallDistance = 0; + caster.fallDistance = 0; // Calculate and apply leap velocity - launchTowardTarget(npc); + launchTowardTarget(caster); } /** - * Calculate ballistic arc and launch NPC toward target. + * Calculate ballistic arc and launch caster toward target. * Accounts for Minecraft's drag physics to actually reach the destination. */ - private void launchTowardTarget(EntityNPCInterface npc) { - double dx = targetX - npc.posX; - double dy = targetY - npc.posY; - double dz = targetZ - npc.posZ; + private void launchTowardTarget(EntityLivingBase caster) { + double dx = targetX - caster.posX; + double dy = targetY - caster.posY; + double dz = targetZ - caster.posZ; double horizontalDist = Math.sqrt(dx * dx + dz * dz); // For AOE_SELF or very close targets, just hop in place if (horizontalDist < 0.5) { - npc.motionX = 0; - npc.motionZ = 0; - npc.motionY = 0.8 * leapSpeed; + caster.motionX = 0; + caster.motionZ = 0; + caster.motionY = 0.8 * leapSpeed; hasLaunched = true; - npc.setNpcJumpingState(true); - npc.velocityChanged = true; - npc.worldObj.playSoundAtEntity(npc, "mob.irongolem.throw", 0.8f, 0.8f); + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(true); + } + caster.velocityChanged = true; + caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); return; } @@ -163,8 +167,8 @@ public class AbilitySlam extends Ability { dx *= scale; dz *= scale; horizontalDist = maxRange; - targetX = npc.posX + dx; - targetZ = npc.posZ + dz; + targetX = caster.posX + dx; + targetZ = caster.posZ + dz; } // Choose flight time based on distance - shorter distances = faster @@ -210,90 +214,100 @@ public class AbilitySlam extends Ability { double dirX = dx / horizontalDist; double dirZ = dz / horizontalDist; - npc.motionX = dirX * vHorizontal; - npc.motionZ = dirZ * vHorizontal; - npc.motionY = vy; + caster.motionX = dirX * vHorizontal; + caster.motionZ = dirZ * vHorizontal; + caster.motionY = vy; hasLaunched = true; - npc.setNpcJumpingState(true); - npc.velocityChanged = true; + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(true); + } + caster.velocityChanged = true; // Face the target float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); - npc.rotationYaw = targetYaw; - npc.rotationYawHead = targetYaw; + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (!hasLaunched) return; if (hasLanded) return; airTicks++; // Continuously reset fall distance during slam to prevent fall damage - npc.fallDistance = 0; + caster.fallDistance = 0; // Check for landing - if (npc.onGround && airTicks > 3) { + if (caster.onGround && airTicks > 3) { // Landed! - onLanding(npc, world); + onLanding(caster, world); return; } // Timeout protection - force landing after max air time if (airTicks >= maxAirTicks) { - onLanding(npc, world); + onLanding(caster, world); return; } // While in air, face toward target - double dx = targetX - npc.posX; - double dz = targetZ - npc.posZ; + double dx = targetX - caster.posX; + double dz = targetZ - caster.posZ; float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); - npc.rotationYaw = targetYaw; - npc.rotationYawHead = targetYaw; + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; } /** - * Called when NPC lands - deal AOE damage. + * Called when caster lands - deal AOE damage and signal completion. */ - private void onLanding(EntityNPCInterface npc, World world) { + private void onLanding(EntityLivingBase caster, World world) { hasLanded = true; - npc.setNpcJumpingState(false); + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } + + // Signal that the ability has completed its active phase + signalCompletion(); // Reset fall distance to prevent fall damage on landing - npc.fallDistance = 0; + caster.fallDistance = 0; // Stop horizontal momentum - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; if (world.isRemote) return; + // Play slam impact sound on landing + world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f); + // Find all entities in radius @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABBExcludingEntity( - npc, npc.boundingBox.expand(radius, radius / 2, radius)); + caster, caster.boundingBox.expand(radius, radius / 2, radius)); for (Entity entity : entities) { - if (entity instanceof EntityLivingBase && entity != npc) { + if (entity instanceof EntityLivingBase && entity != caster) { EntityLivingBase livingTarget = (EntityLivingBase) entity; // Check if actually within radius (bounding box is a cube, we want a circle) - double dx = livingTarget.posX - npc.posX; - double dz = livingTarget.posZ - npc.posZ; + double dx = livingTarget.posX - caster.posX; + double dz = livingTarget.posZ - caster.posZ; if (dx * dx + dz * dz <= radius * radius) { // Apply damage with scripted event support - applyAbilityDamage(npc, livingTarget, damage, knockbackStrength); + applyAbilityDamage(caster, livingTarget, damage, knockbackStrength); } } } // Spawn particles - spawnSlamParticles(world, npc.posX, npc.posY, npc.posZ); + spawnSlamParticles(world, caster.posX, caster.posY, caster.posZ); } /** @@ -317,7 +331,7 @@ public class AbilitySlam extends Ability { } // Ground crack particles - check if block is valid - net.minecraft.block.Block groundBlock = world.getBlock((int)x, (int)y - 1, (int)z); + net.minecraft.block.Block groundBlock = world.getBlock((int) x, (int) y - 1, (int) z); if (groundBlock != null && groundBlock.getMaterial().isSolid()) { int blockId = net.minecraft.block.Block.getIdFromBlock(groundBlock); if (blockId > 0) { @@ -333,16 +347,20 @@ public class AbilitySlam extends Ability { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - npc.setNpcJumpingState(false); + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } hasLaunched = false; hasLanded = false; airTicks = 0; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - npc.setNpcJumpingState(false); + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } hasLaunched = false; hasLanded = false; airTicks = 0; @@ -356,8 +374,102 @@ public class AbilitySlam extends Ability { airTicks = 0; } + // ==================== PREVIEW MODE ==================== + + private transient double previewVelX, previewVelY, previewVelZ; + private transient boolean previewLaunched = false; + private transient double previewGroundY; + @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + previewLaunched = false; + + double tx, ty, tz; + if (previewTarget != null && targetingMode == TargetingMode.AOE_TARGET) { + tx = previewTarget.posX; + ty = previewTarget.posY; + tz = previewTarget.posZ; + } else { + tx = npc.posX; + ty = npc.posY; + tz = npc.posZ; + } + + previewGroundY = npc.posY; + + double dx = tx - npc.posX; + double dz = tz - npc.posZ; + double horizontalDist = Math.sqrt(dx * dx + dz * dz); + + if (horizontalDist < 0.5) { + previewVelX = 0; + previewVelZ = 0; + previewVelY = 0.8 * leapSpeed; + } else { + if (horizontalDist > maxRange) { + double scale = maxRange / horizontalDist; + dx *= scale; + dz *= scale; + horizontalDist = maxRange; + } + + int flightTicks = (int) Math.max(15, Math.min(horizontalDist * 1.5, 30)); + double drag = 0.91; + double dragPowN = Math.pow(drag, flightTicks); + double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN) * leapSpeed; + + double arcHeight = Math.max(1.0, leapHeight) * leapSpeed; + double peakTicks = flightTicks * 0.4; + double vy = (arcHeight * 2.0 / peakTicks) + (0.08 * peakTicks * 0.5); + vy = Math.max(vy, 0.6 * leapSpeed); + + double dirX = dx / horizontalDist; + double dirZ = dz / horizontalDist; + previewVelX = dirX * vHorizontal; + previewVelZ = dirZ * vHorizontal; + previewVelY = vy; + } + previewLaunched = true; + } + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + if (!previewLaunched) return; + + // Apply velocity + npc.prevPosX = npc.posX; + npc.prevPosY = npc.posY; + npc.prevPosZ = npc.posZ; + + npc.posX += previewVelX; + npc.posY += previewVelY; + npc.posZ += previewVelZ; + + // Gravity and drag + previewVelY -= 0.08; + previewVelY *= 0.98; + previewVelX *= 0.91; + previewVelZ *= 0.91; + + // Ground clamp - stop falling below starting Y + if (npc.posY < previewGroundY && previewVelY < 0) { + npc.posY = previewGroundY; + previewVelY = 0; + previewVelX = 0; + previewVelZ = 0; + previewLaunched = false; + } + } + + @Override + public int getPreviewActiveDuration() { + return 60; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { // Check if telegraph should be shown if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { return null; @@ -365,24 +477,24 @@ public class AbilitySlam extends Ability { // Telegraph shows at landing zone if (targetingMode == TargetingMode.AOE_SELF) { - // AOE_SELF: telegraph at NPC position, does NOT follow - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + // AOE_SELF: telegraph at caster position, does NOT follow + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null) { // AOE_TARGET: telegraph at target position, follows target during windup targetX = target.posX; targetY = target.posY; targetZ = target.posZ; } else { - // Fallback to NPC position - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + // Fallback to caster position + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } // Create telegraph at the appropriate position - double groundY = findGroundLevel(npc.worldObj, targetX, targetY, targetZ); + double groundY = findGroundLevel(caster.worldObj, targetX, targetY, targetZ); Telegraph telegraph = Telegraph.circle(radius); telegraph.setDurationTicks(windUpTicks); @@ -391,14 +503,14 @@ public class AbilitySlam extends Ability { telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); - TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); + TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); // AOE_TARGET: telegraph follows target during windup if (targetingMode == TargetingMode.AOE_TARGET && target != null) { instance.setEntityIdToFollow(target.getEntityId()); } - // AOE_SELF: telegraph stays at NPC position, no follow + // AOE_SELF: telegraph stays at caster position, no follow return instance; } @@ -419,7 +531,7 @@ public class AbilitySlam extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { - damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 20.0f; + damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 5.0f; knockbackStrength = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f; leapSpeed = nbt.hasKey("leapSpeed") ? nbt.getFloat("leapSpeed") : 1.0f; @@ -427,18 +539,56 @@ public class AbilitySlam extends Ability { } // Getters & Setters - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getDamage() { + return damage; + } - public float getRadius() { return radius; } - public void setRadius(float radius) { this.radius = radius; } + public void setDamage(float damage) { + this.damage = damage; + } - public float getKnockbackStrength() { return knockbackStrength; } - public void setKnockbackStrength(float knockbackStrength) { this.knockbackStrength = knockbackStrength; } + public float getRadius() { + return radius; + } - public float getLeapSpeed() { return leapSpeed; } - public void setLeapSpeed(float leapSpeed) { this.leapSpeed = leapSpeed; } + public void setRadius(float radius) { + this.radius = radius; + } - public float getLeapHeight() { return leapHeight; } - public void setLeapHeight(float leapHeight) { this.leapHeight = leapHeight; } + public float getKnockbackStrength() { + return knockbackStrength; + } + + public void setKnockbackStrength(float knockbackStrength) { + this.knockbackStrength = knockbackStrength; + } + + public float getLeapSpeed() { + return leapSpeed; + } + + public void setLeapSpeed(float leapSpeed) { + this.leapSpeed = leapSpeed; + } + + public float getLeapHeight() { + return leapHeight; + } + + public void setLeapHeight(float leapHeight) { + this.leapHeight = leapHeight; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("gui.radius", this::getRadius, this::setRadius).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockbackStrength, this::setKnockbackStrength).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.leap"), + FieldDef.floatField("gui.speed", this::getLeapSpeed, this::setLeapSpeed).column(ColumnHint.LEFT), + FieldDef.floatField("gui.height", this::getLeapHeight, this::setLeapHeight).column(ColumnHint.RIGHT), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java new file mode 100644 index 000000000..671305c10 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -0,0 +1,231 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilitySweeper; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.world.World; +import noppes.npcs.LogWriter; +import noppes.npcs.api.ability.type.IAbilitySweeper; + +import java.util.Arrays; +import java.util.List; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Sweeper ability: A low sweeping beam that rotates around the NPC. + * Players can jump over it. The beam completes a set number of rotations before ending. + * Telegraph shows a large circle on the ground indicating range. + * + * All damage logic is handled by the EntityAbilitySweeper entity to ensure + * visual and damage hitbox alignment. + */ +public class AbilitySweeper extends Ability implements IAbilitySweeper { + + // Type-specific parameters + private float beamLength = 10.0f; + private float beamWidth = 0.3f; // Thin like beam trail + private float beamHeight = 0.5f; // Height above ground (low enough to jump over) + private float damage = 5.0f; + private int damageInterval = 5; + private boolean piercing = true; + private float sweepSpeed = 3.0f; + private int numberOfRotations = 2; // How many full rotations before ability ends + private boolean lockOnTarget = false; + + // Visual properties + private EnergyColorData colorData = new EnergyColorData(0xFF6600, 0xFF0000, true, 1.8f, 0.5f, 0.0f); + + // Runtime state (transient) + private transient EntityAbilitySweeper activeEntity = null; + + public AbilitySweeper() { + this.typeId = "ability.cnpc.sweeper"; + this.name = "Sweeper"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 15.0f; + this.minRange = 0.0f; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.cooldownTicks = 0; + this.windUpTicks = 60; + // Circle telegraph to show range + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Spawn the entity that handles BOTH visuals AND damage + activeEntity = new EntityAbilitySweeper(world, caster, target, + beamLength, beamWidth, beamHeight, + colorData, + sweepSpeed, numberOfRotations, + damage, damageInterval, piercing, + lockOnTarget); + world.spawnEntityInWorld(activeEntity); + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (activeEntity == null || activeEntity.isDead) { + activeEntity = null; + signalCompletion(); + } + } + + @Override + public void cleanup() { + // Clean up entity if still alive + if (activeEntity != null && !activeEntity.isDead) { + activeEntity.setDead(); + } + activeEntity = null; + } + + @Override + public float getTelegraphRadius() { + return beamLength; + } + + /** + * Override to position telegraph at NPC (not target) since the sweep is centered on NPC. + */ + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE) { + return null; + } + + // Create circle telegraph centered on caster + Telegraph telegraph = Telegraph.circle(beamLength); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at caster, not target + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, caster.posY, caster.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(caster.getEntityId()); // Follow caster + + return instance; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("beamLength", beamLength); + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("beamHeight", beamHeight); + nbt.setFloat("damage", damage); + nbt.setInteger("damageInterval", damageInterval); + nbt.setBoolean("piercing", piercing); + nbt.setFloat("sweepSpeed", sweepSpeed); + nbt.setInteger("numberOfRotations", numberOfRotations); + nbt.setBoolean("lockOnTarget", lockOnTarget); + colorData.writeNBT(nbt); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.beamLength = nbt.hasKey("beamLength") ? nbt.getFloat("beamLength") : 10.0f; + this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.3f; + this.beamHeight = nbt.hasKey("beamHeight") ? nbt.getFloat("beamHeight") : 0.5f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 5.0f; + this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 5; + this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); + this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 3.0f; + this.numberOfRotations = nbt.hasKey("numberOfRotations") ? nbt.getInteger("numberOfRotations") : 2; + this.lockOnTarget = nbt.hasKey("lockOnTarget") && nbt.getBoolean("lockOnTarget"); + colorData.readNBT(nbt); + } + + // Getters & Setters + public float getBeamLength() { return beamLength; } + public void setBeamLength(float beamLength) { this.beamLength = beamLength; } + public float getBeamWidth() { return beamWidth; } + public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public float getBeamHeight() { return beamHeight; } + public void setBeamHeight(float beamHeight) { this.beamHeight = beamHeight; } + public float getDamage() { return damage; } + public void setDamage(float damage) { this.damage = damage; } + public int getDamageInterval() { return damageInterval; } + public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; } + public boolean isPiercing() { return piercing; } + public void setPiercing(boolean piercing) { this.piercing = piercing; } + public float getSweepSpeed() { return sweepSpeed; } + public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; } + public int getNumberOfRotations() { return numberOfRotations; } + public void setNumberOfRotations(int numberOfRotations) { this.numberOfRotations = numberOfRotations; } + public boolean isLockOnTarget() { return lockOnTarget; } + public void setLockOnTarget(boolean lockOnTarget) { this.lockOnTarget = lockOnTarget; } + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { colorData.outerColor = outerColor; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.beam"), + FieldDef.floatField("gui.length", this::getBeamLength, this::setBeamLength).column(ColumnHint.LEFT), + FieldDef.floatField("gui.width", this::getBeamWidth, this::setBeamWidth).column(ColumnHint.RIGHT), + FieldDef.floatField("gui.height", this::getBeamHeight, this::setBeamHeight).column(ColumnHint.LEFT), + FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed).column(ColumnHint.RIGHT), + FieldDef.intField("ability.rotations", this::getNumberOfRotations, this::setNumberOfRotations), + FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing) + .hover("ability.hover.piercing").column(ColumnHint.LEFT), + FieldDef.boolField("ability.lockTarget", this::isLockOnTarget, this::setLockOnTarget) + .hover("ability.hover.lockTarget").column(ColumnHint.RIGHT), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.section("ability.section.colors").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled) + ); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 72971c572..8f37c5330 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -1,32 +1,33 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTeleport; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.block.Block; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.util.MathHelper; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityTeleport; + +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; import java.util.List; import java.util.Random; /** * Teleport ability: Instant reposition with wall/line-of-sight checks. */ -public class AbilityTeleport extends Ability { +public class AbilityTeleport extends Ability implements IAbilityTeleport { /** * Teleport behavior mode. @@ -34,7 +35,17 @@ public class AbilityTeleport extends Ability { public enum TeleportMode { BLINK, BEHIND, - SINGLE + SINGLE; + + @Override + public String toString() { + switch (this) { + case BLINK: return "ability.teleport.blink"; + case BEHIND: return "ability.teleport.behind"; + case SINGLE: return "ability.teleport.single"; + default: return name(); + } + } } private static final Random RANDOM = new Random(); @@ -61,10 +72,9 @@ public class AbilityTeleport extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 30.0f; this.minRange = 5.0f; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.windUpTicks = 10; - this.activeTicks = 40; - this.recoveryTicks = 10; + this.lockMovement = LockMovementType.NO; // No telegraph for teleport - it's instant repositioning this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -73,57 +83,62 @@ public class AbilityTeleport extends Ability { } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityTeleport(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { currentBlink = 0; ticksSinceLastBlink = blinkDelayTicks; // Trigger first blink immediately } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1; - if (currentBlink >= blinkLimit) return; + if (currentBlink >= blinkLimit) { + signalCompletion(); + return; + } ticksSinceLastBlink++; if (ticksSinceLastBlink >= blinkDelayTicks) { - performBlink(npc, target, world); + performBlink(caster, target, world); currentBlink++; ticksSinceLastBlink = 0; + + // Check if this was the last blink + if (currentBlink >= blinkLimit) { + signalCompletion(); + } } } - private void performBlink(EntityNPCInterface npc, EntityLivingBase target, World world) { + private void performBlink(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) return; - double oldX = npc.posX; - double oldY = npc.posY; - double oldZ = npc.posZ; + double oldX = caster.posX; + double oldY = caster.posY; + double oldZ = caster.posZ; - Vec3 destination = calculateDestination(npc, target, world); + Vec3 destination = calculateDestination(caster, target, world); if (destination == null) return; boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight; - if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + npc.getEyeHeight(), oldZ, - destination.xCoord, destination.yCoord + npc.getEyeHeight(), destination.zCoord)) { - destination = findValidPositionAlongLine(world, npc, oldX, oldY, oldZ, + if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ, + destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) { + destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ, destination.xCoord, destination.yCoord, destination.zCoord); if (destination == null) return; } @@ -134,22 +149,22 @@ public class AbilityTeleport extends Ability { // Damage at origin if (damageAtStart) { - dealDamageAt(npc, world, oldX, oldY, oldZ); + dealDamageAt(caster, world, oldX, oldY, oldZ); } // Spawn particles at origin spawnTeleportParticles(world, oldX, oldY, oldZ); // Teleport - npc.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); - npc.fallDistance = 0; + caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); + caster.fallDistance = 0; // Spawn particles at destination spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord); // Damage at destination if (damageAtEnd) { - dealDamageAt(npc, world, destination.xCoord, destination.yCoord, destination.zCoord); + dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord); } // Face target after teleport @@ -157,8 +172,8 @@ public class AbilityTeleport extends Ability { double dx = target.posX - destination.xCoord; double dz = target.posZ - destination.zCoord; float newYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); - npc.rotationYaw = newYaw; - npc.rotationYawHead = newYaw; + caster.rotationYaw = newYaw; + caster.rotationYawHead = newYaw; } } @@ -174,35 +189,56 @@ public class AbilityTeleport extends Ability { } } - private Vec3 calculateDestination(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (target == null) return null; - + private Vec3 calculateDestination(EntityLivingBase caster, EntityLivingBase target, World world) { double newX; double newY; double newZ; + if (target != null) { + switch (mode) { + case BEHIND: + double yaw = Math.toRadians(target.rotationYaw); + newX = target.posX + Math.sin(yaw) * behindDistance; + newZ = target.posZ - Math.cos(yaw) * behindDistance; + newY = target.posY; + return Vec3.createVectorHelper(newX, newY, newZ); + case SINGLE: + case BLINK: + default: + double angle = RANDOM.nextDouble() * Math.PI * 2; + double dist = Math.min(blinkRadius, maxRange); + double offset = RANDOM.nextDouble() * dist; + newX = target.posX + Math.cos(angle) * offset; + newZ = target.posZ + Math.sin(angle) * offset; + newY = target.posY; + return Vec3.createVectorHelper(newX, newY, newZ); + } + } + + // No target (player use) - teleport in caster's look direction + Vec3 look = caster.getLookVec(); switch (mode) { case BEHIND: - double yaw = Math.toRadians(target.rotationYaw); - newX = target.posX + Math.sin(yaw) * behindDistance; - newZ = target.posZ - Math.cos(yaw) * behindDistance; - newY = target.posY; + // Teleport forward in look direction by behindDistance + newX = caster.posX + look.xCoord * behindDistance; + newZ = caster.posZ + look.zCoord * behindDistance; + newY = caster.posY; return Vec3.createVectorHelper(newX, newY, newZ); case SINGLE: case BLINK: default: - double angle = RANDOM.nextDouble() * Math.PI * 2; + // Teleport forward in look direction within blinkRadius double dist = Math.min(blinkRadius, maxRange); - double offset = RANDOM.nextDouble() * dist; - newX = target.posX + Math.cos(angle) * offset; - newZ = target.posZ + Math.sin(angle) * offset; - newY = target.posY; + double offset = (RANDOM.nextDouble() * 0.5 + 0.5) * dist; // 50-100% of max distance + newX = caster.posX + look.xCoord * offset; + newZ = caster.posZ + look.zCoord * offset; + newY = caster.posY; return Vec3.createVectorHelper(newX, newY, newZ); } } private Vec3 findSafeDestination(World world, double oldX, double oldY, double oldZ, - double destX, double destY, double destZ) { + double destX, double destY, double destZ) { int oldBlockX = (int) Math.floor(oldX); int oldBlockY = (int) Math.floor(oldY); int oldBlockZ = (int) Math.floor(oldZ); @@ -259,8 +295,13 @@ public class AbilityTeleport extends Ability { int blockY = MathHelper.floor_double(checkY); int blockZ = MathHelper.floor_double(checkZ); + // Skip out-of-world coordinates + if (blockY < 0 || blockY >= 256) { + continue; + } + Block block = world.getBlock(blockX, blockY, blockZ); - if (block.getMaterial().isSolid() && block.isOpaqueCube()) { + if (block != null && block.getMaterial().isSolid() && block.isOpaqueCube()) { return false; } } @@ -268,7 +309,7 @@ public class AbilityTeleport extends Ability { return true; } - private Vec3 findValidPositionAlongLine(World world, EntityNPCInterface npc, + private Vec3 findValidPositionAlongLine(World world, EntityLivingBase caster, double x1, double y1, double z1, double x2, double y2, double z2) { double dx = x2 - x1; @@ -290,8 +331,8 @@ public class AbilityTeleport extends Ability { double checkY = y1 + dy * d; double checkZ = z1 + dz * d; - if (hasLineOfSight(world, x1, y1 + npc.getEyeHeight(), z1, - checkX, checkY + npc.getEyeHeight(), checkZ)) { + if (hasLineOfSight(world, x1, y1 + caster.getEyeHeight(), z1, + checkX, checkY + caster.getEyeHeight(), checkZ)) { int blockX = MathHelper.floor_double(checkX); int blockY = MathHelper.floor_double(checkY); int blockZ = MathHelper.floor_double(checkZ); @@ -325,16 +366,26 @@ public class AbilityTeleport extends Ability { } private boolean isSafeLocation(World world, int x, int y, int z) { + // Bounds check - ensure we're within valid world coordinates + if (y < 1 || y >= 255) { + return false; + } + Block groundBlock = world.getBlock(x, y - 1, z); Block feetBlock = world.getBlock(x, y, z); Block headBlock = world.getBlock(x, y + 1, z); + // Defensive null checks (shouldn't happen in MC 1.7.10, but safe) + if (groundBlock == null || feetBlock == null || headBlock == null) { + return false; + } + return groundBlock.getMaterial().isSolid() && - !feetBlock.getMaterial().isSolid() && - !headBlock.getMaterial().isSolid(); + !feetBlock.getMaterial().isSolid() && + !headBlock.getMaterial().isSolid(); } - private void dealDamageAt(EntityNPCInterface npc, World world, double x, double y, double z) { + private void dealDamageAt(EntityLivingBase caster, World world, double x, double y, double z) { AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox( x - damageRadius, y - 1, z - damageRadius, x + damageRadius, y + 2, z + damageRadius @@ -345,13 +396,16 @@ public class AbilityTeleport extends Ability { for (Entity entity : entities) { if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; + if (entity == caster) continue; EntityLivingBase living = (EntityLivingBase) entity; double dist = Math.sqrt(Math.pow(living.posX - x, 2) + Math.pow(living.posZ - z, 2)); if (dist <= damageRadius) { // Apply damage with scripted event support (no knockback for teleport damage) - applyAbilityDamage(npc, living, damage, 0); + boolean wasHit = applyAbilityDamage(caster, living, damage, 0); + if (wasHit) { + applyEffects(living); + } } } } @@ -414,33 +468,120 @@ public class AbilityTeleport extends Ability { } // Getters & Setters - public TeleportMode getMode() { return mode; } - public void setMode(TeleportMode mode) { this.mode = mode; } + public TeleportMode getModeEnum() { + return mode; + } - public int getBlinkCount() { return blinkCount; } - public void setBlinkCount(int blinkCount) { this.blinkCount = blinkCount; } + public void setModeEnum(TeleportMode mode) { + this.mode = mode; + } - public int getBlinkDelayTicks() { return blinkDelayTicks; } - public void setBlinkDelayTicks(int blinkDelayTicks) { this.blinkDelayTicks = blinkDelayTicks; } + @Override + public int getMode() { + return mode.ordinal(); + } - public float getBlinkRadius() { return blinkRadius; } - public void setBlinkRadius(float blinkRadius) { this.blinkRadius = blinkRadius; } + @Override + public void setMode(int mode) { + TeleportMode[] values = TeleportMode.values(); + this.mode = mode >= 0 && mode < values.length ? values[mode] : TeleportMode.BLINK; + } - public float getBehindDistance() { return behindDistance; } - public void setBehindDistance(float behindDistance) { this.behindDistance = behindDistance; } + public int getBlinkCount() { + return blinkCount; + } - public boolean isRequireLineOfSight() { return requireLineOfSight; } - public void setRequireLineOfSight(boolean requireLineOfSight) { this.requireLineOfSight = requireLineOfSight; } + public void setBlinkCount(int blinkCount) { + this.blinkCount = blinkCount; + } - public boolean isDamageAtStart() { return damageAtStart; } - public void setDamageAtStart(boolean damageAtStart) { this.damageAtStart = damageAtStart; } + public int getBlinkDelayTicks() { + return blinkDelayTicks; + } - public boolean isDamageAtEnd() { return damageAtEnd; } - public void setDamageAtEnd(boolean damageAtEnd) { this.damageAtEnd = damageAtEnd; } + public void setBlinkDelayTicks(int blinkDelayTicks) { + this.blinkDelayTicks = blinkDelayTicks; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getBlinkRadius() { + return blinkRadius; + } - public float getDamageRadius() { return damageRadius; } - public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; } + public void setBlinkRadius(float blinkRadius) { + this.blinkRadius = blinkRadius; + } + + public float getBehindDistance() { + return behindDistance; + } + + public void setBehindDistance(float behindDistance) { + this.behindDistance = behindDistance; + } + + public boolean isRequireLineOfSight() { + return requireLineOfSight; + } + + public void setRequireLineOfSight(boolean requireLineOfSight) { + this.requireLineOfSight = requireLineOfSight; + } + + public boolean isDamageAtStart() { + return damageAtStart; + } + + public void setDamageAtStart(boolean damageAtStart) { + this.damageAtStart = damageAtStart; + } + + public boolean isDamageAtEnd() { + return damageAtEnd; + } + + public void setDamageAtEnd(boolean damageAtEnd) { + this.damageAtEnd = damageAtEnd; + } + + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + public float getDamageRadius() { + return damageRadius; + } + + public void setDamageRadius(float damageRadius) { + this.damageRadius = damageRadius; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.enumField("ability.mode", TeleportMode.class, this::getModeEnum, this::setModeEnum), + FieldDef.floatField("ability.blinkRadius", this::getBlinkRadius, this::setBlinkRadius) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE), + FieldDef.intField("ability.blinkCount", this::getBlinkCount, this::setBlinkCount) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK).column(ColumnHint.LEFT), + FieldDef.intField("ability.blinkDelay", this::getBlinkDelayTicks, this::setBlinkDelayTicks) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.behindDistance", this::getBehindDistance, this::setBehindDistance) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BEHIND), + FieldDef.boolField("ability.lineOfSight", this::isRequireLineOfSight, this::setRequireLineOfSight) + .hover("ability.hover.lineOfSight") + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE), + FieldDef.section("ability.section.damage"), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getDamageRadius, this::setDamageRadius).column(ColumnHint.RIGHT), + FieldDef.boolField("ability.damageAtStart", this::isDamageAtStart, this::setDamageAtStart) + .hover("ability.hover.dmgAtStart").column(ColumnHint.LEFT), + FieldDef.boolField("ability.damageAtEnd", this::isDamageAtEnd, this::setDamageAtEnd) + .hover("ability.hover.dmgAtEnd").column(ColumnHint.RIGHT), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 9d1090da6..46f20e91b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -1,23 +1,24 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTrap; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityTrap; + +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Random; @@ -28,29 +29,34 @@ import java.util.UUID; * Trap ability: Places a proximity-triggered trap. * Features arm time, trigger radius, and various effects on trigger. */ -public class AbilityTrap extends Ability { +public class AbilityTrap extends Ability implements IAbilityTrap { public enum TrapPlacement { AT_CASTER, AT_TARGET, - AHEAD_OF_CASTER + AHEAD_OF_CASTER; + + @Override + public String toString() { + switch (this) { + case AT_CASTER: return "ability.trapPlace.atCaster"; + case AT_TARGET: return "ability.trapPlace.atTarget"; + case AHEAD_OF_CASTER: return "ability.trapPlace.aheadOfCaster"; + default: return name(); + } + } } + private int durationTicks = 200; private TrapPlacement placement = TrapPlacement.AT_TARGET; private float placementDistance = 5.0f; private float triggerRadius = 2.0f; private int armTime = 20; private int maxTriggers = 1; private int triggerCooldown = 20; - private float damage = 10.0f; + private float damage = 6.0f; private float damageRadius = 0.0f; private float knockback = 0.5f; - private int stunDuration = 0; - private int rootDuration = 40; - private int slowDuration = 0; - private int slowLevel = 1; - private int poisonDuration = 0; - private int poisonLevel = 0; private boolean visible = true; // Offset parameters - trap spawns near target, not exactly on them @@ -71,37 +77,35 @@ public class AbilityTrap extends Ability { this.name = "Trap"; this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 15.0f; - this.lockMovement = true; - this.cooldownTicks = 150; + this.lockMovement = LockMovementType.WINDUP; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 200; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityTrap(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override - public float getTelegraphRadius() { return triggerRadius; } + public float getTelegraphRadius() { + return triggerRadius; + } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { - TelegraphInstance instance = super.createTelegraph(npc, target); + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + TelegraphInstance instance = super.createTelegraph(caster, target); if (instance == null) return null; // Control telegraph following based on placement mode @@ -111,14 +115,14 @@ public class AbilityTrap extends Ability { // Telegraph at caster or ahead, no following instance.setEntityIdToFollow(-1); if (placement == TrapPlacement.AHEAD_OF_CASTER) { - double yaw = Math.toRadians(npc.rotationYaw); - instance.setX(npc.posX - Math.sin(yaw) * placementDistance); - instance.setY(npc.posY); - instance.setZ(npc.posZ + Math.cos(yaw) * placementDistance); + double yaw = Math.toRadians(caster.rotationYaw); + instance.setX(caster.posX - Math.sin(yaw) * placementDistance); + instance.setY(caster.posY); + instance.setZ(caster.posZ + Math.cos(yaw) * placementDistance); } else { - instance.setX(npc.posX); - instance.setY(npc.posY); - instance.setZ(npc.posZ); + instance.setX(caster.posX); + instance.setY(caster.posY); + instance.setZ(caster.posZ); } break; case AT_TARGET: @@ -131,33 +135,8 @@ public class AbilityTrap extends Ability { return instance; } - /** - * Calculates offset position near the given coordinates. - */ - private double[] calculateOffsetPosition(double baseX, double baseY, double baseZ) { - if (maxOffset <= 0) { - return new double[]{baseX, baseY, baseZ}; - } - - double offsetDist; - double offsetAngle; - - if (randomOffset) { - offsetDist = minOffset + RANDOM.nextDouble() * (maxOffset - minOffset); - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } else { - offsetDist = maxOffset; - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } - - double offsetX = Math.cos(offsetAngle) * offsetDist; - double offsetZ = Math.sin(offsetAngle) * offsetDist; - - return new double[]{baseX + offsetX, baseY, baseZ + offsetZ}; - } - @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { armed = false; triggerCount = 0; ticksSinceLastTrigger = armTime; @@ -168,39 +147,51 @@ public class AbilityTrap extends Ability { switch (placement) { case AT_CASTER: - trapX = npc.posX; - trapY = npc.posY; - trapZ = npc.posZ; + trapX = caster.posX; + trapY = caster.posY; + trapZ = caster.posZ; break; case AT_TARGET: // Use telegraph position with offset if (telegraph != null) { - double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ()); + double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(), + minOffset, maxOffset, randomOffset, RANDOM); trapX = pos[0]; trapY = pos[1]; trapZ = pos[2]; + // Update telegraph to show actual trap position + telegraph.setX(trapX); + telegraph.setY(trapY); + telegraph.setZ(trapZ); } else if (target != null) { - double[] pos = calculateOffsetPosition(target.posX, target.posY, target.posZ); + double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ, + minOffset, maxOffset, randomOffset, RANDOM); trapX = pos[0]; trapY = pos[1]; trapZ = pos[2]; } else { - trapX = npc.posX; - trapY = npc.posY; - trapZ = npc.posZ; + trapX = caster.posX; + trapY = caster.posY; + trapZ = caster.posZ; } break; case AHEAD_OF_CASTER: - double yaw = Math.toRadians(npc.rotationYaw); - trapX = npc.posX - Math.sin(yaw) * placementDistance; - trapY = npc.posY; - trapZ = npc.posZ + Math.cos(yaw) * placementDistance; + double yaw = Math.toRadians(caster.rotationYaw); + trapX = caster.posX - Math.sin(yaw) * placementDistance; + trapY = caster.posY; + trapZ = caster.posZ + Math.cos(yaw) * placementDistance; break; } } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Check if trap duration has ended + if (tick >= durationTicks) { + signalCompletion(); + return; + } + if (!armed) { if (tick >= armTime) { armed = true; @@ -211,6 +202,7 @@ public class AbilityTrap extends Ability { ticksSinceLastTrigger++; if (maxTriggers > 0 && triggerCount >= maxTriggers) { + signalCompletion(); // All triggers used, trap is done return; } @@ -227,7 +219,7 @@ public class AbilityTrap extends Ability { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (entity.isDead) continue; if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue; @@ -236,13 +228,13 @@ public class AbilityTrap extends Ability { double dist = Math.sqrt(dx * dx + dz * dz); if (dist <= triggerRadius) { - triggerTrap(npc, entity, world); + triggerTrap(caster, entity, world); return; } } } - private void triggerTrap(EntityNPCInterface npc, EntityLivingBase triggerer, World world) { + private void triggerTrap(EntityLivingBase caster, EntityLivingBase triggerer, World world) { triggerCount++; ticksSinceLastTrigger = 0; triggeredEntities.add(triggerer.getUniqueID()); @@ -259,7 +251,7 @@ public class AbilityTrap extends Ability { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; double dx = entity.posX - trapX; double dz = entity.posZ - trapZ; double dist = Math.sqrt(dx * dx + dz * dz); @@ -273,39 +265,24 @@ public class AbilityTrap extends Ability { for (EntityLivingBase entity : affected) { // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback); + boolean wasHit = applyAbilityDamage(caster, entity, damage, knockback); - // Only apply effects if the hit wasn't cancelled + // Apply effects if the hit wasn't cancelled if (wasHit) { - if (stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - - if (rootDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, rootDuration, 127)); - } - - if (slowDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, slowDuration, slowLevel)); - } - - if (poisonDuration > 0 && poisonLevel >= 0) { - entity.addPotionEffect(new PotionEffect(Potion.poison.id, poisonDuration, poisonLevel)); - } + applyEffects(entity); } } } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { armed = false; triggerCount = 0; triggeredEntities.clear(); } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { armed = false; triggerCount = 0; triggeredEntities.clear(); @@ -313,6 +290,7 @@ public class AbilityTrap extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setString("placement", placement.name()); nbt.setFloat("placementDistance", placementDistance); nbt.setFloat("triggerRadius", triggerRadius); @@ -322,12 +300,6 @@ public class AbilityTrap extends Ability { nbt.setFloat("damage", damage); nbt.setFloat("damageRadius", damageRadius); nbt.setFloat("knockback", knockback); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("rootDuration", rootDuration); - nbt.setInteger("slowDuration", slowDuration); - nbt.setInteger("slowLevel", slowLevel); - nbt.setInteger("poisonDuration", poisonDuration); - nbt.setInteger("poisonLevel", poisonLevel); nbt.setBoolean("visible", visible); nbt.setFloat("minOffset", minOffset); nbt.setFloat("maxOffset", maxOffset); @@ -336,6 +308,7 @@ public class AbilityTrap extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 200; try { this.placement = TrapPlacement.valueOf(nbt.getString("placement")); } catch (Exception e) { @@ -346,15 +319,9 @@ public class AbilityTrap extends Ability { this.armTime = nbt.hasKey("armTime") ? nbt.getInteger("armTime") : 20; this.maxTriggers = nbt.hasKey("maxTriggers") ? nbt.getInteger("maxTriggers") : 1; this.triggerCooldown = nbt.hasKey("triggerCooldown") ? nbt.getInteger("triggerCooldown") : 20; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 0.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.rootDuration = nbt.hasKey("rootDuration") ? nbt.getInteger("rootDuration") : 40; - this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; - this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 1; - this.poisonDuration = nbt.hasKey("poisonDuration") ? nbt.getInteger("poisonDuration") : 0; - this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : 0; this.visible = !nbt.hasKey("visible") || nbt.getBoolean("visible"); this.minOffset = nbt.hasKey("minOffset") ? nbt.getFloat("minOffset") : 0.0f; this.maxOffset = nbt.hasKey("maxOffset") ? nbt.getFloat("maxOffset") : 1.5f; @@ -362,66 +329,163 @@ public class AbilityTrap extends Ability { } // Getters & Setters - public TrapPlacement getPlacement() { return placement; } - public void setPlacement(TrapPlacement placement) { this.placement = placement; } + public int getDurationTicks() { + return durationTicks; + } - public float getPlacementDistance() { return placementDistance; } - public void setPlacementDistance(float placementDistance) { this.placementDistance = placementDistance; } + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } - public float getTriggerRadius() { return triggerRadius; } - public void setTriggerRadius(float triggerRadius) { this.triggerRadius = triggerRadius; } + public TrapPlacement getPlacementEnum() { + return placement; + } - public int getArmTime() { return armTime; } - public void setArmTime(int armTime) { this.armTime = armTime; } + public void setPlacementEnum(TrapPlacement placement) { + this.placement = placement; + } - public int getMaxTriggers() { return maxTriggers; } - public void setMaxTriggers(int maxTriggers) { this.maxTriggers = maxTriggers; } + @Override + public int getPlacement() { + return placement.ordinal(); + } - public int getTriggerCooldown() { return triggerCooldown; } - public void setTriggerCooldown(int triggerCooldown) { this.triggerCooldown = triggerCooldown; } + @Override + public void setPlacement(int placement) { + TrapPlacement[] values = TrapPlacement.values(); + this.placement = placement >= 0 && placement < values.length ? values[placement] : TrapPlacement.AT_TARGET; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getPlacementDistance() { + return placementDistance; + } - public float getDamageRadius() { return damageRadius; } - public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; } + public void setPlacementDistance(float placementDistance) { + this.placementDistance = placementDistance; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public float getTriggerRadius() { + return triggerRadius; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public void setTriggerRadius(float triggerRadius) { + this.triggerRadius = triggerRadius; + } - public int getRootDuration() { return rootDuration; } - public void setRootDuration(int rootDuration) { this.rootDuration = rootDuration; } + public int getArmTime() { + return armTime; + } - public int getSlowDuration() { return slowDuration; } - public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } + public void setArmTime(int armTime) { + this.armTime = armTime; + } - public int getSlowLevel() { return slowLevel; } - public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } + public int getMaxTriggers() { + return maxTriggers; + } - public int getPoisonDuration() { return poisonDuration; } - public void setPoisonDuration(int poisonDuration) { this.poisonDuration = poisonDuration; } + public void setMaxTriggers(int maxTriggers) { + this.maxTriggers = maxTriggers; + } - public int getPoisonLevel() { return poisonLevel; } - public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; } + public int getTriggerCooldown() { + return triggerCooldown; + } - public boolean isVisible() { return visible; } - public void setVisible(boolean visible) { this.visible = visible; } + public void setTriggerCooldown(int triggerCooldown) { + this.triggerCooldown = triggerCooldown; + } - public float getMinOffset() { return minOffset; } - public void setMinOffset(float minOffset) { this.minOffset = minOffset; } + public float getDamage() { + return damage; + } - public float getMaxOffset() { return maxOffset; } - public void setMaxOffset(float maxOffset) { this.maxOffset = maxOffset; } + public void setDamage(float damage) { + this.damage = damage; + } - public boolean isRandomOffset() { return randomOffset; } - public void setRandomOffset(boolean randomOffset) { this.randomOffset = randomOffset; } + public float getDamageRadius() { + return damageRadius; + } + + public void setDamageRadius(float damageRadius) { + this.damageRadius = damageRadius; + } + + public float getKnockback() { + return knockback; + } + + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public float getMinOffset() { + return minOffset; + } + + public void setMinOffset(float minOffset) { + this.minOffset = minOffset; + } + + public float getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(float maxOffset) { + this.maxOffset = maxOffset; + } + + public boolean isRandomOffset() { + return randomOffset; + } + + public void setRandomOffset(boolean randomOffset) { + this.randomOffset = randomOffset; + } // Runtime getters - public double getTrapX() { return trapX; } - public double getTrapY() { return trapY; } - public double getTrapZ() { return trapZ; } - public boolean isArmed() { return armed; } + public double getTrapX() { + return trapX; + } + + public double getTrapY() { + return trapY; + } + + public double getTrapZ() { + return trapZ; + } + + public boolean isArmed() { + return armed; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) + .range(1, 2000), + FieldDef.enumField("ability.placement", TrapPlacement.class, this::getPlacementEnum, this::setPlacementEnum) + .hover("ability.hover.placement"), + FieldDef.section("ability.section.trigger"), + FieldDef.floatField("gui.radius", this::getTriggerRadius, this::setTriggerRadius).column(ColumnHint.LEFT), + FieldDef.intField("ability.armTime", this::getArmTime, this::setArmTime).column(ColumnHint.RIGHT), + FieldDef.intField("ability.maxTriggers", this::getMaxTriggers, this::setMaxTriggers).column(ColumnHint.LEFT), + FieldDef.intField("ability.triggerCooldown", this::getTriggerCooldown, this::setTriggerCooldown).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.damage"), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getDamageRadius, this::setDamageRadius).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index ccd8d4a19..25b895374 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -1,23 +1,21 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityVortex; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; +import noppes.npcs.api.ability.type.IAbilityVortex; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -27,79 +25,81 @@ import java.util.UUID; * Vortex ability: Pulls targets toward the caster. * Can pull single target or AOE, with optional damage and stun on arrival. */ -public class AbilityVortex extends Ability { +public class AbilityVortex extends Ability implements IAbilityVortex { private float pullRadius = 8.0f; private float pullStrength = 0.8f; private float damage = 0.0f; private float knockback = 0.0f; - private int stunDuration = 0; - private int rootDuration = 0; private boolean aoe = false; private int maxTargets = 5; private boolean damageOnPull = false; private float pullDamage = 0.0f; // Runtime state - private transient Set pulledEntities = new HashSet<>(); + private transient Set pulledEntities; private transient boolean pullComplete = false; private transient int ticksSincePullDamage = 0; + private Set getPulledEntities() { + if (pulledEntities == null) { + pulledEntities = new HashSet<>(); + } + return pulledEntities; + } + public AbilityVortex() { this.typeId = "ability.cnpc.vortex"; this.name = "Vortex"; this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 15.0f; - this.lockMovement = true; - this.cooldownTicks = 120; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 40; - this.recoveryTicks = 20; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "mob.ghast.charge"; this.activeSound = "mob.ghast.fireball"; } @Override - public boolean hasTypeSettings() { return true; } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityVortex(this, callback); + public boolean hasTypeSettings() { + return true; } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF }; + return new TargetingMode[]{TargetingMode.AOE_SELF}; } @Override - public float getTelegraphRadius() { return pullRadius; } + public float getTelegraphRadius() { + return pullRadius; + } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - pulledEntities.clear(); + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + getPulledEntities().clear(); pullComplete = false; ticksSincePullDamage = 0; if (aoe) { - AxisAlignedBB box = npc.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); int count = 0; for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (entity.isDead) continue; - double dist = npc.getDistanceToEntity(entity); + double dist = caster.getDistanceToEntity(entity); if (dist <= pullRadius) { - pulledEntities.add(entity.getUniqueID()); + getPulledEntities().add(entity.getUniqueID()); count++; if (count >= maxTargets) break; } @@ -107,31 +107,32 @@ public class AbilityVortex extends Ability { } else { // Single target mode - still check pullRadius if (target != null && !target.isDead) { - double dist = npc.getDistanceToEntity(target); + double dist = caster.getDistanceToEntity(target); if (dist <= pullRadius) { - pulledEntities.add(target.getUniqueID()); + getPulledEntities().add(target.getUniqueID()); } } } } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (pullComplete || pulledEntities.isEmpty()) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (pullComplete || getPulledEntities().isEmpty()) { + signalCompletion(); return; } - double destX = npc.posX; - double destY = npc.posY; - double destZ = npc.posZ; + double destX = caster.posX; + double destY = caster.posY; + double destZ = caster.posZ; boolean anyStillPulling = false; ticksSincePullDamage++; - for (UUID uuid : new HashSet<>(pulledEntities)) { - EntityLivingBase entity = findEntity(world, uuid); + for (UUID uuid : new HashSet<>(getPulledEntities())) { + EntityLivingBase entity = findEntity(caster, world, uuid); if (entity == null || entity.isDead) { - pulledEntities.remove(uuid); + getPulledEntities().remove(uuid); continue; } @@ -141,8 +142,8 @@ public class AbilityVortex extends Ability { double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); if (dist <= 1.0f) { - pulledEntities.remove(uuid); - onTargetArrived(npc, entity, world); + getPulledEntities().remove(uuid); + onTargetArrived(caster, entity, world); continue; } @@ -155,7 +156,7 @@ public class AbilityVortex extends Ability { AxisAlignedBB nextBox = entity.boundingBox.copy().offset(nextX, nextY, nextZ); if (!world.getCollidingBoundingBoxes(entity, nextBox).isEmpty()) { - pulledEntities.remove(uuid); + getPulledEntities().remove(uuid); continue; } @@ -167,55 +168,47 @@ public class AbilityVortex extends Ability { if (damageOnPull && pullDamage > 0 && ticksSincePullDamage >= 10) { ticksSincePullDamage = 0; entity.hurtResistantTime = 0; - applyAbilityDamage(npc, entity, pullDamage * 0.5f, 0); + applyAbilityDamage(caster, entity, pullDamage * 0.5f, 0); } } - if (!anyStillPulling && pulledEntities.isEmpty()) { + if (!anyStillPulling && getPulledEntities().isEmpty()) { pullComplete = true; + signalCompletion(); } } - private void onTargetArrived(EntityNPCInterface npc, EntityLivingBase entity, World world) { + private void onTargetArrived(EntityLivingBase caster, EntityLivingBase entity, World world) { // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback * 0.5f); + boolean wasHit = applyAbilityDamage(caster, entity, damage, knockback * 0.5f); // Only apply effects if hit wasn't cancelled if (wasHit) { - if (stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - - if (rootDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, rootDuration, 127)); - } + applyEffects(entity); } } + /** + * Find an entity by UUID within the vortex pull area. + * Uses AABB search instead of iterating all loaded entities. + */ @SuppressWarnings("unchecked") - private EntityLivingBase findEntity(World world, UUID uuid) { - for (Object obj : world.loadedEntityList) { - if (obj instanceof EntityLivingBase) { - EntityLivingBase entity = (EntityLivingBase) obj; - if (entity.getUniqueID().equals(uuid)) { - return entity; - } + private EntityLivingBase findEntity(EntityLivingBase caster, World world, UUID uuid) { + // Search within pull radius (entities being pulled should be within this area) + AxisAlignedBB searchBox = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + List nearbyEntities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); + + for (EntityLivingBase entity : nearbyEntities) { + if (entity.getUniqueID().equals(uuid)) { + return entity; } } return null; } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - pulledEntities.clear(); - pullComplete = false; - ticksSincePullDamage = 0; - } - - @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - pulledEntities.clear(); + public void cleanup() { + getPulledEntities().clear(); pullComplete = false; ticksSincePullDamage = 0; } @@ -226,8 +219,6 @@ public class AbilityVortex extends Ability { nbt.setFloat("pullStrength", pullStrength); nbt.setFloat("damage", damage); nbt.setFloat("knockback", knockback); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("rootDuration", rootDuration); nbt.setBoolean("aoe", aoe); nbt.setInteger("maxTargets", maxTargets); nbt.setBoolean("damageOnPull", damageOnPull); @@ -240,8 +231,6 @@ public class AbilityVortex extends Ability { this.pullStrength = nbt.hasKey("pullStrength") ? nbt.getFloat("pullStrength") : 0.8f; this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 0.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.0f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.rootDuration = nbt.hasKey("rootDuration") ? nbt.getInteger("rootDuration") : 0; this.aoe = nbt.hasKey("aoe") && nbt.getBoolean("aoe"); this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 5; this.damageOnPull = nbt.hasKey("damageOnPull") && nbt.getBoolean("damageOnPull"); @@ -249,33 +238,89 @@ public class AbilityVortex extends Ability { } // Getters & Setters - public float getPullRadius() { return pullRadius; } - public void setPullRadius(float pullRadius) { this.pullRadius = pullRadius; } + public float getPullRadius() { + return pullRadius; + } - public float getPullStrength() { return pullStrength; } - public void setPullStrength(float pullStrength) { this.pullStrength = pullStrength; } + public void setPullRadius(float pullRadius) { + this.pullRadius = pullRadius; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getPullStrength() { + return pullStrength; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setPullStrength(float pullStrength) { + this.pullStrength = pullStrength; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public float getDamage() { + return damage; + } - public int getRootDuration() { return rootDuration; } - public void setRootDuration(int rootDuration) { this.rootDuration = rootDuration; } + public void setDamage(float damage) { + this.damage = damage; + } - public boolean isAoe() { return aoe; } - public void setAoe(boolean aoe) { this.aoe = aoe; } + public float getKnockback() { + return knockback; + } - public int getMaxTargets() { return maxTargets; } - public void setMaxTargets(int maxTargets) { this.maxTargets = maxTargets; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public boolean isDamageOnPull() { return damageOnPull; } - public void setDamageOnPull(boolean damageOnPull) { this.damageOnPull = damageOnPull; } + public boolean isAoe() { + return aoe; + } - public float getPullDamage() { return pullDamage; } - public void setPullDamage(float pullDamage) { this.pullDamage = pullDamage; } + public void setAoe(boolean aoe) { + this.aoe = aoe; + } + + public int getMaxTargets() { + return maxTargets; + } + + public void setMaxTargets(int maxTargets) { + this.maxTargets = maxTargets; + } + + public boolean isDamageOnPull() { + return damageOnPull; + } + + public void setDamageOnPull(boolean damageOnPull) { + this.damageOnPull = damageOnPull; + } + + public float getPullDamage() { + return pullDamage; + } + + public void setPullDamage(float pullDamage) { + this.pullDamage = pullDamage; + } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("ability.pullRadius", this::getPullRadius, this::setPullRadius).column(ColumnHint.LEFT), + FieldDef.floatField("ability.pullStrength", this::getPullStrength, this::setPullStrength).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.damage"), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.aoe"), + FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe) + .hover("ability.hover.aoe"), + FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets) + .visibleWhen(this::isAoe), + FieldDef.section("ability.section.pullDamage"), + FieldDef.boolField("gui.enabled", this::isDamageOnPull, this::setDamageOnPull) + .hover("ability.hover.dmgOnPull"), + FieldDef.floatField("enchantment.damage", this::getPullDamage, this::setPullDamage) + .visibleWhen(this::isDamageOnPull), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java index ccbae74ef..e4eb04dfe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java @@ -5,6 +5,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerTradeData; import java.util.ArrayList; import java.util.List; @@ -19,7 +20,10 @@ public class CNPCData implements IProfileData { @Override public NBTTagCompound getCurrentNBT(EntityPlayer player) { PlayerData customNPCData = PlayerData.get(player); - return customNPCData.getNBT(); + NBTTagCompound compound = customNPCData.getNBT(); + compound.removeTag(PlayerTradeData.NBT_KEY); + + return compound; } @Override @@ -32,6 +36,10 @@ public class CNPCData implements IProfileData { @Override public void setNBT(EntityPlayer player, NBTTagCompound replace) { PlayerData customNPCData = PlayerData.get(player); + + NBTTagCompound sharedData = new NBTTagCompound(); + customNPCData.tradeData.writeToNBT(sharedData); + if (replace.hasNoTags()) { PlayerData newData = new PlayerData(); newData.player = player; @@ -39,6 +47,8 @@ public class CNPCData implements IProfileData { } else { customNPCData.setNBT(replace); } + + customNPCData.tradeData.readFromNBT(sharedData); customNPCData.updateClient = true; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java similarity index 67% rename from src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java rename to src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java index 1b42b55e1..3ba858acc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java @@ -1,10 +1,10 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; +package kamkeel.npcs.controllers.data.telegraph; import net.minecraft.nbt.NBTTagCompound; /** * Telegraph configuration - defines a visual warning shape. - * Telegraphs warn players about incoming attacks. + * Telegraphs warn players about incoming attacks or mark areas of effect. */ public class Telegraph { @@ -35,7 +35,8 @@ public class Telegraph { private boolean followsCaster = false; private float offsetForward = 0.0f; - public Telegraph() {} + public Telegraph() { + } public Telegraph(String id, TelegraphType type) { this.id = id; @@ -145,58 +146,147 @@ public class Telegraph { return t; } + public static Telegraph point() { + return new Telegraph("", TelegraphType.POINT); + } + // ═══════════════════════════════════════════════════════════════════ // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ - public String getId() { return id; } - public void setId(String id) { this.id = id; } + public String getId() { + return id; + } - public TelegraphType getType() { return type; } - public void setType(TelegraphType type) { this.type = type; } + public void setId(String id) { + this.id = id; + } - public float getRadius() { return radius; } - public void setRadius(float radius) { this.radius = radius; } + public TelegraphType getType() { + return type; + } - public float getInnerRadius() { return innerRadius; } - public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; } + public void setType(TelegraphType type) { + this.type = type; + } - public float getLength() { return length; } - public void setLength(float length) { this.length = length; } + public float getRadius() { + return radius; + } - public float getWidth() { return width; } - public void setWidth(float width) { this.width = width; } + public void setRadius(float radius) { + this.radius = radius; + } - public float getAngle() { return angle; } - public void setAngle(float angle) { this.angle = angle; } + public float getInnerRadius() { + return innerRadius; + } - public int getDurationTicks() { return durationTicks; } - public void setDurationTicks(int durationTicks) { this.durationTicks = durationTicks; } + public void setInnerRadius(float innerRadius) { + this.innerRadius = innerRadius; + } - public int getColor() { return color; } - public void setColor(int color) { this.color = color; } + public float getLength() { + return length; + } - public int getWarningColor() { return warningColor; } - public void setWarningColor(int warningColor) { this.warningColor = warningColor; } + public void setLength(float length) { + this.length = length; + } - public int getWarningStartTick() { return warningStartTick; } - public void setWarningStartTick(int warningStartTick) { this.warningStartTick = warningStartTick; } + public float getWidth() { + return width; + } - public boolean isAnimated() { return animated; } - public void setAnimated(boolean animated) { this.animated = animated; } + public void setWidth(float width) { + this.width = width; + } - public float getHeightOffset() { return heightOffset; } - public void setHeightOffset(float heightOffset) { this.heightOffset = heightOffset; } + public float getAngle() { + return angle; + } - public boolean isAtTarget() { return atTarget; } - public void setAtTarget(boolean atTarget) { this.atTarget = atTarget; } + public void setAngle(float angle) { + this.angle = angle; + } - public boolean isFollowsTarget() { return followsTarget; } - public void setFollowsTarget(boolean followsTarget) { this.followsTarget = followsTarget; } + public int getDurationTicks() { + return durationTicks; + } - public boolean isFollowsCaster() { return followsCaster; } - public void setFollowsCaster(boolean followsCaster) { this.followsCaster = followsCaster; } + public void setDurationTicks(int durationTicks) { + this.durationTicks = durationTicks; + } - public float getOffsetForward() { return offsetForward; } - public void setOffsetForward(float offsetForward) { this.offsetForward = offsetForward; } + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + + public int getWarningColor() { + return warningColor; + } + + public void setWarningColor(int warningColor) { + this.warningColor = warningColor; + } + + public int getWarningStartTick() { + return warningStartTick; + } + + public void setWarningStartTick(int warningStartTick) { + this.warningStartTick = warningStartTick; + } + + public boolean isAnimated() { + return animated; + } + + public void setAnimated(boolean animated) { + this.animated = animated; + } + + public float getHeightOffset() { + return heightOffset; + } + + public void setHeightOffset(float heightOffset) { + this.heightOffset = heightOffset; + } + + public boolean isAtTarget() { + return atTarget; + } + + public void setAtTarget(boolean atTarget) { + this.atTarget = atTarget; + } + + public boolean isFollowsTarget() { + return followsTarget; + } + + public void setFollowsTarget(boolean followsTarget) { + this.followsTarget = followsTarget; + } + + public boolean isFollowsCaster() { + return followsCaster; + } + + public void setFollowsCaster(boolean followsCaster) { + this.followsCaster = followsCaster; + } + + public float getOffsetForward() { + return offsetForward; + } + + public void setOffsetForward(float offsetForward) { + this.offsetForward = offsetForward; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java similarity index 51% rename from src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java rename to src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java index b16a9f0f2..a5d2f0d95 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java @@ -1,6 +1,6 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; +package kamkeel.npcs.controllers.data.telegraph; -import net.minecraft.block.Block; +import kamkeel.npcs.controllers.data.ability.Ability; import net.minecraft.entity.Entity; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; @@ -17,15 +17,22 @@ public class TelegraphInstance { private String instanceId; private Telegraph telegraph; - // Position + // Position (current) private double x; private double y; private double z; private float yaw; + // Position (previous tick - for interpolation) + private double prevX; + private double prevY; + private double prevZ; + private float prevYaw; + // Entity tracking private int entityIdToFollow = -1; private int casterEntityId = -1; + private int targetEntityId = -1; // For LINE telegraphs: entity to face (yaw tracking) // Timing private int remainingTicks; @@ -34,6 +41,10 @@ public class TelegraphInstance { // State private boolean isWarning = false; + // Server-side tracking (for removal) + private transient World world; + private transient int dimensionId; + public TelegraphInstance() { this.instanceId = UUID.randomUUID().toString().substring(0, 8); } @@ -45,6 +56,11 @@ public class TelegraphInstance { this.y = y; this.z = z; this.yaw = yaw; + // Initialize prev positions to current for first frame + this.prevX = x; + this.prevY = y; + this.prevZ = z; + this.prevYaw = yaw; this.remainingTicks = telegraph.getDurationTicks(); this.totalTicks = telegraph.getDurationTicks(); } @@ -55,6 +71,7 @@ public class TelegraphInstance { /** * Called every tick to update telegraph state. + * * @param world The world (for entity lookup) * @return false if telegraph should be removed */ @@ -63,6 +80,12 @@ public class TelegraphInstance { return false; } + // Save previous position for interpolation + this.prevX = this.x; + this.prevY = this.y; + this.prevZ = this.z; + this.prevYaw = this.yaw; + remainingTicks--; // Update position if following an entity @@ -72,7 +95,17 @@ public class TelegraphInstance { this.x = entity.posX; this.z = entity.posZ; // Find ground level below the entity for proper telegraph placement - this.y = findGroundLevel(world, entity.posX, entity.posY, entity.posZ); + this.y = Ability.findGroundLevel(world, entity.posX, entity.posY, entity.posZ); + } + } + + // Update yaw to face target entity (for LINE telegraphs) + if (targetEntityId >= 0 && world != null) { + Entity target = world.getEntityByID(targetEntityId); + if (target != null) { + double dx = target.posX - this.x; + double dz = target.posZ - this.z; + this.yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI); } } @@ -86,34 +119,6 @@ public class TelegraphInstance { return true; } - /** - * Find the ground level at a given position. - * Searches downward from the given Y to find a solid block. - * - * @param world The world - * @param x X coordinate - * @param startY Starting Y coordinate (entity feet position) - * @param z Z coordinate - * @return The Y coordinate of the ground surface - */ - private double findGroundLevel(World world, double x, double startY, double z) { - int blockX = (int) Math.floor(x); - int blockZ = (int) Math.floor(z); - int startBlockY = (int) Math.floor(startY); - - // Search downward for solid ground (max 10 blocks down) - for (int checkY = startBlockY; checkY >= startBlockY - 10 && checkY >= 0; checkY--) { - Block block = world.getBlock(blockX, checkY, blockZ); - if (block != null && block.getMaterial().isSolid()) { - // Found solid block, telegraph goes on top of it - return checkY + 1; - } - } - - // No ground found, use original position - return startY; - } - /** * Get progress from 0.0 (just started) to 1.0 (finished). */ @@ -131,7 +136,7 @@ public class TelegraphInstance { } /** - * Get alpha-modulated color for pulsing animation. + * Get alpha-modulated color for breathing animation. */ public int getAnimatedColor(float partialTicks) { int baseColor = getCurrentColor(); @@ -139,9 +144,12 @@ public class TelegraphInstance { return baseColor; } - // Pulsing animation: modulate alpha based on time + // Breathing animation: slow, smooth modulation of alpha float time = (totalTicks - remainingTicks) + partialTicks; - float pulse = (float) (Math.sin(time * 0.3) * 0.3 + 0.7); // 0.4 to 1.0 range + // Slower breathing: 0.08 for ~4 second cycle, smoother sine curve + float breathPhase = (float) Math.sin(time * 0.08); + // Smooth easing: square the sine for more organic feel, range 0.5 to 1.0 + float pulse = 0.5f + (breathPhase * breathPhase * 0.5f * Math.signum(breathPhase)); int alpha = (baseColor >> 24) & 0xFF; int r = (baseColor >> 16) & 0xFF; @@ -153,6 +161,43 @@ public class TelegraphInstance { return (alpha << 24) | (r << 16) | (g << 8) | b; } + // ═══════════════════════════════════════════════════════════════════ + // INTERPOLATED POSITION (for smooth rendering) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Get interpolated X position for smooth rendering. + */ + public double getInterpolatedX(float partialTicks) { + return prevX + (x - prevX) * partialTicks; + } + + /** + * Get interpolated Y position for smooth rendering. + */ + public double getInterpolatedY(float partialTicks) { + return prevY + (y - prevY) * partialTicks; + } + + /** + * Get interpolated Z position for smooth rendering. + */ + public double getInterpolatedZ(float partialTicks) { + return prevZ + (z - prevZ) * partialTicks; + } + + /** + * Get interpolated yaw for smooth rendering. + * Handles angle wrapping for smooth rotation. + */ + public float getInterpolatedYaw(float partialTicks) { + // Handle angle wrapping to prevent spinning the wrong way + float diff = yaw - prevYaw; + while (diff > 180.0f) diff -= 360.0f; + while (diff < -180.0f) diff += 360.0f; + return prevYaw + diff * partialTicks; + } + // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ @@ -172,6 +217,7 @@ public class TelegraphInstance { nbt.setInteger("entityIdToFollow", entityIdToFollow); nbt.setInteger("casterEntityId", casterEntityId); + nbt.setInteger("targetEntityId", targetEntityId); nbt.setInteger("remainingTicks", remainingTicks); nbt.setInteger("totalTicks", totalTicks); @@ -196,6 +242,7 @@ public class TelegraphInstance { this.entityIdToFollow = nbt.getInteger("entityIdToFollow"); this.casterEntityId = nbt.getInteger("casterEntityId"); + this.targetEntityId = nbt.getInteger("targetEntityId"); this.remainingTicks = nbt.getInteger("remainingTicks"); this.totalTicks = nbt.getInteger("totalTicks"); @@ -207,42 +254,127 @@ public class TelegraphInstance { // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ - public String getInstanceId() { return instanceId; } - public void setInstanceId(String instanceId) { this.instanceId = instanceId; } + public String getInstanceId() { + return instanceId; + } - public Telegraph getTelegraph() { return telegraph; } - public void setTelegraph(Telegraph telegraph) { this.telegraph = telegraph; } + public void setInstanceId(String instanceId) { + this.instanceId = instanceId; + } - public double getX() { return x; } - public void setX(double x) { this.x = x; } + public Telegraph getTelegraph() { + return telegraph; + } - public double getY() { return y; } - public void setY(double y) { this.y = y; } + public void setTelegraph(Telegraph telegraph) { + this.telegraph = telegraph; + } - public double getZ() { return z; } - public void setZ(double z) { this.z = z; } + public double getX() { + return x; + } - public float getYaw() { return yaw; } - public void setYaw(float yaw) { this.yaw = yaw; } + public void setX(double x) { + this.x = x; + } - public int getEntityIdToFollow() { return entityIdToFollow; } - public void setEntityIdToFollow(int entityIdToFollow) { this.entityIdToFollow = entityIdToFollow; } + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getZ() { + return z; + } + + public void setZ(double z) { + this.z = z; + } + + public float getYaw() { + return yaw; + } + + public void setYaw(float yaw) { + this.yaw = yaw; + } + + public int getEntityIdToFollow() { + return entityIdToFollow; + } + + public void setEntityIdToFollow(int entityIdToFollow) { + this.entityIdToFollow = entityIdToFollow; + } /** - * Locks the telegraph at its current position. + * Locks the telegraph at its current position and direction. * Stops following any entity - used when ability commits to action. */ - public void lockPosition() { this.entityIdToFollow = -1; } + public void lockPosition() { + this.entityIdToFollow = -1; + this.targetEntityId = -1; + } - public int getCasterEntityId() { return casterEntityId; } - public void setCasterEntityId(int casterEntityId) { this.casterEntityId = casterEntityId; } + public int getCasterEntityId() { + return casterEntityId; + } - public int getRemainingTicks() { return remainingTicks; } - public void setRemainingTicks(int remainingTicks) { this.remainingTicks = remainingTicks; } + public void setCasterEntityId(int casterEntityId) { + this.casterEntityId = casterEntityId; + } - public int getTotalTicks() { return totalTicks; } - public void setTotalTicks(int totalTicks) { this.totalTicks = totalTicks; } + public int getTargetEntityId() { + return targetEntityId; + } - public boolean isWarning() { return isWarning; } - public void setWarning(boolean warning) { isWarning = warning; } + public void setTargetEntityId(int targetEntityId) { + this.targetEntityId = targetEntityId; + } + + public int getRemainingTicks() { + return remainingTicks; + } + + public void setRemainingTicks(int remainingTicks) { + this.remainingTicks = remainingTicks; + } + + public int getTotalTicks() { + return totalTicks; + } + + public void setTotalTicks(int totalTicks) { + this.totalTicks = totalTicks; + } + + public boolean isWarning() { + return isWarning; + } + + public void setWarning(boolean warning) { + isWarning = warning; + } + + public World getWorld() { + return world; + } + + public void setWorld(World world) { + this.world = world; + if (world != null) { + this.dimensionId = world.provider.dimensionId; + } + } + + public int getDimensionId() { + return dimensionId; + } + + public void setDimensionId(int dimensionId) { + this.dimensionId = dimensionId; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java similarity index 96% rename from src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java rename to src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java index b82afffd2..57b245fd4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java @@ -1,4 +1,4 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; +package kamkeel.npcs.controllers.data.telegraph; import net.minecraft.world.World; @@ -12,12 +12,15 @@ import java.util.concurrent.ConcurrentHashMap; */ public class TelegraphManager { - /** Client-side instance for rendering */ + /** + * Client-side instance for rendering + */ public static TelegraphManager ClientInstance; private final ConcurrentHashMap telegraphs = new ConcurrentHashMap<>(); - public TelegraphManager() {} + public TelegraphManager() { + } /** * Initialize the client-side manager. @@ -92,7 +95,7 @@ public class TelegraphManager { } // ═══════════════════════════════════════════════════════════════════ - // HELPER METHODS FOR ABILITIES + // HELPER METHODS FOR SPAWNING // ═══════════════════════════════════════════════════════════════════ /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java new file mode 100644 index 000000000..4a2fe4b12 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java @@ -0,0 +1,36 @@ +package kamkeel.npcs.controllers.data.telegraph; + +/** + * Types of telegraph shapes for visual attack warnings. + */ +public enum TelegraphType { + /** + * Filled circle (uses radius) + */ + CIRCLE, + + /** + * Donut shape (uses radius and innerRadius) + */ + RING, + + /** + * Rectangle (uses length, width) + */ + LINE, + + /** + * Fan/wedge shape (uses length, angle) + */ + CONE, + + /** + * Single point indicator + */ + POINT, + + /** + * No telegraph + */ + NONE +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java new file mode 100644 index 000000000..6aed40c3f --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -0,0 +1,851 @@ +package kamkeel.npcs.entity; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.LogWriter; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Beam projectile - head with trailing path that curves with homing. + * Stays attached to origin point, trail shows path of head. + * + * IMPORTANT: Trail points are stored RELATIVE to startX/Y/Z (origin). + * This ensures rendering is stable regardless of entity position interpolation. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityBeam extends EntityAbilityProjectile { + + // Beam-specific movement properties + private float speed = 0.5f; + private boolean homing = true; + private float homingStrength = 0.35f; // Increased from 0.15 for better tracking + private float homingRange = 20.0f; + private UUID siblingUUID = null; + + // Beam shape properties + private float beamWidth = 0.3f; + private float headSize = 0.5f; + + // Trail - list of points showing beam path (RELATIVE to origin!) + private List trailPoints = new ArrayList<>(); + private static final int MAX_TRAIL_POINTS = 200; + private static final double MIN_POINT_DISTANCE = 0.2; + + // Head position relative to origin + private double headOffsetX, headOffsetY, headOffsetZ; + private double prevHeadOffsetX, prevHeadOffsetY, prevHeadOffsetZ; + + // Origin stays fixed (or follows owner) + private boolean attachedToOwner = true; + + // Whether to render tail orb (only when anchored) + private boolean renderTailOrb = true; + + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private float chargeOffsetDistance = 1.0f; + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); + + // Trail fading for non-anchored beams (comet effect) + private boolean fadeTrail = false; + private int trailFadeTime = 20; // Ticks for trail to fully fade + private List trailPointAges = new ArrayList<>(); + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + + // Debug logging + private static final boolean DEBUG_LOGGING = false; + + public EntityAbilityBeam(World world) { + super(world); + } + + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * Check if beam is in charging state (synced via data watcher). + * In preview mode, uses local field since data watcher isn't synced. + */ + public boolean isCharging() { + if (previewMode) { + return this.charging; + } + return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + + /** + * Full constructor with all parameters using data classes. + * @param anchoredMode If true, origin follows owner and tail orb is rendered. + * If false, beam is free-moving with trailing length (no tail orb). + */ + public EntityAbilityBeam(World world, EntityLivingBase owner, EntityLivingBase target, + double x, double y, double z, + float beamWidth, float headSize, + EnergyColorData color, EnergyCombatData combat, + EnergyHomingData homing, EnergyLightningData lightning, + EnergyLifespanData lifespan, + boolean anchoredMode) { + super(world); + + // Initialize base properties via parent + initProjectile(owner, target, x, y, z, headSize, color, combat, lightning, lifespan); + + // Beam-specific properties from homing data + this.speed = homing.speed; + this.homing = homing.homing; + this.homingStrength = homing.homingStrength; + this.homingRange = homing.homingRange; + this.beamWidth = beamWidth; + this.headSize = headSize; + + // Anchored mode controls whether origin follows owner and tail orb is rendered + this.attachedToOwner = anchoredMode; + this.renderTailOrb = anchoredMode; + + // Initialize head offset at origin (0,0,0 relative) + this.headOffsetX = 0; + this.headOffsetY = 0; + this.headOffsetZ = 0; + this.prevHeadOffsetX = 0; + this.prevHeadOffsetY = 0; + this.prevHeadOffsetZ = 0; + + // Add initial trail point at origin (relative 0,0,0) + trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); + + if (DEBUG_LOGGING && !world.isRemote) { + LogWriter.info("[Beam] Created at origin " + x + ", " + y + ", " + z + " maxDist=" + lifespan.maxDistance); + } + + // Calculate initial velocity toward target + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.getEyeHeight()) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.motionX = (dx / len) * speed; + this.motionY = (dy / len) * speed; + this.motionZ = (dz / len) * speed; + } + } else { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + this.motionY = -Math.sin(pitch) * speed; + this.motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + public void setupCharging(EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.chargeOffsetDistance = chargeOffsetDistance; + this.anchorData = anchor; + this.fadeTrail = !attachedToOwner; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + public void setupPreview(EntityLivingBase owner, float beamWidth, float headSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); + + // Set visual properties + this.beamWidth = beamWidth; + this.headSize = headSize; + this.size = headSize; + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.outerColorAlpha = color.outerColorAlpha; + this.rotationSpeed = color.rotationSpeed; + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + + // Set charging state + this.setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.chargeOffsetDistance = chargeOffsetDistance; + this.anchorData = anchor; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData, chargeOffsetDistance); + this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + this.prevPosX = pos.xCoord; + this.prevPosY = pos.yCoord; + this.prevPosZ = pos.zCoord; + this.startX = pos.xCoord; + this.startY = pos.yCoord; + this.startZ = pos.zCoord; + + // Initialize head offsets + this.headOffsetX = 0; + this.headOffsetY = 0; + this.headOffsetZ = 0; + this.prevHeadOffsetX = 0; + this.prevHeadOffsetY = 0; + this.prevHeadOffsetZ = 0; + + // Attach to owner for anchor following + this.attachedToOwner = true; + this.renderTailOrb = true; + + // Clear motion + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + /** + * Start preview firing (simulates firing toward a point in front of NPC). + */ + public void startPreviewFiring() { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Sync prev position to prevent visual jump on first frame + prevPosX = posX; + prevPosY = posY; + prevPosZ = posZ; + + // Fire forward based on owner facing direction + Entity owner = getOwner(); + if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = 0; // Fire horizontally + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } else { + motionX = speed; + motionY = 0; + motionZ = 0; + } + } + + /** + * Start the beam firing (exit charging mode). + * Called by ability when windup ends. + * + * For anchored beams: origin follows owner, head starts at charged position + * For non-anchored beams: origin fixed at charged position, head starts there + */ + public void startFiring(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Origin (tail) stays at the charged position - where the orb was + // This is the same for both anchored and non-anchored modes + startX = posX; + startY = posY; + startZ = posZ; + + // Head starts at the origin (tail position) + headOffsetX = 0; + headOffsetY = 0; + headOffsetZ = 0; + prevHeadOffsetX = 0; + prevHeadOffsetY = 0; + prevHeadOffsetZ = 0; + + // Origin no longer follows owner after firing starts + // (for anchored mode, tail is fixed in space; for non-anchored, there's no tail) + attachedToOwner = false; + + // Initialize trail with just the origin point + trailPoints.clear(); + trailPointAges.clear(); + trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); + if (fadeTrail) trailPointAges.add(0); + + // Calculate velocity toward target (head starts at origin = startX/Y/Z) + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - startX; + double dy = (target.posY + target.getEyeHeight()) - startY; + double dz = target.posZ - startZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } else if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + + if (DEBUG_LOGGING && !worldObj.isRemote) { + LogWriter.info("[Beam] startFiring: origin=(" + startX + "," + startY + "," + startZ + + ") headOffset=(" + headOffsetX + "," + headOffsetY + "," + headOffsetZ + + ") motion=(" + motionX + "," + motionY + "," + motionZ + ")"); + } + } + + @Override + protected boolean checkMaxDistance() { + // Check distance using head offset (distance from origin) + double distFromOrigin = Math.sqrt( + headOffsetX * headOffsetX + + headOffsetY * headOffsetY + + headOffsetZ * headOffsetZ + ); + boolean exceeded = distFromOrigin >= maxDistance; + if (exceeded && !worldObj.isRemote && DEBUG_LOGGING) { + LogWriter.info("[Beam] DEAD: Max distance exceeded. dist=" + distFromOrigin + " max=" + maxDistance); + } + return exceeded; + } + + @Override + protected void updateProjectile() { + // Handle charging state (windup phase) - use isCharging() for synced value + if (isCharging()) { + updateCharging(); + return; + } + + // Store previous head offset for interpolation + prevHeadOffsetX = headOffsetX; + prevHeadOffsetY = headOffsetY; + prevHeadOffsetZ = headOffsetZ; + + // Age trail points for fading effect + if (fadeTrail) { + ageTrailPoints(); + } + + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + + if (worldObj.isRemote) { + // Client: interpolate entity position + handleClientInterpolation(); + + // Client: derive head offset from entity position and origin + // Entity position = head world position, so headOffset = pos - origin + // Note: During charging, updateCharging() handles origin positioning + // After firing, origin is fixed at the charged position + headOffsetX = posX - startX; + headOffsetY = posY - startY; + headOffsetZ = posZ - startZ; + + // Add trail point on client + addTrailPoint(); + } else { + // Server-side logic + updateHoming(); + + // Move head offset + headOffsetX += motionX; + headOffsetY += motionY; + headOffsetZ += motionZ; + + // Update origin if attached to owner + if (attachedToOwner) { + Entity owner = getOwner(); + if (owner != null) { + startX = owner.posX; + startY = owner.posY + owner.getEyeHeight() * 0.7; + startZ = owner.posZ; + } + } + + // Calculate world position of head for collision detection + double headWorldX = startX + headOffsetX; + double headWorldY = startY + headOffsetY; + double headWorldZ = startZ + headOffsetZ; + + // Update entity position to head (for network sync) + this.setPosition(headWorldX, headWorldY, headWorldZ); + + // Add trail point + addTrailPoint(); + + // Debug log periodically + if (DEBUG_LOGGING && ticksExisted % 20 == 0) { + LogWriter.info("[Beam] Server tick=" + ticksExisted + " headOffset=(" + + String.format("%.2f", headOffsetX) + "," + + String.format("%.2f", headOffsetY) + "," + + String.format("%.2f", headOffsetZ) + ") origin=(" + + String.format("%.2f", startX) + "," + + String.format("%.2f", startY) + "," + + String.format("%.2f", startZ) + ") trail=" + trailPoints.size()); + } + + // Skip collision checks on first few ticks + if (ticksExisted > 2) { + checkBlockCollision(headWorldX, headWorldY, headWorldZ); + checkEntityCollision(headWorldX, headWorldY, headWorldZ); + } + } + } + + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Calculate position based on anchor point + Vec3 pos; + if (owner instanceof EntityLivingBase) { + pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData, chargeOffsetDistance); + } else { + // Fallback for non-living entities (shouldn't happen normally) + float yaw = (float) Math.toRadians(owner.rotationYaw); + double offsetX = -Math.sin(yaw) * chargeOffsetDistance; + double offsetZ = Math.cos(yaw) * chargeOffsetDistance; + pos = Vec3.createVectorHelper( + owner.posX + offsetX, + owner.posY + owner.getEyeHeight() * 0.7, + owner.posZ + offsetZ + ); + } + + // Offset downward by half headSize to center + double centeredY = pos.yCoord - headSize * 0.5; + setPosition(pos.xCoord, centeredY, pos.zCoord); + + // Also update origin for when firing starts + startX = pos.xCoord; + startY = centeredY; + startZ = pos.zCoord; + } + + /** + * Age trail points and remove old ones (for comet effect). + */ + private void ageTrailPoints() { + // Age all trail points + for (int i = 0; i < trailPointAges.size(); i++) { + trailPointAges.set(i, trailPointAges.get(i) + 1); + } + + // Remove trail points that have fully faded + while (!trailPointAges.isEmpty() && trailPointAges.get(0) >= trailFadeTime) { + trailPointAges.remove(0); + if (!trailPoints.isEmpty()) { + trailPoints.remove(0); + } + } + } + + private void addTrailPoint() { + // Trail points are RELATIVE to origin + if (trailPoints.isEmpty()) { + trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); + if (fadeTrail) { + trailPointAges.add(0); + } + return; + } + + Vec3 lastPoint = trailPoints.get(trailPoints.size() - 1); + double dist = Math.sqrt( + (headOffsetX - lastPoint.xCoord) * (headOffsetX - lastPoint.xCoord) + + (headOffsetY - lastPoint.yCoord) * (headOffsetY - lastPoint.yCoord) + + (headOffsetZ - lastPoint.zCoord) * (headOffsetZ - lastPoint.zCoord) + ); + + if (dist >= MIN_POINT_DISTANCE) { + trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); + if (fadeTrail) { + trailPointAges.add(0); + } + + // Limit trail length (only if not using fading - fading handles its own cleanup) + if (!fadeTrail) { + while (trailPoints.size() > MAX_TRAIL_POINTS) { + trailPoints.remove(0); + } + } + } + } + + /** + * Update movement in preview mode (client-side only, no damage). + */ + private void updatePreviewMovement() { + // Move head offset + headOffsetX += motionX; + headOffsetY += motionY; + headOffsetZ += motionZ; + + // Update origin if attached to owner + if (attachedToOwner) { + Entity owner = getOwner(); + if (owner != null) { + startX = owner.posX; + startY = owner.posY + owner.getEyeHeight() * 0.7; + startZ = owner.posZ; + } + } + + // Calculate world position of head + double headWorldX = startX + headOffsetX; + double headWorldY = startY + headOffsetY; + double headWorldZ = startZ + headOffsetZ; + + // Update entity position to head + this.setPosition(headWorldX, headWorldY, headWorldZ); + + // Add trail point + addTrailPoint(); + } + + private void updateHoming() { + if (!homing) return; + + Entity target = getTarget(); + if (target == null || !target.isEntityAlive()) return; + + // Calculate world position of head + double headWorldX = startX + headOffsetX; + double headWorldY = startY + headOffsetY; + double headWorldZ = startZ + headOffsetZ; + + double dx = target.posX - headWorldX; + double dy = (target.posY + target.getEyeHeight()) - headWorldY; + double dz = target.posZ - headWorldZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= homingRange && dist > 0) { + // Calculate effective homing strength - increases when closer to commit to target + float effectiveStrength = homingStrength; + if (dist < homingRange * 0.3) { + effectiveStrength = Math.min(1.0f, homingStrength * 2.5f); + } else if (dist < homingRange * 0.6) { + effectiveStrength = Math.min(0.8f, homingStrength * 1.5f); + } + + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * effectiveStrength; + motionY += (desiredVY - motionY) * effectiveStrength; + motionZ += (desiredVZ - motionZ) * effectiveStrength; + + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void checkBlockCollision(double headX, double headY, double headZ) { + // Previous head world position + double prevHeadWorldX = startX + prevHeadOffsetX; + double prevHeadWorldY = startY + prevHeadOffsetY; + double prevHeadWorldZ = startZ + prevHeadOffsetZ; + + Vec3 currentPos = Vec3.createVectorHelper(prevHeadWorldX, prevHeadWorldY, prevHeadWorldZ); + Vec3 nextPos = Vec3.createVectorHelper(headX, headY, headZ); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + hasHit = true; + if (explosive) { + posX = blockHit.hitVec.xCoord; + posY = blockHit.hitVec.yCoord; + posZ = blockHit.hitVec.zCoord; + doExplosion(); + } + this.setDead(); + } + } + + private void checkEntityCollision(double headX, double headY, double headZ) { + double hitSize = headSize * 0.5; + AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( + headX - hitSize, headY - hitSize, headZ - hitSize, + headX + hitSize, headY + hitSize, headZ + hitSize + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + + if (DEBUG_LOGGING) { + LogWriter.info("[Beam] DEAD: Entity collision with " + entity.getClass().getSimpleName() + " at tick " + ticksExisted); + } + hasHit = true; + + if (explosive) { + posX = headX; + posY = headY; + posZ = headZ; + doExplosion(); + } else { + applyDamage(entity); + } + + this.setDead(); + return; + } + } + + @Override + protected boolean shouldIgnoreEntity(Entity entity) { + if (entity.getPersistentID().equals(siblingUUID)) return true; + return super.shouldIgnoreEntity(entity); + } + + // ==================== GETTERS FOR RENDERER ==================== + + public float getBeamWidth() { + return beamWidth; + } + + public float getHeadSize() { + return headSize; + } + + /** + * Get interpolated head offset for smooth rendering. + * Returns offset RELATIVE to origin. + */ + public double getInterpolatedHeadOffsetX(float partialTicks) { + return prevHeadOffsetX + (headOffsetX - prevHeadOffsetX) * partialTicks; + } + + public double getInterpolatedHeadOffsetY(float partialTicks) { + return prevHeadOffsetY + (headOffsetY - prevHeadOffsetY) * partialTicks; + } + + public double getInterpolatedHeadOffsetZ(float partialTicks) { + return prevHeadOffsetZ + (headOffsetZ - prevHeadOffsetZ) * partialTicks; + } + + /** + * Get trail points. These are RELATIVE to origin (startX/Y/Z). + */ + public List getTrailPoints() { + return trailPoints; + } + + public double getOriginX() { + return startX; + } + + public double getOriginY() { + return startY; + } + + public double getOriginZ() { + return startZ; + } + + /** + * Whether the tail orb should be rendered (only true in anchored mode). + */ + public boolean shouldRenderTailOrb() { + return renderTailOrb; + } + + /** + * Whether the beam is attached to its owner (anchored mode). + */ + public boolean isAttachedToOwner() { + return attachedToOwner; + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + + /** + * Whether trail should fade (comet effect for non-anchored beams). + */ + public boolean hasFadingTrail() { + return fadeTrail; + } + + /** + * Get trail point ages for fading calculation. + */ + public List getTrailPointAges() { + return trailPointAges; + } + + /** + * Get trail fade time in ticks. + */ + public int getTrailFadeTime() { + return trailFadeTime; + } + + public UUID getSiblingUUID() { + return siblingUUID; + } + + public void setSiblingUUID(UUID siblingUUID) { + if (siblingUUID == null) + return; + + if (this.siblingUUID == null) + this.siblingUUID = siblingUUID; + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("Speed") ? nbt.getFloat("Speed") : 0.5f; + this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); + this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; + this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; + this.beamWidth = nbt.hasKey("BeamWidth") ? nbt.getFloat("BeamWidth") : 0.3f; + this.headSize = nbt.hasKey("HeadSize") ? nbt.getFloat("HeadSize") : 0.5f; + this.headOffsetX = nbt.hasKey("HeadOffsetX") ? nbt.getDouble("HeadOffsetX") : 0; + this.headOffsetY = nbt.hasKey("HeadOffsetY") ? nbt.getDouble("HeadOffsetY") : 0; + this.headOffsetZ = nbt.hasKey("HeadOffsetZ") ? nbt.getDouble("HeadOffsetZ") : 0; + this.prevHeadOffsetX = this.headOffsetX; + this.prevHeadOffsetY = this.headOffsetY; + this.prevHeadOffsetZ = this.headOffsetZ; + this.attachedToOwner = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner"); + this.renderTailOrb = !nbt.hasKey("RenderTailOrb") || nbt.getBoolean("RenderTailOrb"); + // Read charging state and sync to data watcher + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f; + this.fadeTrail = nbt.hasKey("FadeTrail") && nbt.getBoolean("FadeTrail"); + this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20; + + this.anchorData.readNBT(nbt); + + // Read trail points (relative to origin) + trailPoints.clear(); + if (nbt.hasKey("Trail")) { + NBTTagList trailList = nbt.getTagList("Trail", 10); + for (int i = 0; i < trailList.tagCount(); i++) { + NBTTagCompound point = trailList.getCompoundTagAt(i); + trailPoints.add(Vec3.createVectorHelper( + point.getDouble("X"), + point.getDouble("Y"), + point.getDouble("Z") + )); + } + } + + this.siblingUUID = nbt.hasKey("SiblingUUID") ? UUID.fromString(nbt.getString("SiblingUUID")) : null; + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("Speed", speed); + nbt.setBoolean("Homing", homing); + nbt.setFloat("HomingStrength", homingStrength); + nbt.setFloat("HomingRange", homingRange); + nbt.setFloat("BeamWidth", beamWidth); + nbt.setFloat("HeadSize", headSize); + nbt.setDouble("HeadOffsetX", headOffsetX); + nbt.setDouble("HeadOffsetY", headOffsetY); + nbt.setDouble("HeadOffsetZ", headOffsetZ); + nbt.setBoolean("AttachedToOwner", attachedToOwner); + nbt.setBoolean("RenderTailOrb", renderTailOrb); + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); + nbt.setBoolean("FadeTrail", fadeTrail); + nbt.setInteger("TrailFadeTime", trailFadeTime); + + this.anchorData.writeNBT(nbt); + + // Write trail points + NBTTagList trailList = new NBTTagList(); + for (Vec3 point : trailPoints) { + NBTTagCompound pointNbt = new NBTTagCompound(); + pointNbt.setDouble("X", point.xCoord); + pointNbt.setDouble("Y", point.yCoord); + pointNbt.setDouble("Z", point.zCoord); + trailList.appendTag(pointNbt); + } + nbt.setTag("Trail", trailList); + + if (siblingUUID instanceof UUID) { + nbt.setString("SiblingUUID", siblingUUID.toString()); + } + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java new file mode 100644 index 000000000..04afb2224 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -0,0 +1,633 @@ +package kamkeel.npcs.entity; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; + +import java.util.List; +import java.util.UUID; + +/** + * Disc projectile - flat spinning disc with optional boomerang behavior. + * Has a wider, thinner hitbox compared to Orb. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityDisc extends EntityAbilityProjectile { + + // Disc-specific movement properties + private float speed = 0.5f; + private boolean homing = true; + private float homingStrength = 0.35f; // Increased from 0.15 for better tracking + private float homingRange = 20.0f; + + // Boomerang properties + private boolean boomerang = false; + private int boomerangDelay = 40; // Ticks before returning + private boolean returning = false; + private int ticksSinceMiss = 0; + + // Disc shape properties + private float discRadius = 1.0f; // Width of disc + private float discThickness = 0.2f; // Height of disc + + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private float targetDiscRadius = 1.0f; // Full radius to grow to during charging + private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging + private EnergyAnchorData anchorData = new EnergyAnchorData(); + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + + private UUID siblingUUID = null; + + public EntityAbilityDisc(World world) { + super(world); + } + + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * Check if disc is in charging state (synced via data watcher). + * In preview mode, uses local field since data watcher isn't synced. + */ + public boolean isCharging() { + if (previewMode) { + return this.charging; + } + return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + + /** + * Full constructor with all parameters using data classes. + */ + public EntityAbilityDisc(World world, EntityLivingBase owner, EntityLivingBase target, + double x, double y, double z, + float discRadius, float discThickness, + EnergyColorData color, EnergyCombatData combat, + EnergyHomingData homing, EnergyLightningData lightning, + EnergyLifespanData lifespan, + boolean boomerang, int boomerangDelay) { + super(world); + + // Initialize base properties + initProjectile(owner, target, x, y, z, discRadius, color, combat, lightning, lifespan); + + // Disc-specific properties from homing data + this.speed = homing.speed; + this.homing = homing.homing; + this.homingStrength = homing.homingStrength; + this.homingRange = homing.homingRange; + + this.boomerang = boomerang; + this.boomerangDelay = boomerangDelay; + + this.discRadius = discRadius; + this.discThickness = discThickness; + + // Calculate initial velocity toward target + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.getEyeHeight()) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.motionX = (dx / len) * speed; + this.motionY = (dy / len) * speed; + this.motionZ = (dz / len) * speed; + } + } else { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + this.motionY = -Math.sin(pitch) * speed; + this.motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + /** + * Setup this disc in charging mode (for windup phase). + * The disc will grow from 0 to discRadius over chargeDuration ticks. + * Position follows the owner based on anchor point. + */ + public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorData = anchor; + this.targetDiscRadius = this.discRadius; + this.targetDiscThickness = this.discThickness; + this.discRadius = 0.01f; + this.discThickness = 0.01f; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + /** + * Setup this disc in preview mode for GUI display. + * Follows anchor point and animations like in the real game. + * Can be fired when transitioning to active phase. + */ + public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); + + // Set visual properties + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.rotationSpeed = color.rotationSpeed; + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + + // Set charging state + this.setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorData = anchor; + + // Store target size and start at 0 for grow effect + this.targetDiscRadius = discRadius; + this.targetDiscThickness = discThickness; + this.discRadius = 0.01f; + this.discThickness = 0.01f; + this.size = 0.01f; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData); + this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + this.prevPosX = pos.xCoord; + this.prevPosY = pos.yCoord; + this.prevPosZ = pos.zCoord; + this.startX = pos.xCoord; + this.startY = pos.yCoord; + this.startZ = pos.zCoord; + + // Clear motion + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + /** + * Start preview firing (simulates firing toward a point in front of NPC). + */ + public void startPreviewFiring() { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Sync prev position to prevent visual jump on first frame + prevPosX = posX; + prevPosY = posY; + prevPosZ = posZ; + + // Fire forward based on owner facing direction + Entity owner = getOwner(); + if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = 0; // Fire horizontally + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } else { + motionX = speed; + motionY = 0; + motionZ = 0; + } + } + + /** + * Start the disc moving (exit charging mode). + * Called by ability when windup ends. + */ + public void startMoving(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Calculate velocity toward target + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - posX; + double dy = (target.posY + target.getEyeHeight()) - posY; + double dz = target.posZ - posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } else if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + @Override + protected void updateRotation() { + // Disc spins ONLY on Y axis (flat spin like a saw blade) + this.rotationValY += rotationSpeed; + // No wobble - keep X and Z at 0 for stable flight + if (this.rotationValY > 360.0f) this.rotationValY -= 360.0f; + } + + @Override + protected void updateProjectile() { + // Handle charging state (windup phase) + if (isCharging()) { + updateCharging(); + return; + } + + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + + if (worldObj.isRemote) { + handleClientInterpolation(); + } else { + // Server-side logic + if (returning) { + updateReturnToOwner(); + } else { + updateHoming(); + } + + checkBlockCollision(); + checkEntityCollision(); + this.moveEntity(motionX, motionY, motionZ); + + // Track ticks for boomerang delay (checkMaxDistance handles the actual return trigger) + if (boomerang && !returning && !hasHit) { + ticksSinceMiss++; + } + } + + // Check wall collision + if (this.isCollidedHorizontally || this.isCollidedVertically) { + if (!worldObj.isRemote) { + hasHit = true; + if (explosive) { + doExplosion(); + } + } + this.setDead(); + } + } + + @Override + protected boolean checkMaxDistance() { + double distTraveled = Math.sqrt( + (posX - startX) * (posX - startX) + + (posY - startY) * (posY - startY) + + (posZ - startZ) * (posZ - startZ) + ); + + if (boomerang) { + // If not returning yet, check if we should start returning + if (!returning && !hasHit) { + if (distTraveled >= maxDistance * 0.8 || ticksSinceMiss >= boomerangDelay) { + returning = true; + } + } + + // If returning, only die when close to owner + if (returning) { + Entity owner = getOwner(); + if (owner != null) { + double distToOwner = Math.sqrt( + (posX - owner.posX) * (posX - owner.posX) + + (posY - owner.posY) * (posY - owner.posY) + + (posZ - owner.posZ) * (posZ - owner.posZ) + ); + return distToOwner < 1.5; + } + // Owner gone but returning - keep going toward last known position + return false; + } + + // Not returning yet - don't die from distance (we'll trigger return above) + return false; + } + + // Non-boomerang: standard max distance check + return distTraveled >= maxDistance; + } + + /** + * Update movement in preview mode (client-side only, no damage). + */ + private void updatePreviewMovement() { + // Simple movement - just apply motion + this.posX += motionX; + this.posY += motionY; + this.posZ += motionZ; + } + + private void updateHoming() { + if (!homing) return; + + Entity target = getTarget(); + if (target == null || !target.isEntityAlive()) return; + + double dx = target.posX - posX; + double dy = (target.posY + target.getEyeHeight()) - posY; + double dz = target.posZ - posZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= homingRange && dist > 0) { + // Calculate effective homing strength - increases when closer to commit to target + float effectiveStrength = homingStrength; + if (dist < homingRange * 0.3) { + effectiveStrength = Math.min(1.0f, homingStrength * 2.5f); + } else if (dist < homingRange * 0.6) { + effectiveStrength = Math.min(0.8f, homingStrength * 1.5f); + } + + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * effectiveStrength; + motionY += (desiredVY - motionY) * effectiveStrength; + motionZ += (desiredVZ - motionZ) * effectiveStrength; + + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void updateReturnToOwner() { + Entity owner = getOwner(); + + // If owner is gone (unloaded/dead), head back toward start position + double targetX, targetY, targetZ; + if (owner != null) { + targetX = owner.posX; + targetY = owner.posY + owner.height * 0.5; + targetZ = owner.posZ; + } else { + // Return to start position if owner not available + targetX = startX; + targetY = startY; + targetZ = startZ; + } + + double dx = targetX - posX; + double dy = targetY - posY; + double dz = targetZ - posZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist > 0) { + // Stronger homing when returning + double returnStrength = Math.min(1.0, homingStrength * 2.5); + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * returnStrength; + motionY += (desiredVY - motionY) * returnStrength; + motionZ += (desiredVZ - motionZ) * returnStrength; + + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void checkBlockCollision() { + Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ); + Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + hasHit = true; + if (explosive) { + posX = blockHit.hitVec.xCoord; + posY = blockHit.hitVec.yCoord; + posZ = blockHit.hitVec.zCoord; + doExplosion(); + } + this.setDead(); + } + } + + private void checkEntityCollision() { + // Disc hitbox: wider but thinner + double halfWidth = discRadius * 0.5; + double halfHeight = discThickness * 0.5; + AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( + posX - halfWidth, posY - halfHeight, posZ - halfWidth, + posX + halfWidth, posY + halfHeight, posZ + halfWidth + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + + hasHit = true; + + if (explosive) { + doExplosion(); + } else { + applyDamage(entity); + } + + this.setDead(); + return; + } + } + + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Grow size based on charge progress + float progress = getChargeProgress(); + this.discRadius = targetDiscRadius * progress; + this.discThickness = targetDiscThickness * progress; + this.size = this.discRadius; // Base size for interpolation + + // Calculate position based on anchor point, offset downward by half radius to center + if (owner instanceof EntityLivingBase) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + } + + // Update rotation during charging (disc still spins) + updateRotation(); + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + + // ==================== GETTERS ==================== + + public float getSpeed() { + return speed; + } + + public boolean isHoming() { + return homing; + } + + public float getDiscRadius() { + return discRadius; + } + + public float getDiscThickness() { + return discThickness; + } + + public boolean isBoomerang() { + return boomerang; + } + + public boolean isReturning() { + return returning; + } + + public UUID getSiblingUUID() { + return siblingUUID; + } + + public void setSiblingUUID(UUID siblingUUID) { + if (siblingUUID == null) + return; + + if (this.siblingUUID == null) + this.siblingUUID = siblingUUID; + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("Speed") ? nbt.getFloat("Speed") : 0.5f; + this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); + this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; + this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; + this.boomerang = nbt.hasKey("Boomerang") && nbt.getBoolean("Boomerang"); + this.boomerangDelay = nbt.hasKey("BoomerangDelay") ? nbt.getInteger("BoomerangDelay") : 40; + this.discRadius = nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f; + this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f; + this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning"); + // Charging state + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.targetDiscRadius = nbt.hasKey("TargetDiscRadius") ? nbt.getFloat("TargetDiscRadius") : this.discRadius; + this.targetDiscThickness = nbt.hasKey("TargetDiscThickness") ? nbt.getFloat("TargetDiscThickness") : this.discThickness; + this.anchorData.readNBT(nbt); + this.siblingUUID = nbt.hasKey("SiblingUUID") ? UUID.fromString(nbt.getString("SiblingUUID")) : null; + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("Speed", speed); + nbt.setBoolean("Homing", homing); + nbt.setFloat("HomingStrength", homingStrength); + nbt.setFloat("HomingRange", homingRange); + nbt.setBoolean("Boomerang", boomerang); + nbt.setInteger("BoomerangDelay", boomerangDelay); + nbt.setFloat("DiscRadius", discRadius); + nbt.setFloat("DiscThickness", discThickness); + nbt.setBoolean("Returning", returning); + // Charging state + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setFloat("TargetDiscRadius", targetDiscRadius); + nbt.setFloat("TargetDiscThickness", targetDiscThickness); + + // Anchor data + this.anchorData.writeNBT(nbt); + + if (siblingUUID instanceof UUID) { + nbt.setString("SiblingUUID", siblingUUID.toString()); + } + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java new file mode 100644 index 000000000..e93ea5a77 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -0,0 +1,525 @@ +package kamkeel.npcs.entity; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Laser projectile - fast expanding thin line that pierces through multiple targets. + * No homing, travels in a straight line from origin to max distance. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityLaser extends EntityAbilityProjectile { + + // Laser-specific properties + private float laserWidth = 0.2f; + private float expansionSpeed = 2.0f; // Blocks per tick + private int lingerTicks = 10; // How long laser stays visible after reaching max + + // State + private float currentLength = 0.0f; + private boolean fullyExtended = false; + private int ticksSinceFullExtension = 0; + + // Direction (normalized) + private double dirX, dirY, dirZ; + + // Track hit entities to avoid double-damage + private Set hitEntities = new HashSet<>(); + + // End point for rendering + private double endX, endY, endZ; + + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); + private float targetSize = 1.0f; // Full size to grow to during charging + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + + public EntityAbilityLaser(World world) { + super(world); + } + + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * Check if orb is in charging state (synced via data watcher). + * In preview mode, uses local field since data watcher isn't synced. + */ + public boolean isCharging() { + if (previewMode) { + return this.charging; + } + return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + + /** + * Full constructor with all parameters using data classes. + */ + public EntityAbilityLaser(World world, EntityLivingBase owner, EntityLivingBase target, + double x, double y, double z, + float laserWidth, + EnergyColorData color, EnergyCombatData combat, + EnergyLightningData lightning, EnergyLifespanData lifespan, + float expansionSpeed, int lingerTicks) { + super(world); + + // Initialize base properties (laser doesn't rotate) + initProjectile(owner, target, x, y, z, laserWidth, color, combat, lightning, lifespan); + this.rotationSpeed = 0.0f; + + // Laser-specific properties + this.laserWidth = laserWidth; + this.expansionSpeed = expansionSpeed; + this.lingerTicks = lingerTicks; + + // Calculate direction toward target or forward + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.getEyeHeight() - 0.4) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.dirX = dx / len; + this.dirY = dy / len; + this.dirZ = dz / len; + } + } else { + // 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); + } + + // Initialize end point + this.endX = x; + this.endY = y; + this.endZ = z; + } + + /** + * Setup this orb in charging mode (for windup phase). + * The orb will grow from 0 to orbSize over chargeDuration ticks. + * Position follows the owner based on anchor point. + */ + public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorData = anchor; + this.targetSize = this.size; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + @Override + protected void updateRotation() { + // Laser doesn't rotate + } + + @Override + protected boolean checkMaxDistance() { + // Laser handles distance differently - it expands to max then lingers + return false; + } + + @Override + protected void updateProjectile() { + if (isCharging()) { + updateCharging(); + return; + } + + if (!fullyExtended) { + // Expand the laser + currentLength += expansionSpeed; + + if (currentLength >= maxDistance) { + currentLength = maxDistance; + fullyExtended = true; + } + + // Update end point + endX = startX + dirX * currentLength; + endY = startY + dirY * currentLength; + endZ = startZ + dirZ * currentLength; + + // Check for block collision along the new segment + if (!worldObj.isRemote) { + checkBlockCollision(); + checkEntityCollisionAlongLine(); + } + } else { + // Laser is fully extended, count down linger time + ticksSinceFullExtension++; + if (ticksSinceFullExtension >= lingerTicks) { + this.setDead(); + } + } + } + + public void startMoving(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Calculate velocity toward target + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - startX; + double dy = (target.posY + target.getEyeHeight() - 0.4) - startY; + double dz = target.posZ - startZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.dirX = dx / len; + 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); + + // Initialize end point + this.endX = posX; + this.endY = posY; + this.endZ = posZ; + } + + private void checkBlockCollision() { + Vec3 start = Vec3.createVectorHelper(startX, startY, startZ); + Vec3 end = Vec3.createVectorHelper(endX, endY, endZ); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(start, end, false, true, false); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + // Laser stops at block + currentLength = (float) Math.sqrt( + (blockHit.hitVec.xCoord - startX) * (blockHit.hitVec.xCoord - startX) + + (blockHit.hitVec.yCoord - startY) * (blockHit.hitVec.yCoord - startY) + + (blockHit.hitVec.zCoord - startZ) * (blockHit.hitVec.zCoord - startZ) + ); + endX = blockHit.hitVec.xCoord; + endY = blockHit.hitVec.yCoord; + endZ = blockHit.hitVec.zCoord; + fullyExtended = true; + + if (explosive) { + // Explode at hit point + double oldPosX = posX; + double oldPosY = posY; + double oldPosZ = posZ; + posX = endX; + posY = endY; + posZ = endZ; + doExplosion(); + posX = oldPosX; + posY = oldPosY; + posZ = oldPosZ; + } + } + } + + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Grow size based on charge progress + float progress = getChargeProgress(); + this.size = targetSize * progress; + + // Calculate position based on anchor point, offset downward by half size to center + if (owner instanceof EntityLivingBase) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + } + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + + private void checkEntityCollisionAlongLine() { + // Check collision along the entire laser line + // Use a slightly expanded AABB for the full line + double minX = Math.min(startX, endX) - laserWidth; + double minY = Math.min(startY, endY) - laserWidth; + double minZ = Math.min(startZ, endZ) - laserWidth; + double maxX = Math.max(startX, endX) + laserWidth; + double maxY = Math.max(startY, endY) + laserWidth; + double maxZ = Math.max(startZ, endZ) + laserWidth; + + AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + if (hitEntities.contains(entity.getEntityId())) continue; + + // Check if entity is close to the laser line + if (isEntityOnLine(entity)) { + hitEntities.add(entity.getEntityId()); + applyDamage(entity); + + // Piercing - don't stop, continue to next entity + } + } + } + + /** + * Check if an entity intersects with the laser line. + */ + private boolean isEntityOnLine(EntityLivingBase entity) { + // Get entity center + double ex = entity.posX; + double ey = entity.posY + entity.height * 0.5; + double ez = entity.posZ; + + // Vector from start to entity + double vx = ex - startX; + double vy = ey - startY; + double vz = ez - startZ; + + // Project onto line direction + double dot = vx * dirX + vy * dirY + vz * dirZ; + + // Clamp to line segment + dot = Math.max(0, Math.min(dot, currentLength)); + + // Closest point on line + double closestX = startX + dirX * dot; + double closestY = startY + dirY * dot; + double closestZ = startZ + dirZ * dot; + + // Distance from entity to closest point + double distSq = (ex - closestX) * (ex - closestX) + + (ey - closestY) * (ey - closestY) + + (ez - closestZ) * (ez - closestZ); + + // Hit if within laser width + entity radius + double hitRadius = laserWidth * 0.5 + entity.width * 0.5; + return distSq <= hitRadius * hitRadius; + } + + // ==================== GETTERS FOR RENDERER ==================== + + public float getLaserWidth() { + return laserWidth; + } + + public float getCurrentLength() { + return currentLength; + } + + public boolean isFullyExtended() { + return fullyExtended; + } + + public double getStartX() { + return startX; + } + + public double getStartY() { + return startY; + } + + public double getStartZ() { + return startZ; + } + + public double getEndX() { + return endX; + } + + public double getEndY() { + return endY; + } + + public double getEndZ() { + return endZ; + } + + public double getDirX() { + return dirX; + } + + public double getDirY() { + return dirY; + } + + public double getDirZ() { + return dirZ; + } + + /** + * Get the alpha for fade-out effect during linger. + */ + public float getLingerAlpha() { + if (!fullyExtended) return 1.0f; + return 1.0f - ((float) ticksSinceFullExtension / lingerTicks); + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.laserWidth = nbt.hasKey("LaserWidth") ? nbt.getFloat("LaserWidth") : 0.2f; + this.expansionSpeed = nbt.hasKey("ExpansionSpeed") ? nbt.getFloat("ExpansionSpeed") : 2.0f; + this.lingerTicks = nbt.hasKey("LingerTicks") ? nbt.getInteger("LingerTicks") : 10; + this.dirX = nbt.getDouble("DirX"); + this.dirY = nbt.getDouble("DirY"); + this.dirZ = nbt.getDouble("DirZ"); + this.currentLength = nbt.getFloat("CurrentLength"); + this.fullyExtended = nbt.getBoolean("FullyExtended"); + this.endX = nbt.getDouble("EndX"); + this.endY = nbt.getDouble("EndY"); + this.endZ = nbt.getDouble("EndZ"); + + // Charging state + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size; + anchorData.readNBT(nbt); + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("LaserWidth", laserWidth); + nbt.setFloat("ExpansionSpeed", expansionSpeed); + nbt.setInteger("LingerTicks", lingerTicks); + nbt.setDouble("DirX", dirX); + nbt.setDouble("DirY", dirY); + nbt.setDouble("DirZ", dirZ); + nbt.setFloat("CurrentLength", currentLength); + nbt.setBoolean("FullyExtended", fullyExtended); + nbt.setDouble("EndX", endX); + nbt.setDouble("EndY", endY); + nbt.setDouble("EndZ", endZ); + + // Charging state + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setFloat("TargetSize", targetSize); + anchorData.writeNBT(nbt); + } + + /** + * Setup this laser in preview mode for GUI display. + * Laser doesn't have charging state - spawns at active phase and fires immediately. + */ + public void setupPreview(EntityLivingBase owner, float laserWidth, EnergyColorData color, + EnergyLightningData lightning, float expansionSpeed, float maxDistance) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); + + // Set visual properties + this.laserWidth = laserWidth; + this.size = laserWidth; + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.expansionSpeed = expansionSpeed; + this.maxDistance = Math.min(maxDistance, 5.0f); // Limit for GUI preview + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + + // Position at chest height + double x = owner.posX; + double y = owner.posY + owner.height * 0.7; + double z = owner.posZ; + this.setPosition(x, y, z); + this.prevPosX = x; + this.prevPosY = y; + this.prevPosZ = z; + this.startX = x; + this.startY = y; + this.startZ = z; + + // Fire in owner's facing direction + float yaw = (float) Math.toRadians(owner.rotationYaw); + this.dirX = -Math.sin(yaw); + this.dirY = 0; + this.dirZ = Math.cos(yaw); + + // Initialize end point (same as start, will expand) + this.endX = x; + this.endY = y; + this.endZ = z; + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java new file mode 100644 index 000000000..3c5895206 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -0,0 +1,497 @@ +package kamkeel.npcs.entity; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; + +import java.util.List; +import java.util.UUID; + +/** + * Orb projectile - spherical homing energy ball. + * Extends EntityAbilityProjectile for shared functionality. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityOrb extends EntityAbilityProjectile { + + // Orb-specific movement properties + private float speed = 0.5f; + private boolean homing = true; + private float homingStrength = 0.35f; // Increased from 0.15 for better tracking + private float homingRange = 20.0f; + + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); + private float targetSize = 1.0f; // Full size to grow to during charging + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + + private UUID siblingUUID = null; + + public EntityAbilityOrb(World world) { + super(world); + } + + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * Check if orb is in charging state (synced via data watcher). + * In preview mode, uses local field since data watcher isn't synced. + */ + public boolean isCharging() { + if (previewMode) { + return this.charging; + } + return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + + /** + * Full constructor using data classes for grouped parameters. + */ + public EntityAbilityOrb(World world, EntityLivingBase owner, EntityLivingBase target, + double x, double y, double z, float orbSize, + EnergyColorData color, EnergyCombatData combat, + EnergyHomingData homing, EnergyLightningData lightning, + EnergyLifespanData lifespan) { + super(world); + + // Initialize base properties + initProjectile(owner, target, x, y, z, orbSize, color, combat, lightning, lifespan); + + // Orb-specific properties + this.speed = homing.speed; + this.homing = homing.homing; + this.homingStrength = homing.homingStrength; + this.homingRange = homing.homingRange; + + // Calculate initial velocity toward target + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.getEyeHeight()) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.motionX = (dx / len) * homing.speed; + this.motionY = (dy / len) * homing.speed; + this.motionZ = (dz / len) * homing.speed; + } + } else { + // Fire in NPC's facing direction + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.motionX = -Math.sin(yaw) * Math.cos(pitch) * homing.speed; + this.motionY = -Math.sin(pitch) * homing.speed; + this.motionZ = Math.cos(yaw) * Math.cos(pitch) * homing.speed; + } + } + + /** + * Setup this orb in charging mode (for windup phase). + * The orb will grow from 0 to orbSize over chargeDuration ticks. + * Position follows the owner based on anchor point. + */ + public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorData = anchor; + this.targetSize = this.size; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + /** + * Setup this orb in preview mode for GUI display. + * Follows anchor point and animations like in the real game. + * Can be fired when transitioning to active phase. + */ + public void setupPreview(EntityLivingBase owner, float orbSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); + + // Set visual properties + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.rotationSpeed = color.rotationSpeed; + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + this.lightningFadeTime = lightning.lightningFadeTime; + + // Set charging state + this.setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorData = anchor; + + // Store target size and start at 0 for grow effect + this.targetSize = orbSize; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData); + this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + this.prevPosX = pos.xCoord; + this.prevPosY = pos.yCoord; + this.prevPosZ = pos.zCoord; + this.startX = pos.xCoord; + this.startY = pos.yCoord; + this.startZ = pos.zCoord; + + // Clear motion + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + + /** + * Start preview firing (simulates firing toward a point in front of NPC). + */ + public void startPreviewFiring() { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Sync prev position to prevent visual jump on first frame + prevPosX = posX; + prevPosY = posY; + prevPosZ = posZ; + + // Fire forward based on owner facing direction + Entity owner = getOwner(); + if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(0); // Fire horizontally + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } else { + // Default: fire forward (positive X in model space) + motionX = speed; + motionY = 0; + motionZ = 0; + } + } + + /** + * Start the orb moving (exit charging mode). + * Called by ability when windup ends. + */ + public void startMoving(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Calculate velocity toward target + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - posX; + double dy = (target.posY + target.getEyeHeight()) - posY; + double dz = target.posZ - posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } else if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + @Override + protected void updateProjectile() { + // Handle charging state (windup phase) + if (isCharging()) { + updateCharging(); + return; + } + + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + + if (worldObj.isRemote) { + handleClientInterpolation(); + } else { + // Server-side logic + updateHoming(); + checkBlockCollision(); + checkEntityCollision(); + this.moveEntity(motionX, motionY, motionZ); + } + + // Check wall collision + if (this.isCollidedHorizontally || this.isCollidedVertically) { + if (!worldObj.isRemote) { + hasHit = true; + if (explosive) { + doExplosion(); + } + } + this.setDead(); + } + } + + /** + * Update movement in preview mode (client-side only, no damage). + */ + private void updatePreviewMovement() { + // Simple movement - just apply motion + this.posX += motionX; + this.posY += motionY; + this.posZ += motionZ; + } + + private void updateHoming() { + if (!homing) return; + + Entity target = getTarget(); + if (target == null || !target.isEntityAlive()) return; + + double dx = target.posX - posX; + double dy = (target.posY + target.getEyeHeight()) - posY; + double dz = target.posZ - posZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= homingRange && dist > 0) { + // Calculate effective homing strength - increases when closer to commit to target + // This prevents orbiting behavior at close range + float effectiveStrength = homingStrength; + if (dist < homingRange * 0.3) { + // Within 30% of homing range, dramatically increase strength to commit + effectiveStrength = Math.min(1.0f, homingStrength * 2.5f); + } else if (dist < homingRange * 0.6) { + // Within 60% of homing range, moderately increase strength + effectiveStrength = Math.min(0.8f, homingStrength * 1.5f); + } + + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * effectiveStrength; + motionY += (desiredVY - motionY) * effectiveStrength; + motionZ += (desiredVZ - motionZ) * effectiveStrength; + + // Normalize to maintain speed + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void checkBlockCollision() { + Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ); + Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + hasHit = true; + if (explosive) { + posX = blockHit.hitVec.xCoord; + posY = blockHit.hitVec.yCoord; + posZ = blockHit.hitVec.zCoord; + doExplosion(); + } + this.setDead(); + } + } + + private void checkEntityCollision() { + double hitSize = size * 0.5; + AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( + posX - hitSize, posY - hitSize, posZ - hitSize, + posX + hitSize, posY + hitSize, posZ + hitSize + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + + hasHit = true; + + if (explosive) { + doExplosion(); + } else { + applyDamage(entity); + } + + this.setDead(); + return; + } + } + + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Grow size based on charge progress + float progress = getChargeProgress(); + this.size = targetSize * progress; + + // Calculate position based on anchor point, offset downward by half size to center + if (owner instanceof EntityLivingBase) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + } + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + + // ==================== GETTERS ==================== + + public float getSpeed() { + return speed; + } + + public boolean isHoming() { + return homing; + } + + public float getHomingStrength() { + return homingStrength; + } + + public float getHomingRange() { + return homingRange; + } + + // Legacy getter for renderer compatibility + public float getOrbSize() { + return size; + } + + public UUID getSiblingUUID() { + return siblingUUID; + } + + public void setSiblingUUID(UUID siblingUUID) { + if (siblingUUID == null) + return; + + if (this.siblingUUID == null) + this.siblingUUID = siblingUUID; + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("OrbSpeed") ? nbt.getFloat("OrbSpeed") : 0.5f; + this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); + this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; + this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; + // Charging state + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size; + // Anchor data + this.anchorData.readNBT(nbt); + + this.siblingUUID = nbt.hasKey("SiblingUUID") ? UUID.fromString(nbt.getString("SiblingUUID")) : null; + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("OrbSpeed", speed); + nbt.setBoolean("Homing", homing); + nbt.setFloat("HomingStrength", homingStrength); + nbt.setFloat("HomingRange", homingRange); + // Charging state + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + anchorData.writeNBT(nbt); + nbt.setFloat("TargetSize", targetSize); + + if (siblingUUID instanceof UUID) { + nbt.setString("SiblingUUID", siblingUUID.toString()); + } + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java new file mode 100644 index 000000000..57f99e1c8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -0,0 +1,687 @@ +package kamkeel.npcs.entity; + +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.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.NpcDamageSource; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base class for all ability projectiles (Orb, Disc, Beam, Laser). + * Provides common functionality for visuals, combat, effects, and interpolation. + * + * Design inspired by LouisXIV's energy attack system. + */ +public abstract class EntityAbilityProjectile extends Entity implements IEntityAdditionalSpawnData { + + // ==================== VISUAL PROPERTIES ==================== + protected float size = 1.0f; + protected int innerColor = 0xFFFFFF; + protected int outerColor = 0x8888FF; + protected boolean outerColorEnabled = true; + protected float outerColorWidth = 0.4f; // Additive offset from inner size + protected float outerColorAlpha = 0.5f; + protected float rotationSpeed = 4.0f; + + // ==================== COMBAT PROPERTIES ==================== + protected float damage = 7.0f; + protected float knockback = 1.0f; + protected float knockbackUp = 0.1f; + + // ==================== EFFECT PROPERTIES ==================== + protected List effects = new ArrayList<>(); + + // ==================== EXPLOSION PROPERTIES ==================== + protected boolean explosive = false; + protected float explosionRadius = 3.0f; + protected float explosionDamageFalloff = 0.5f; + + // ==================== LIGHTNING EFFECT PROPERTIES ==================== + protected boolean lightningEffect = false; + protected float lightningDensity = 0.15f; // Bolts spawned per tick (0.15 = ~15% chance per frame) + protected float lightningRadius = 0.5f; // Max distance lightning can arc from center + protected int lightningFadeTime = 6; // Ticks before lightning fades out + + // Client-side lightning state (not saved to NBT) + @cpw.mods.fml.relauncher.SideOnly(cpw.mods.fml.relauncher.Side.CLIENT) + public transient Object lightningState; // Actually AttachedLightningRenderer.LightningState + + // ==================== LIFESPAN ==================== + protected float maxDistance = 30.0f; + protected int maxLifetime = 200; + protected long deathWorldTime = -1; // World time when entity should die (-1 = not set) + + // ==================== TRACKING ==================== + protected double startX, startY, startZ; + protected int ownerEntityId = -1; + protected int targetEntityId = -1; + + // ==================== STATE ==================== + protected boolean hasHit = false; + protected boolean previewMode = false; // Client-side preview mode (no damage/effects) + protected EntityLivingBase previewOwner = null; // Direct reference for GUI preview (no world lookup) + + // ==================== ROTATION INTERPOLATION ==================== + public float prevRotationValX, prevRotationValY, prevRotationValZ; + public float rotationValX, rotationValY, rotationValZ; + + // ==================== SIZE INTERPOLATION ==================== + protected float renderCurrentSize; + protected float prevRenderSize; + + // ==================== POSITION INTERPOLATION ==================== + protected double interpTargetX, interpTargetY, interpTargetZ; + protected double interpTargetMotionX, interpTargetMotionY, interpTargetMotionZ; + protected int interpSteps; + + public EntityAbilityProjectile(World world) { + super(world); + this.setSize(0.5f, 0.5f); + this.noClip = false; + this.isImmuneToFire = true; + this.ignoreFrustumCheck = true; + } + + /** + * Initialize common properties using data classes. Call from subclass constructors. + */ + protected void initProjectile(EntityLivingBase owner, EntityLivingBase target, + double x, double y, double z, float size, + EnergyColorData color, EnergyCombatData combat, + EnergyLightningData lightning, EnergyLifespanData lifespan) { + this.setPosition(x, y, z); + this.startX = x; + this.startY = y; + this.startZ = z; + + this.ownerEntityId = owner.getEntityId(); + this.targetEntityId = target != null ? target.getEntityId() : -1; + + // Visual + this.size = size; + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.outerColorAlpha = color.outerColorAlpha; + this.rotationSpeed = color.rotationSpeed; + + // Combat + this.damage = combat.damage; + this.knockback = combat.knockback; + this.knockbackUp = combat.knockbackUp; + + // Explosion + this.explosive = combat.explosive; + this.explosionRadius = combat.explosionRadius; + this.explosionDamageFalloff = combat.explosionDamageFalloff; + + // Lifespan + this.maxDistance = lifespan.maxDistance; + this.maxLifetime = lifespan.maxLifetime; + // deathWorldTime will be set on first tick when world is available + + // Lightning + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + this.lightningFadeTime = lightning.lightningFadeTime; + + // Initialize render size + this.renderCurrentSize = size; + this.prevRenderSize = size; + } + + @Override + protected void entityInit() { + // No DataWatcher needed - using IEntityAdditionalSpawnData with NBT + } + + @Override + @SideOnly(Side.CLIENT) + public boolean isInRangeToRenderDist(double distance) { + double d1 = this.boundingBox.getAverageEdgeLength() * 4.0D; + d1 *= 128.0D; + return distance < d1 * d1; + } + + @Override + public boolean shouldRenderInPass(int pass) { + return pass == 1; // Translucent pass + } + + @Override + public void onUpdate() { + // Store previous values for interpolation + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + this.prevRotationValX = this.rotationValX; + this.prevRotationValY = this.rotationValY; + this.prevRotationValZ = this.rotationValZ; + this.prevRenderSize = this.renderCurrentSize; + + // Skip super.onUpdate() in preview mode to avoid world checks + if (!previewMode) { + super.onUpdate(); + } + + // Update rotation + updateRotation(); + + // Lerp render size toward actual size + this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f; + + // Skip lifetime/distance checks in preview mode + if (!previewMode) { + // Set death time on first tick if not already set (handles chunk load/unload) + if (deathWorldTime < 0 && worldObj != null) { + deathWorldTime = worldObj.getTotalWorldTime() + maxLifetime; + } + + // Check lifespan using world time (survives chunk unload/reload) + if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { + this.setDead(); + return; + } + + // Check max distance (subclass can override if needed) + if (checkMaxDistance()) { + this.setDead(); + return; + } + + if (hasHit) { + this.setDead(); + return; + } + } + + // Subclass-specific update + updateProjectile(); + } + + /** + * Update rotation values. Override for custom rotation behavior. + */ + protected void updateRotation() { + this.rotationValX += rotationSpeed * 0.7f; + this.rotationValY += rotationSpeed; + this.rotationValZ += rotationSpeed * 0.5f; + + if (this.rotationValX > 360.0f) this.rotationValX -= 360.0f; + if (this.rotationValY > 360.0f) this.rotationValY -= 360.0f; + if (this.rotationValZ > 360.0f) this.rotationValZ -= 360.0f; + } + + /** + * Check if projectile has exceeded max distance. + * @return true if should die + */ + protected boolean checkMaxDistance() { + double distTraveled = Math.sqrt( + (posX - startX) * (posX - startX) + + (posY - startY) * (posY - startY) + + (posZ - startZ) * (posZ - startZ) + ); + return distTraveled >= maxDistance; + } + + /** + * Subclass-specific update logic. Called every tick. + */ + protected abstract void updateProjectile(); + + // ==================== POSITION INTERPOLATION ==================== + + @Override + public void setPositionAndRotation2(double x, double y, double z, float yaw, float pitch, int posRotationIncrements) { + this.interpTargetX = x; + this.interpTargetY = y; + this.interpTargetZ = z; + this.interpSteps = posRotationIncrements; + } + + @Override + public void setVelocity(double motionX, double motionY, double motionZ) { + this.interpTargetMotionX = motionX; + this.interpTargetMotionY = motionY; + this.interpTargetMotionZ = motionZ; + this.motionX = motionX; + this.motionY = motionY; + this.motionZ = motionZ; + } + + /** + * Handle client-side position interpolation. + */ + protected void handleClientInterpolation() { + if (this.interpSteps > 0) { + double newX = this.posX + (this.interpTargetX - this.posX) / this.interpSteps; + double newY = this.posY + (this.interpTargetY - this.posY) / this.interpSteps; + double newZ = this.posZ + (this.interpTargetZ - this.posZ) / this.interpSteps; + + this.motionX = this.motionX + (this.interpTargetMotionX - this.motionX) / this.interpSteps; + this.motionY = this.motionY + (this.interpTargetMotionY - this.motionY) / this.interpSteps; + this.motionZ = this.motionZ + (this.interpTargetMotionZ - this.motionZ) / this.interpSteps; + + this.setPosition(newX, newY, newZ); + this.interpSteps--; + } else { + this.posX += this.motionX; + this.posY += this.motionY; + this.posZ += this.motionZ; + this.setPosition(this.posX, this.posY, this.posZ); + } + } + + // ==================== PREVIEW MODE ==================== + + /** + * Set preview mode for GUI display. + * In preview mode, no damage or world effects are applied. + */ + public void setPreviewMode(boolean preview) { + this.previewMode = preview; + } + + /** + * Check if entity is in preview mode. + */ + public boolean isPreviewMode() { + return previewMode; + } + + // ==================== DAMAGE & EFFECTS ==================== + + protected void applyDamage(EntityLivingBase target) { + if (previewMode) return; // Skip damage in preview mode + applyDamage(target, this.damage, this.knockback); + } + + protected void applyDamage(EntityLivingBase target, float dmg, float kb) { + if (previewMode) return; // Skip damage in preview mode + Entity owner = getOwner(); + + 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) { + double dx = target.posX - posX; + double dz = target.posZ - posZ; + double len = Math.sqrt(dx * dx + dz * dz); + if (len > 0) { + double kbX = (dx / len) * kb * 0.5; + double kbZ = (dz / len) * kb * 0.5; + double kbY = knockbackUp > 0 ? knockbackUp : 0.1; + target.addVelocity(kbX, kbY, kbZ); + target.velocityChanged = true; + } + } + + applyEffects(target); + } + + protected void applyEffects(EntityLivingBase target) { + for (AbilityEffect effect : effects) { + effect.apply(target); + } + } + + /** + * Set the effects list from the ability's configured effects. + */ + public void setEffects(List effects) { + this.effects = effects != null ? effects : new ArrayList<>(); + } + + protected void doExplosion() { + if (previewMode) return; // Skip explosion in preview mode + worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f); + + Entity owner = getOwner(); + + AxisAlignedBB explosionBox = AxisAlignedBB.getBoundingBox( + posX - explosionRadius, posY - explosionRadius, posZ - explosionRadius, + posX + explosionRadius, posY + explosionRadius, posZ + explosionRadius + ); + + @SuppressWarnings("unchecked") + List targets = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, explosionBox); + + for (EntityLivingBase target : targets) { + if (target == owner) continue; + + double dist = Math.sqrt( + Math.pow(target.posX - posX, 2) + + Math.pow(target.posY - posY, 2) + + Math.pow(target.posZ - posZ, 2) + ); + + if (dist <= explosionRadius) { + float falloff = 1.0f - (float) (dist / explosionRadius) * explosionDamageFalloff; + applyDamage(target, damage * falloff, knockback * falloff); + } + } + } + + // ==================== ENTITY HELPERS ==================== + + protected Entity getOwner() { + // In preview mode, use direct reference (no world lookup) + if (previewMode && previewOwner != null) { + return previewOwner; + } + if (ownerEntityId == -1) return null; + return worldObj.getEntityByID(ownerEntityId); + } + + /** + * Set the preview owner for GUI preview mode. + * This allows anchor point calculations without world entity lookup. + */ + public void setPreviewOwner(EntityLivingBase owner) { + this.previewOwner = owner; + } + + protected Entity getTarget() { + if (targetEntityId == -1) return null; + return worldObj.getEntityByID(targetEntityId); + } + + /** + * Check if an entity should be ignored for collision. + */ + protected boolean shouldIgnoreEntity(Entity entity) { + Entity owner = getOwner(); + if (entity == owner) return true; + + if (owner instanceof EntityNPCInterface && entity instanceof EntityNPCInterface) { + EntityNPCInterface ownerNpc = (EntityNPCInterface) owner; + EntityNPCInterface targetNpc = (EntityNPCInterface) entity; + if (ownerNpc.faction.id == targetNpc.faction.id) return true; + } + return false; + } + + // ==================== VISUAL GETTERS ==================== + + public float getSize() { + return size; + } + + public int getInnerColor() { + return innerColor; + } + + public int getOuterColor() { + return outerColor; + } + + public boolean isOuterColorEnabled() { + return outerColorEnabled; + } + + public float getOuterColorWidth() { + return outerColorWidth; + } + + public float getOuterColorAlpha() { + return outerColorAlpha; + } + + public float getRotationSpeed() { + return rotationSpeed; + } + + public float getInterpolatedRotationX(float partialTicks) { + return this.prevRotationValX + (this.rotationValX - this.prevRotationValX) * partialTicks; + } + + public float getInterpolatedRotationY(float partialTicks) { + return this.prevRotationValY + (this.rotationValY - this.prevRotationValY) * partialTicks; + } + + public float getInterpolatedRotationZ(float partialTicks) { + return this.prevRotationValZ + (this.rotationValZ - this.prevRotationValZ) * partialTicks; + } + + public float getInterpolatedSize(float partialTicks) { + return this.prevRenderSize + (this.renderCurrentSize - this.prevRenderSize) * partialTicks; + } + + // ==================== LIGHTNING GETTERS ==================== + + public boolean hasLightningEffect() { + return lightningEffect; + } + + public float getLightningDensity() { + return lightningDensity; + } + + public float getLightningRadius() { + return lightningRadius; + } + + public int getLightningFadeTime() { + return lightningFadeTime; + } + + // ==================== BRIGHTNESS ==================== + + @Override + public float getBrightness(float partialTicks) { + return 1.0f; + } + + @Override + @SideOnly(Side.CLIENT) + public int getBrightnessForRender(float partialTicks) { + return 0xF000F0; + } + + // ==================== NBT ==================== + + @Override + protected void readEntityFromNBT(NBTTagCompound nbt) { + readBaseNBT(nbt); + readProjectileNBT(nbt); + } + + @Override + protected void writeEntityToNBT(NBTTagCompound nbt) { + writeBaseNBT(nbt); + writeProjectileNBT(nbt); + } + + /** + * Read base projectile properties from NBT. + */ + protected void readBaseNBT(NBTTagCompound nbt) { + this.size = nbt.getFloat("Size"); + this.innerColor = nbt.getInteger("InnerColor"); + this.outerColor = nbt.getInteger("OuterColor"); + this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled"); + this.outerColorAlpha = nbt.hasKey("OuterColorAlpha") ? nbt.getFloat("OuterColorAlpha") : 0.5f; + this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 0.4f; + this.rotationSpeed = nbt.hasKey("RotationSpeed") ? nbt.getFloat("RotationSpeed") : 4.0f; + + this.damage = nbt.getFloat("Damage"); + this.knockback = nbt.getFloat("Knockback"); + this.knockbackUp = nbt.getFloat("KnockbackUp"); + + this.explosive = nbt.getBoolean("Explosive"); + this.explosionRadius = nbt.getFloat("ExplosionRadius"); + this.explosionDamageFalloff = nbt.getFloat("ExplosionDamageFalloff"); + + // Read effects list + this.effects.clear(); + if (nbt.hasKey("Effects")) { + NBTTagList effectsList = nbt.getTagList("Effects", 10); // 10 = compound tag + for (int i = 0; i < effectsList.tagCount(); i++) { + effects.add(AbilityEffect.fromNBT(effectsList.getCompoundTagAt(i))); + } + } + + this.maxDistance = nbt.getFloat("MaxDistance"); + this.maxLifetime = nbt.getInteger("MaxLifetime"); + this.deathWorldTime = nbt.hasKey("DeathWorldTime") ? nbt.getLong("DeathWorldTime") : -1; + + this.startX = nbt.getDouble("StartX"); + this.startY = nbt.getDouble("StartY"); + this.startZ = nbt.getDouble("StartZ"); + + this.ownerEntityId = nbt.getInteger("OwnerId"); + this.targetEntityId = nbt.getInteger("TargetId"); + + this.motionX = nbt.getDouble("MotionX"); + this.motionY = nbt.getDouble("MotionY"); + this.motionZ = nbt.getDouble("MotionZ"); + + // Lightning + this.lightningEffect = nbt.getBoolean("LightningEffect"); + this.lightningDensity = nbt.hasKey("LightningDensity") ? nbt.getFloat("LightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("LightningRadius") ? nbt.getFloat("LightningRadius") : 0.5f; + this.lightningFadeTime = nbt.hasKey("LightningFadeTime") ? nbt.getInteger("LightningFadeTime") : 6; + + if (this.worldObj != null && this.worldObj.isRemote) { + this.renderCurrentSize = this.size; + this.prevRenderSize = this.size; + } + } + + /** + * Write base projectile properties to NBT. + */ + protected void writeBaseNBT(NBTTagCompound nbt) { + nbt.setFloat("Size", size); + nbt.setInteger("InnerColor", innerColor); + nbt.setInteger("OuterColor", outerColor); + nbt.setBoolean("OuterColorEnabled", outerColorEnabled); + nbt.setFloat("OuterColorWidth", outerColorWidth); + nbt.setFloat("OuterColorAlpha", outerColorAlpha); + nbt.setFloat("RotationSpeed", rotationSpeed); + + nbt.setFloat("Damage", damage); + nbt.setFloat("Knockback", knockback); + nbt.setFloat("KnockbackUp", knockbackUp); + + nbt.setBoolean("Explosive", explosive); + nbt.setFloat("ExplosionRadius", explosionRadius); + nbt.setFloat("ExplosionDamageFalloff", explosionDamageFalloff); + + // Write effects list + if (!effects.isEmpty()) { + NBTTagList effectsList = new NBTTagList(); + for (AbilityEffect effect : effects) { + effectsList.appendTag(effect.writeNBT()); + } + nbt.setTag("Effects", effectsList); + } + + nbt.setFloat("MaxDistance", maxDistance); + nbt.setLong("DeathWorldTime", deathWorldTime); + nbt.setInteger("MaxLifetime", maxLifetime); + + nbt.setDouble("StartX", startX); + nbt.setDouble("StartY", startY); + nbt.setDouble("StartZ", startZ); + + nbt.setInteger("OwnerId", ownerEntityId); + nbt.setInteger("TargetId", targetEntityId); + + nbt.setDouble("MotionX", motionX); + nbt.setDouble("MotionY", motionY); + nbt.setDouble("MotionZ", motionZ); + + // Lightning + nbt.setBoolean("LightningEffect", lightningEffect); + nbt.setFloat("LightningDensity", lightningDensity); + nbt.setFloat("LightningRadius", lightningRadius); + nbt.setInteger("LightningFadeTime", lightningFadeTime); + } + + /** + * Subclass-specific NBT reading. + */ + protected abstract void readProjectileNBT(NBTTagCompound nbt); + + /** + * Subclass-specific NBT writing. + */ + protected abstract void writeProjectileNBT(NBTTagCompound nbt); + + // ==================== SPAWN DATA ==================== + + @Override + public void writeSpawnData(ByteBuf buffer) { + try { + NBTTagCompound compound = new NBTTagCompound(); + this.writeEntityToNBT(compound); + cpw.mods.fml.common.network.ByteBufUtils.writeTag(buffer, compound); + } catch (Exception ignored) {} + } + + @Override + public void readSpawnData(ByteBuf buffer) { + try { + NBTTagCompound compound = cpw.mods.fml.common.network.ByteBufUtils.readTag(buffer); + if (compound != null) { + this.readEntityFromNBT(compound); + } + } catch (Exception ignored) {} + } + + // ==================== COLLISION SETTINGS ==================== + + @Override + public boolean canBeCollidedWith() { + return false; + } + + @Override + public boolean canBePushed() { + return false; + } + + @Override + protected boolean canTriggerWalking() { + return false; + } + + @Override + public boolean isBurning() { + return false; + } + + @SideOnly(Side.CLIENT) + public float getShadowSize() { + return 0.0f; + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java new file mode 100644 index 000000000..8540d4df9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -0,0 +1,462 @@ +package kamkeel.npcs.entity; + +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.data.EnergyColorData; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; +import net.minecraft.world.World; +import noppes.npcs.LogWriter; +import noppes.npcs.NpcDamageSource; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Sweeper ability entity - a rotating beam attached to the NPC. + * Unlike other projectiles, this handles its own damage logic since it's + * attached to and rotates around the caster. + * + * Design: Similar to Beam trail visuals but rotating around origin. + */ +public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpawnData { + + // Beam properties + private float beamLength = 10.0f; + private float beamWidth = 0.3f; // Thin like beam trail + private float beamHeight = 0.8f; + private int innerColor = 0xFF6600; + private int outerColor = 0xFF0000; + private boolean outerColorEnabled = true; + private float outerColorWidth = 1.8f; + + // Combat properties + private float damage = 5.0f; + private int damageInterval = 5; + private boolean piercing = true; + + // Rotation + private float currentAngle = 0; + private float prevAngle = 0; + private float sweepSpeed = 3.0f; + private int numberOfRotations = 2; + private int completedRotations = 0; + private float baseYaw = 0; // Starting yaw direction + private boolean lockOnTarget = false; + + // Owner tracking + private int ownerEntityId = -1; + private int targetEntityId = -1; + + // Lifetime + private int maxTicks = 400; + private long deathWorldTime = -1; + + // Damage state + private transient int ticksSinceDamage = 0; + private transient Set hitThisTick = new HashSet<>(); + + public EntityAbilitySweeper(World world) { + super(world); + this.setSize(0.1f, 0.1f); + this.noClip = true; + this.isImmuneToFire = true; + this.ignoreFrustumCheck = true; + } + + public EntityAbilitySweeper(World world, EntityLivingBase owner, EntityLivingBase target, + float beamLength, float beamWidth, float beamHeight, + EnergyColorData colorData, + float sweepSpeed, int numberOfRotations, + float damage, int damageInterval, boolean piercing, + boolean lockOnTarget) { + this(world); + + this.ownerEntityId = owner.getEntityId(); + this.targetEntityId = target != null ? target.getEntityId() : -1; + this.beamLength = beamLength; + this.beamWidth = beamWidth; + this.beamHeight = beamHeight; + this.innerColor = colorData.innerColor; + this.outerColor = colorData.outerColor; + this.outerColorEnabled = colorData.outerColorEnabled; + this.outerColorWidth = colorData.outerColorWidth; + this.sweepSpeed = sweepSpeed; + this.numberOfRotations = numberOfRotations; + this.damage = damage; + this.damageInterval = damageInterval; + this.piercing = piercing; + this.lockOnTarget = lockOnTarget; + + // Calculate max ticks based on rotations + this.maxTicks = (int) ((360.0f * numberOfRotations) / sweepSpeed) + 10; + + // Calculate base yaw + if (lockOnTarget && target != null) { + double dx = target.posX - owner.posX; + double dz = target.posZ - owner.posZ; + this.baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); + } else { + this.baseYaw = owner.rotationYaw; + } + + // Position at owner + this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ); + + // Ready to deal damage immediately + this.ticksSinceDamage = damageInterval; + } + + @Override + protected void entityInit() { + // No DataWatcher needed + } + + @Override + public void onUpdate() { + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + this.prevAngle = this.currentAngle; + + super.onUpdate(); + + // Set death time on first tick + if (deathWorldTime < 0 && worldObj != null) { + deathWorldTime = worldObj.getTotalWorldTime() + maxTicks; + } + + // Check lifetime + if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { + this.setDead(); + return; + } + + // Follow owner + Entity owner = getOwner(); + if (owner == null) { + this.setDead(); + return; + } + + // Update position to follow owner + this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ); + + // Check if sweep is done + if (completedRotations >= numberOfRotations) { + this.setDead(); + return; + } + + // Update rotation + currentAngle += sweepSpeed; + if (currentAngle >= 360) { + currentAngle -= 360; + completedRotations++; + if (!worldObj.isRemote) { + LogWriter.info("[Sweeper] Completed rotation " + completedRotations + "/" + numberOfRotations); + } + } + + // Server handles damage + if (!worldObj.isRemote) { + handleDamage(owner); + } + } + + /** + * Handle damage detection and application. + * This runs on server only. + */ + private void handleDamage(Entity owner) { + hitThisTick.clear(); + ticksSinceDamage++; + + if (ticksSinceDamage < damageInterval) { + return; + } + ticksSinceDamage = 0; + + // Calculate beam direction using the ACTUAL render angle + float beamYaw = baseYaw + currentAngle; + float yawRad = (float) Math.toRadians(beamYaw); + + double startX = posX; + double startY = posY; + double startZ = posZ; + + double dirX = -Math.sin(yawRad); + double dirZ = Math.cos(yawRad); + + // Sample points along the beam for collision + int samples = (int) (beamLength / 0.5); + for (int i = 1; i <= samples; i++) { + double progress = (double) i / samples; + double checkX = startX + dirX * beamLength * progress; + double checkY = startY; + double checkZ = startZ + dirZ * beamLength * progress; + + // Narrow hitbox for thin beam + AxisAlignedBB checkBox = AxisAlignedBB.getBoundingBox( + checkX - beamWidth * 2, checkY - 0.5, checkZ - beamWidth * 2, + checkX + beamWidth * 2, checkY + 1.0, checkZ + beamWidth * 2 + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, checkBox); + + for (Entity entity : entities) { + if (!(entity instanceof EntityLivingBase)) continue; + if (entity == owner) continue; + if (hitThisTick.contains(entity.getEntityId())) continue; + + EntityLivingBase livingEntity = (EntityLivingBase) entity; + + // Check if actually in beam path + if (isInBeamPath(livingEntity, startX, startY, startZ, dirX, dirZ)) { + hitThisTick.add(entity.getEntityId()); + applyDamage(livingEntity, owner); + + if (!piercing) { + return; + } + } + } + } + } + + private boolean isInBeamPath(EntityLivingBase entity, double startX, double startY, double startZ, + double dirX, double dirZ) { + double entityX = entity.posX - startX; + double entityZ = entity.posZ - startZ; + + // Dot product to find projection distance + double projectionDist = entityX * dirX + entityZ * dirZ; + + // Check if within beam length + if (projectionDist < 0 || projectionDist > beamLength) { + return false; + } + + // Calculate perpendicular distance + double projX = dirX * projectionDist; + double projZ = dirZ * projectionDist; + double perpX = entityX - projX; + double perpZ = entityZ - projZ; + double perpDist = Math.sqrt(perpX * perpX + perpZ * perpZ); + + // Check if within beam width (use wider hitbox than visual) + if (perpDist > beamWidth * 3 + entity.width / 2) { + return false; + } + + // Height check - entity's feet must be below beam top (allows jumping over) + double entityFeetY = entity.posY; + double beamTopY = startY + 0.5; + + return entityFeetY < beamTopY; + } + + private void applyDamage(EntityLivingBase target, Entity owner) { + if (owner instanceof EntityNPCInterface) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), damage); + } else if (owner instanceof EntityPlayer) { + target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), damage); + } else if (owner instanceof EntityLivingBase) { + target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), damage); + } else { + target.attackEntityFrom(new NpcDamageSource("npc_ability", null), damage); + } + + // Small upward knockback to help escape + target.addVelocity(0, 0.2, 0); + target.velocityChanged = true; + } + + private Entity getOwner() { + if (ownerEntityId == -1) return null; + return worldObj.getEntityByID(ownerEntityId); + } + + // ==================== RENDERING ==================== + + @Override + @SideOnly(Side.CLIENT) + public boolean isInRangeToRenderDist(double distance) { + return distance < 16384.0D; // 128 blocks squared + } + + @Override + public boolean shouldRenderInPass(int pass) { + return pass == 1; // Translucent pass + } + + @Override + public float getBrightness(float partialTicks) { + return 1.0f; + } + + @Override + @SideOnly(Side.CLIENT) + public int getBrightnessForRender(float partialTicks) { + return 0xF000F0; + } + + // ==================== GETTERS FOR RENDERER ==================== + + public float getBeamLength() { + return beamLength; + } + + public float getBeamWidth() { + return beamWidth; + } + + public float getBeamHeight() { + return beamHeight; + } + + public int getInnerColor() { + return innerColor; + } + + public int getOuterColor() { + return outerColor; + } + + public boolean isOuterColorEnabled() { + return outerColorEnabled; + } + + public float getOuterColorWidth() { + return outerColorWidth; + } + + /** + * Get the full interpolated angle INCLUDING base yaw for rendering. + */ + public float getInterpolatedAngle(float partialTicks) { + // Handle wrap-around for the rotating part + float diff = currentAngle - prevAngle; + if (diff > 180) diff -= 360; + if (diff < -180) diff += 360; + float interpAngle = prevAngle + diff * partialTicks; + // Add base yaw for correct world orientation + return baseYaw + interpAngle; + } + + public float getCurrentAngle() { + return currentAngle; + } + + public float getBaseYaw() { + return baseYaw; + } + + // ==================== NBT ==================== + + @Override + protected void readEntityFromNBT(NBTTagCompound nbt) { + this.beamLength = nbt.getFloat("BeamLength"); + this.beamWidth = nbt.getFloat("BeamWidth"); + this.beamHeight = nbt.getFloat("BeamHeight"); + this.innerColor = nbt.getInteger("InnerColor"); + this.outerColor = nbt.getInteger("OuterColor"); + this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled"); + this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 1.8f; + this.sweepSpeed = nbt.getFloat("SweepSpeed"); + this.numberOfRotations = nbt.getInteger("NumRotations"); + this.completedRotations = nbt.getInteger("CompletedRotations"); + this.maxTicks = nbt.getInteger("MaxTicks"); + this.ownerEntityId = nbt.getInteger("OwnerId"); + this.targetEntityId = nbt.getInteger("TargetId"); + this.currentAngle = nbt.getFloat("CurrentAngle"); + this.baseYaw = nbt.getFloat("BaseYaw"); + this.deathWorldTime = nbt.getLong("DeathWorldTime"); + this.damage = nbt.getFloat("Damage"); + this.damageInterval = nbt.getInteger("DamageInterval"); + this.piercing = nbt.getBoolean("Piercing"); + this.lockOnTarget = nbt.getBoolean("LockOnTarget"); + } + + @Override + protected void writeEntityToNBT(NBTTagCompound nbt) { + nbt.setFloat("BeamLength", beamLength); + nbt.setFloat("BeamWidth", beamWidth); + nbt.setFloat("BeamHeight", beamHeight); + nbt.setInteger("InnerColor", innerColor); + nbt.setInteger("OuterColor", outerColor); + nbt.setBoolean("OuterColorEnabled", outerColorEnabled); + nbt.setFloat("OuterColorWidth", outerColorWidth); + nbt.setFloat("SweepSpeed", sweepSpeed); + nbt.setInteger("NumRotations", numberOfRotations); + nbt.setInteger("CompletedRotations", completedRotations); + nbt.setInteger("MaxTicks", maxTicks); + nbt.setInteger("OwnerId", ownerEntityId); + nbt.setInteger("TargetId", targetEntityId); + nbt.setFloat("CurrentAngle", currentAngle); + nbt.setFloat("BaseYaw", baseYaw); + nbt.setLong("DeathWorldTime", deathWorldTime); + nbt.setFloat("Damage", damage); + nbt.setInteger("DamageInterval", damageInterval); + nbt.setBoolean("Piercing", piercing); + nbt.setBoolean("LockOnTarget", lockOnTarget); + } + + // ==================== SPAWN DATA ==================== + + @Override + public void writeSpawnData(ByteBuf buffer) { + buffer.writeFloat(beamLength); + buffer.writeFloat(beamWidth); + buffer.writeFloat(beamHeight); + buffer.writeInt(innerColor); + buffer.writeInt(outerColor); + buffer.writeBoolean(outerColorEnabled); + buffer.writeFloat(outerColorWidth); + buffer.writeFloat(sweepSpeed); + buffer.writeInt(numberOfRotations); + buffer.writeInt(completedRotations); + buffer.writeInt(maxTicks); + buffer.writeInt(ownerEntityId); + buffer.writeInt(targetEntityId); + buffer.writeFloat(currentAngle); + buffer.writeFloat(baseYaw); + buffer.writeFloat(damage); + buffer.writeInt(damageInterval); + buffer.writeBoolean(piercing); + buffer.writeBoolean(lockOnTarget); + } + + @Override + public void readSpawnData(ByteBuf buffer) { + this.beamLength = buffer.readFloat(); + this.beamWidth = buffer.readFloat(); + this.beamHeight = buffer.readFloat(); + this.innerColor = buffer.readInt(); + this.outerColor = buffer.readInt(); + this.outerColorEnabled = buffer.readBoolean(); + this.outerColorWidth = buffer.readFloat(); + this.sweepSpeed = buffer.readFloat(); + this.numberOfRotations = buffer.readInt(); + this.completedRotations = buffer.readInt(); + this.maxTicks = buffer.readInt(); + this.ownerEntityId = buffer.readInt(); + this.targetEntityId = buffer.readInt(); + this.currentAngle = buffer.readFloat(); + this.baseYaw = buffer.readFloat(); + this.damage = buffer.readFloat(); + this.damageInterval = buffer.readInt(); + this.piercing = buffer.readBoolean(); + this.lockOnTarget = buffer.readBoolean(); + this.prevAngle = this.currentAngle; + } +} diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 3a4c35d05..63ab9b192 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -26,6 +26,9 @@ import kamkeel.npcs.network.packets.data.SoundManagementPacket; import kamkeel.npcs.network.packets.data.SwingPlayerArmPacket; import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket; import kamkeel.npcs.network.packets.data.VillagerListPacket; +import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.data.gui.GuiClosePacket; import kamkeel.npcs.network.packets.data.gui.GuiErrorPacket; import kamkeel.npcs.network.packets.data.gui.GuiOpenBookPacket; @@ -49,8 +52,6 @@ import kamkeel.npcs.network.packets.data.npc.WeaponNpcPacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayClosePacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket; import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; import kamkeel.npcs.network.packets.player.BankActionPacket; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.CompanionActionPacket; @@ -59,11 +60,13 @@ import kamkeel.npcs.network.packets.player.FollowerPacket; import kamkeel.npcs.network.packets.player.GetNPCRole; import kamkeel.npcs.network.packets.player.GetTraderData; import kamkeel.npcs.network.packets.player.InputDevicePacket; +import kamkeel.npcs.network.packets.player.AuctionActionPacket; import kamkeel.npcs.network.packets.player.MailActionPacket; import kamkeel.npcs.network.packets.player.SaveBookPacket; import kamkeel.npcs.network.packets.player.SaveSignPacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; +import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; import kamkeel.npcs.network.packets.player.TransportPacket; import kamkeel.npcs.network.packets.player.customgui.CustomGuiButtonPacket; import kamkeel.npcs.network.packets.player.customgui.CustomGuiClosePacket; @@ -75,7 +78,6 @@ import kamkeel.npcs.network.packets.player.item.MagicCyclesPacket; import kamkeel.npcs.network.packets.player.profile.ProfileChangePacket; import kamkeel.npcs.network.packets.player.profile.ProfileCreatePacket; import kamkeel.npcs.network.packets.player.profile.ProfileGetInfoPacket; -import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; import kamkeel.npcs.network.packets.player.profile.ProfileGetPacket; import kamkeel.npcs.network.packets.player.profile.ProfileRemovePacket; import kamkeel.npcs.network.packets.player.profile.ProfileRenamePacket; @@ -88,10 +90,18 @@ import kamkeel.npcs.network.packets.request.MountPacket; import kamkeel.npcs.network.packets.request.TileEntityGetPacket; import kamkeel.npcs.network.packets.request.TileEntitySavePacket; import kamkeel.npcs.network.packets.request.TraderMarketSavePacket; +import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; +import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; +import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket; +import kamkeel.npcs.network.packets.request.animation.BuiltInAnimationGetPacket; import kamkeel.npcs.network.packets.request.bank.BankGetPacket; import kamkeel.npcs.network.packets.request.bank.BankRemovePacket; import kamkeel.npcs.network.packets.request.bank.BankSavePacket; @@ -141,9 +151,6 @@ import kamkeel.npcs.network.packets.request.linked.LinkedItemSavePacket; import kamkeel.npcs.network.packets.request.linked.LinkedNPCAddPacket; import kamkeel.npcs.network.packets.request.linked.LinkedNPCRemovePacket; import kamkeel.npcs.network.packets.request.linked.LinkedSetPacket; -import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; -import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; -import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket; import kamkeel.npcs.network.packets.request.magic.MagicCycleRemovePacket; import kamkeel.npcs.network.packets.request.magic.MagicCycleSavePacket; import kamkeel.npcs.network.packets.request.magic.MagicGetAllPacket; @@ -224,6 +231,7 @@ import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; import kamkeel.npcs.network.packets.request.script.NPCScriptPacket; import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; +import kamkeel.npcs.network.packets.request.script.ScriptFilesPacket; import kamkeel.npcs.network.packets.request.script.ScriptInfoPacket; import kamkeel.npcs.network.packets.request.script.item.ItemScriptErrorPacket; import kamkeel.npcs.network.packets.request.script.item.ItemScriptPacket; @@ -304,6 +312,7 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new NPCScriptPacket()); REQUEST_PACKET.registerPacket(new PlayerScriptPacket()); REQUEST_PACKET.registerPacket(new ScriptInfoPacket()); + REQUEST_PACKET.registerPacket(new ScriptFilesPacket()); REQUEST_PACKET.registerPacket(new EffectScriptPacket()); // Cloner Packets @@ -466,6 +475,7 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new AnimationGetPacket()); REQUEST_PACKET.registerPacket(new AnimationRemovePacket()); REQUEST_PACKET.registerPacket(new AnimationSavePacket()); + REQUEST_PACKET.registerPacket(new BuiltInAnimationGetPacket()); // Moving Path REQUEST_PACKET.registerPacket(new MovingPathSavePacket()); @@ -496,6 +506,10 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new AbilitiesGetAllPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcGetPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcSavePacket()); + REQUEST_PACKET.registerPacket(new CustomAbilitiesGetPacket()); + REQUEST_PACKET.registerPacket(new CustomAbilityGetPacket()); + REQUEST_PACKET.registerPacket(new CustomAbilityRemovePacket()); + REQUEST_PACKET.registerPacket(new CustomAbilitySavePacket()); // Other Packets REQUEST_PACKET.registerPacket(new IsGuiOpenInform()); @@ -565,6 +579,7 @@ public class PacketHandler { // Data | Ability Packets DATA_PACKET.registerPacket(new TelegraphSpawnPacket()); DATA_PACKET.registerPacket(new TelegraphRemovePacket()); + DATA_PACKET.registerPacket(new PlayerAbilitySyncPacket()); } public void registerPlayerPackets() { @@ -589,6 +604,9 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new GetNPCRole()); PLAYER_PACKET.registerPacket(new GetTraderData()); + // Auction Packets + PLAYER_PACKET.registerPacket(new AuctionActionPacket()); + PLAYER_PACKET.registerPacket(new InputDevicePacket()); PLAYER_PACKET.registerPacket(new ScreenSizePacket()); diff --git a/src/main/java/kamkeel/npcs/network/PacketUtil.java b/src/main/java/kamkeel/npcs/network/PacketUtil.java index f262bce45..9235dc1f7 100644 --- a/src/main/java/kamkeel/npcs/network/PacketUtil.java +++ b/src/main/java/kamkeel/npcs/network/PacketUtil.java @@ -15,6 +15,7 @@ import noppes.npcs.NBTTags; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.items.ItemNpcTool; import java.io.IOException; @@ -221,9 +222,9 @@ public class PacketUtil { compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(data.getConsoleText())); GuiDataPacket.sendGuiData(player, compound); - List containers = data.getScripts(); + List containers = data.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); NBTTagCompound tabCompound = new NBTTagCompound(); tabCompound.setInteger("Tab", i); tabCompound.setTag("Script", container.writeToNBT(new NBTTagCompound())); @@ -250,8 +251,8 @@ public class PacketUtil { data.getScripts().add(new ScriptContainer(data)); } NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - ScriptContainer script = new ScriptContainer(data); - script.readFromNBT(tabCompound); + // Use factory method to create correct script unit type based on NBT + IScriptUnit script = IScriptUnit.createFromNBT(tabCompound, data); data.getScripts().set(tab, script); } else { NBTTagCompound compound = ByteBufUtils.readNBT(buffer); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index ebcbdfc59..18321d0a4 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -62,4 +62,8 @@ public enum EnumDataPacket { // Ability System TELEGRAPH_SPAWN, TELEGRAPH_REMOVE, + PLAYER_ABILITY_SYNC, + + // Auction System + AUCTION_DATA, } diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 1defc81c6..8183941d0 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -93,6 +93,7 @@ public enum EnumRequestPacket { AnimationGet, AnimationRemove, AnimationSave, + BuiltInAnimationGet, RemoteNpcsGet, RemoteTpToNpc, @@ -172,6 +173,7 @@ public enum EnumRequestPacket { BlockScript, NPCScript, ScriptInfo, + ScriptFiles, EffectScript, DimensionTeleport, @@ -196,6 +198,10 @@ public enum EnumRequestPacket { NpcAbilitiesGet, NpcAbilitiesSave, AbilitiesGetAll, + CustomAbilitiesGet, + CustomAbilityGet, + CustomAbilityRemove, + CustomAbilitySave, Hammer, RecipeScript, diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java index a84e62392..1ac0ec828 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java @@ -12,5 +12,6 @@ public enum EnumSyncType { WORKBENCH_RECIPES, CARPENTRY_RECIPES, ANVIL_RECIPES, - CUSTOM_EFFECTS + CUSTOM_EFFECTS, + CUSTOM_ABILITY } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java index c341bd02e..2717d6277 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java @@ -12,8 +12,11 @@ import kamkeel.npcs.network.enums.EnumSyncType; import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.AuctionClientConfig; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.config.ConfigMain; +import noppes.npcs.controllers.AuctionConfigSync; import java.io.IOException; import java.util.EnumMap; @@ -57,6 +60,11 @@ public final class LoginPacket extends AbstractPacket { out.writeInt(entry.getKey().ordinal()); out.writeInt(entry.getValue()); } + + // Send auction config (server-side sync) + NBTTagCompound auctionConfig = new NBTTagCompound(); + AuctionConfigSync.writeToNBT(auctionConfig); + ByteBufUtils.writeNBT(out, auctionConfig); } @SideOnly(Side.CLIENT) @@ -76,6 +84,10 @@ public final class LoginPacket extends AbstractPacket { ClientCacheHandler.setActiveServer(serverKey, serverRevisions); + // Read auction config + NBTTagCompound auctionConfig = ByteBufUtils.readNBT(in); + AuctionClientConfig.readFromNBT(auctionConfig); + PacketClient.sendClient(new SyncRevisionInfoPacket( serverKey, ClientCacheHandler.getLastServerKey(), diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java new file mode 100644 index 000000000..08df836aa --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java @@ -0,0 +1,73 @@ +package kamkeel.npcs.network.packets.data.ability; + +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.enums.EnumDataPacket; +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.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; + +import java.io.IOException; + +/** + * Syncs the player's ability data (unlocked abilities, selected index) to the client. + * Sent on login and whenever abilities change (unlock/lock/select). + */ +public final class PlayerAbilitySyncPacket extends AbstractPacket { + public static final String packetName = "Data|PlayerAbilitySync"; + + private NBTTagCompound abilityNBT; + + public PlayerAbilitySyncPacket() { + } + + public PlayerAbilitySyncPacket(NBTTagCompound abilityNBT) { + this.abilityNBT = abilityNBT; + } + + @Override + public Enum getType() { + return EnumDataPacket.PLAYER_ABILITY_SYNC; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeNBT(out, abilityNBT); + } + + @SideOnly(Side.CLIENT) + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + NBTTagCompound nbt = ByteBufUtils.readNBT(in); + if (nbt == null) return; + + PlayerData data = PlayerData.get(player); + if (data != null) { + data.abilityData.readFromNBT(nbt); + } + } + + /** + * Send the player's ability data to their client. + */ + public static void sendToPlayer(EntityPlayerMP player) { + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (data == null) return; + + NBTTagCompound nbt = new NBTTagCompound(); + data.abilityData.writeToNBT(nbt); + PacketHandler.Instance.sendToPlayer(new PlayerAbilitySyncPacket(nbt), player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphRemovePacket.java similarity index 75% rename from src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java rename to src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphRemovePacket.java index e0c5023cb..9e84bb224 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphRemovePacket.java @@ -1,9 +1,9 @@ -package kamkeel.npcs.network.packets.data.ability; +package kamkeel.npcs.network.packets.data.telegraph; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; @@ -16,7 +16,7 @@ import java.io.IOException; /** * Packet to remove a telegraph on the client. - * Sent from server to client when an ability's telegraph should be removed early. + * Sent from server to client when a telegraph should be removed early. */ public final class TelegraphRemovePacket extends AbstractPacket { public static final String packetName = "Data|TelegraphRemove"; @@ -67,4 +67,18 @@ public final class TelegraphRemovePacket extends AbstractPacket { public static void sendToTracking(String instanceId, net.minecraft.entity.Entity entity) { PacketHandler.Instance.sendTracking(new TelegraphRemovePacket(instanceId), entity); } + + /** + * Send remove to all players in a dimension. + */ + public static void sendToDimension(String instanceId, int dimensionId) { + PacketHandler.Instance.sendToDimension(new TelegraphRemovePacket(instanceId), dimensionId); + } + + /** + * Send remove to all players on the server. + */ + public static void sendToAll(String instanceId) { + PacketHandler.Instance.sendToAll(new TelegraphRemovePacket(instanceId)); + } } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphSpawnPacket.java similarity index 84% rename from src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java rename to src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphSpawnPacket.java index bbf342ade..fa30229ce 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphSpawnPacket.java @@ -1,11 +1,10 @@ -package kamkeel.npcs.network.packets.data.ability; +package kamkeel.npcs.network.packets.data.telegraph; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; @@ -19,7 +18,7 @@ import java.io.IOException; /** * Packet to spawn a telegraph on the client. - * Sent from server to client when an ability creates a telegraph. + * Sent from server to client when a telegraph is created. */ public final class TelegraphSpawnPacket extends AbstractPacket { public static final String packetName = "Data|TelegraphSpawn"; @@ -79,4 +78,11 @@ public final class TelegraphSpawnPacket extends AbstractPacket { public static void sendToDimension(TelegraphInstance instance, int dimensionId) { PacketHandler.Instance.sendToDimension(new TelegraphSpawnPacket(instance), dimensionId); } + + /** + * Send telegraph to all players on the server. + */ + public static void sendToAll(TelegraphInstance instance) { + PacketHandler.Instance.sendToAll(new TelegraphSpawnPacket(instance)); + } } diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java new file mode 100644 index 000000000..3ce74eb4c --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -0,0 +1,522 @@ +package kamkeel.npcs.network.packets.player; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import 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.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.constants.EnumRoleType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; + +import java.io.IOException; +import java.util.List; + +/** + * Handles all auction-related client-server communication. + */ +public class AuctionActionPacket extends AbstractPacket { + public static final String packetName = "Player|Auction"; + private static final int PAGE_SIZE = 45; // 9x5 grid + + private Action action; + private String listingId; + private long amount; + private NBTTagCompound data; + + public AuctionActionPacket() {} + + private AuctionActionPacket(Action action) { + this.action = action; + } + + private AuctionActionPacket(Action action, String listingId) { + this.action = action; + this.listingId = listingId; + } + + private AuctionActionPacket(Action action, String listingId, long amount) { + this.action = action; + this.listingId = listingId; + this.amount = amount; + } + + @Override + public boolean needsNPC() { return false; } // Allow opening via command without NPC + + // ========================================= + // Client-side static methods + // ========================================= + + @SideOnly(Side.CLIENT) + public static void placeBid(String listingId, long bidAmount) { + AuctionActionPacket packet = new AuctionActionPacket(Action.Bid, listingId, bidAmount); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void buyout(String listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.Buyout, listingId); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void cancelListing(String listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.Cancel, listingId); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void claimItem(String claimId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimItem, claimId); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void claimCurrency(String claimId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimCurrency, claimId); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void createListing(ItemStack item, long startingPrice, long buyoutPrice) { + AuctionActionPacket packet = new AuctionActionPacket(Action.CreateListing); + packet.amount = startingPrice; + packet.listingId = String.valueOf(buyoutPrice); + packet.data = new NBTTagCompound(); + item.writeToNBT(packet.data); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void openPage(int page) { + AuctionActionPacket packet = new AuctionActionPacket(Action.OpenPage); + packet.amount = page; + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void openBidding(String listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.OpenBidding, listingId); + PacketClient.sendClient(packet); + } + + /** Request listings from server with filter and page */ + @SideOnly(Side.CLIENT) + public static void requestListings(AuctionFilter filter, int page) { + AuctionActionPacket packet = new AuctionActionPacket(Action.RequestListings); + packet.amount = page; + packet.data = filter.writeToNBT(new NBTTagCompound()); + PacketClient.sendClient(packet); + } + + // ========================================= + // Packet Implementation + // ========================================= + + @Override + public Enum getType() { return EnumPlayerPacket.AuctionAction; } + + @Override + public PacketChannel getChannel() { return PacketHandler.PLAYER_PACKET; } + + @Override + @SideOnly(Side.CLIENT) + public void sendData(ByteBuf out) throws IOException { + out.writeInt(action.ordinal()); + + switch (action) { + case Bid: + ByteBufUtils.writeString(out, listingId); + out.writeLong(amount); + break; + case Buyout: + case Cancel: + case ClaimItem: + case ClaimCurrency: + ByteBufUtils.writeString(out, listingId); + break; + case CreateListing: + out.writeLong(amount); + ByteBufUtils.writeString(out, listingId); + ByteBufUtils.writeNBT(out, data); + break; + case OpenPage: + out.writeInt((int) amount); + break; + case OpenBidding: + ByteBufUtils.writeString(out, listingId); + break; + case RequestListings: + out.writeInt((int) amount); + ByteBufUtils.writeNBT(out, data); + break; + } + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) return; + EntityPlayerMP playerMP = (EntityPlayerMP) player; + + // Skip role check if NPC is null (opened via command/script) + // Otherwise require Auctioneer role + if (npc != null && npc.advanced.role != EnumRoleType.Auctioneer) return; + if (!ConfigMarket.AuctionEnabled) return; + + int actionOrdinal = in.readInt(); + if (actionOrdinal < 0 || actionOrdinal >= Action.values().length) { + return; // Invalid action + } + Action requestedAction = Action.values()[actionOrdinal]; + + AuctionController controller = AuctionController.getInstance(); + if (controller == null) { + sendError(playerMP, "Auction system is not available."); + return; + } + + String result = null; + + switch (requestedAction) { + case Bid: + result = handleBid(in, playerMP, controller); + break; + + case Buyout: + result = handleBuyout(in, playerMP, controller); + break; + + case Cancel: + result = handleCancel(in, playerMP, controller); + break; + + case ClaimItem: + result = handleClaimItem(in, playerMP, controller); + break; + + case ClaimCurrency: + result = handleClaimCurrency(in, playerMP, controller); + break; + + case CreateListing: + result = handleCreateListing(in, playerMP, controller); + break; + + case OpenPage: + handleOpenPage(in, playerMP); + break; + + case OpenBidding: + handleOpenBidding(in, playerMP, controller); + break; + + case RequestListings: + handleRequestListings(in, playerMP, controller); + break; + } + + if (result != null) { + sendError(playerMP, result); + } + } + + // ========================================= + // Action Handlers + // ========================================= + + private String handleBid(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String id = ByteBufUtils.readString(in); + long amount = in.readLong(); + String result = controller.placeBid(id, player, amount); + if (result == null) { + sendListingsUpdate(player); + } + return result; + } + + private String handleBuyout(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String id = ByteBufUtils.readString(in); + String result = controller.buyout(id, player); + if (result == null) { + sendListingsUpdate(player); + } + return result; + } + + private String handleCancel(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String id = ByteBufUtils.readString(in); + String result = controller.cancelListing(id, player, false); + if (result == null) { + sendTradesUpdate(player); + } + return result; + } + + private String handleClaimItem(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String id = ByteBufUtils.readString(in); + String result = controller.claimItem(id, player); + if (result == null) { + sendTradesUpdate(player); + } + return result; + } + + private String handleClaimCurrency(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String id = ByteBufUtils.readString(in); + String result = controller.claimCurrency(id, player); + if (result == null) { + sendTradesUpdate(player); + } + return result; + } + + private String handleCreateListing(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + long startingPrice = in.readLong(); + String buyoutStr = ByteBufUtils.readString(in); + long buyoutPrice = 0; + try { buyoutPrice = Long.parseLong(buyoutStr); } catch (NumberFormatException ignored) {} + + NBTTagCompound itemNBT = ByteBufUtils.readNBT(in); + if (itemNBT == null) return "No item data received."; + + ItemStack item = ItemStack.loadItemStackFromNBT(itemNBT); + if (item == null) return "Invalid item data."; + + // Remove item from inventory first + if (!removeItemFromInventory(player, item)) { + return "Item not found in inventory."; + } + + String result = controller.createListing(player, item, startingPrice, buyoutPrice); + if (result != null) { + // Failed - return item (drop if inventory full to prevent item loss) + if (!player.inventory.addItemStackToInventory(item)) { + player.entityDropItem(item, 0.5f); + } + } + return result; + } + + private void handleOpenPage(ByteBuf in, EntityPlayerMP player) { + int page = in.readInt(); + EnumGuiType guiType; + switch (page) { + case 0: guiType = EnumGuiType.PlayerAuction; break; + case 1: guiType = EnumGuiType.PlayerAuctionSell; break; + case 2: guiType = EnumGuiType.PlayerAuctionTrades; break; + default: guiType = EnumGuiType.PlayerAuction; break; + } + NoppesUtilServer.sendOpenGui(player, guiType, npc); + + // Send trades data when opening the trades page + if (page == 2) { + AuctionController controller = AuctionController.getInstance(); + if (controller != null) { + sendTradesData(player, controller); + } + } + } + + /** Send full trades data to player (listings, bids, claims) */ + private void sendTradesData(EntityPlayerMP player, AuctionController controller) { + NBTTagCompound response = new NBTTagCompound(); + response.setBoolean("TradesData", true); + + // Send player's max trade slots (permission-aware) + response.setInteger("MaxTradeSlots", controller.getMaxTradesForPlayer(player)); + + // Active listings (items player is selling) + List listings = controller.getPlayerActiveListings(player.getUniqueID()); + NBTTagList listingsNBT = new NBTTagList(); + for (AuctionListing listing : listings) { + listingsNBT.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + response.setTag("ActiveListings", listingsNBT); + + // Active bids (items player is bidding on) + List bids = controller.getPlayerActiveBids(player.getUniqueID()); + NBTTagList bidsNBT = new NBTTagList(); + for (AuctionListing bid : bids) { + bidsNBT.appendTag(bid.writeToNBT(new NBTTagCompound())); + } + response.setTag("ActiveBids", bidsNBT); + + // Claims (items/currency to claim) - now from PlayerData + List claims = controller.getPlayerClaims(player); + NBTTagList claimsNBT = new NBTTagList(); + for (AuctionClaim claim : claims) { + claimsNBT.appendTag(claim.writeToNBT(new NBTTagCompound())); + } + response.setTag("Claims", claimsNBT); + + GuiDataPacket.sendGuiData(player, response); + } + + private void handleOpenBidding(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String listingId = ByteBufUtils.readString(in); + AuctionListing listing = controller.getListing(listingId); + + if (listing == null || !listing.isActive()) { + sendError(player, "Listing not found or has ended."); + return; + } + + // Open bidding GUI + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuctionBidding, npc); + + // Check if this is the player's own listing + boolean isOwnListing = listing.isSeller(player.getUniqueID()); + + // Send listing data + NBTTagCompound response = new NBTTagCompound(); + response.setBoolean("BiddingData", true); + response.setTag("Listing", listing.writeToNBT(new NBTTagCompound())); + response.setLong("Balance", controller.getPlayerBalance(player)); + response.setBoolean("IsOwnListing", isOwnListing); + GuiDataPacket.sendGuiData(player, response); + } + + private void handleRequestListings(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + int page = in.readInt(); + NBTTagCompound filterNBT = ByteBufUtils.readNBT(in); + + AuctionFilter filter = new AuctionFilter(); + if (filterNBT != null) { + filter.readFromNBT(filterNBT); + } + + // Get listings for this page + List listings = controller.getActiveListings(filter, page, PAGE_SIZE); + int totalListings = controller.getTotalActiveListings(filter); + int totalPages = Math.max(1, (int) Math.ceil((double) totalListings / PAGE_SIZE)); + + // Build response + NBTTagCompound response = new NBTTagCompound(); + response.setBoolean("ListingsData", true); + response.setInteger("Page", page); + response.setInteger("TotalPages", totalPages); + response.setInteger("TotalListings", totalListings); + + NBTTagList listingsList = new NBTTagList(); + for (AuctionListing listing : listings) { + listingsList.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + response.setTag("Listings", listingsList); + + GuiDataPacket.sendGuiData(player, response); + } + + // ========================================= + // Helpers + // ========================================= + + /** + * Checks if two ItemStacks are the same type (item, damage, NBT) ignoring stack size. + */ + private boolean areItemsSameType(ItemStack item1, ItemStack item2) { + if (item1 == null || item2 == null) + return item1 == item2; + if (item1.getItem() != item2.getItem()) + return false; + if (item1.getItemDamage() != item2.getItemDamage()) + return false; + return ItemStack.areItemStackTagsEqual(item1, item2); + } + + /** + * Counts total matching items across all inventory stacks. + */ + private int countItemsInInventory(EntityPlayerMP player, ItemStack target) { + int count = 0; + for (int i = 0; i < player.inventory.mainInventory.length; i++) { + ItemStack stack = player.inventory.mainInventory[i]; + if (stack != null && areItemsSameType(stack, target)) { + count += stack.stackSize; + } + } + return count; + } + + /** + * Removes items from inventory, consuming from multiple stacks if needed. + * Returns true if successful, false if not enough items. + */ + private boolean removeItemFromInventory(EntityPlayerMP player, ItemStack target) { + if (target == null) return false; + + int needed = target.stackSize; + + // First verify player has enough total items + int available = countItemsInInventory(player, target); + if (available < needed) { + return false; + } + + // Consume items from matching stacks + for (int i = 0; i < player.inventory.mainInventory.length && needed > 0; i++) { + ItemStack stack = player.inventory.mainInventory[i]; + if (stack == null || !areItemsSameType(stack, target)) + continue; + + if (needed >= stack.stackSize) { + // Need entire stack + needed -= stack.stackSize; + player.inventory.mainInventory[i] = null; + } else { + // Need only part of stack + stack.splitStack(needed); + needed = 0; + } + } + + player.inventory.markDirty(); + return true; + } + + private void sendError(EntityPlayerMP player, String message) { + player.addChatMessage(new net.minecraft.util.ChatComponentText( + net.minecraft.util.EnumChatFormatting.RED + "[Auction] " + message)); + } + + private void sendTradesUpdate(EntityPlayerMP player) { + // Send full trades data to refresh the client + AuctionController controller = AuctionController.getInstance(); + if (controller != null) { + sendTradesData(player, controller); + } + } + + private void sendListingsUpdate(EntityPlayerMP player) { + NBTTagCompound compound = new NBTTagCompound(); + compound.setBoolean("ListingsRefresh", true); + GuiDataPacket.sendGuiData(player, compound); + } + + private enum Action { + Bid, + Buyout, + Cancel, + ClaimItem, + ClaimCurrency, + CreateListing, + OpenPage, + OpenBidding, + RequestListings + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java index 5b7cb7dfe..a04628962 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java @@ -53,7 +53,7 @@ public class GetTraderData extends AbstractPacket { // Player balance PlayerData data = PlayerData.get(player); - compound.setLong("Balance", data.currencyData.getBalance()); + compound.setLong("Balance", data.tradeData.getBalance()); // Stock info compound.setBoolean("StockEnabled", role.stock.enableStock); diff --git a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java index 3b9f56690..48ac3391b 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java @@ -54,7 +54,13 @@ public final class SpecialKeyStatePacket extends AbstractPacket { PlayerData data = PlayerDataController.Instance.getPlayerData((EntityPlayerMP) player); if (data != null) { + boolean wasDown = data.isSpecialKeyDown(); data.setSpecialKeyDown(keyDown); + + // Trigger ability activation on key press (rising edge) + if (keyDown && !wasDown) { + data.abilityData.activateAbility(player); + } } } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java new file mode 100644 index 000000000..8febd9fc0 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java @@ -0,0 +1,51 @@ +package kamkeel.npcs.network.packets.request.ability; + +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 net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to get list of all custom ability preset UUIDs/names. + */ +public final class CustomAbilitiesGetPacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilitiesGet"; + + public CustomAbilitiesGetPacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.CustomAbilitiesGet; + } + + @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, EnumItemPacketType.WAND, player)) + return; + + NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java new file mode 100644 index 000000000..d95051a66 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java @@ -0,0 +1,70 @@ +package kamkeel.npcs.network.packets.request.ability; + +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.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 java.io.IOException; + +/** + * Request packet to get a specific custom ability by UUID. + */ +public final class CustomAbilityGetPacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilityGet"; + + private String uuid; + + public CustomAbilityGetPacket() { + } + + public CustomAbilityGetPacket(String uuid) { + this.uuid = uuid; + } + + @Override + public Enum getType() { + return EnumRequestPacket.CustomAbilityGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, uuid); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String id = ByteBufUtils.readString(in); + Ability ability = AbilityController.Instance.getCustomAbility(id); + if (ability != null) { + NBTTagCompound compound = ability.writeNBT(); + // Include the UUID so the client can track it + compound.setString("_uuid", id); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java new file mode 100644 index 000000000..3ef040700 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java @@ -0,0 +1,69 @@ +package kamkeel.npcs.network.packets.request.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.AbilityController; +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.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to remove a custom ability by UUID. + */ +public final class CustomAbilityRemovePacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilityRemove"; + + private String uuid; + + public CustomAbilityRemovePacket() { + } + + public CustomAbilityRemovePacket(String uuid) { + this.uuid = uuid; + } + + @Override + public Enum getType() { + return EnumRequestPacket.CustomAbilityRemove; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.GLOBAL_ABILITY; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, uuid); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String id = ByteBufUtils.readString(in); + AbilityController.Instance.deleteCustomAbility(id); + NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java new file mode 100644 index 000000000..d5ca1c556 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java @@ -0,0 +1,78 @@ +package kamkeel.npcs.network.packets.request.ability; + +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.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.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to save a custom ability preset. + */ +public final class CustomAbilitySavePacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilitySave"; + + private NBTTagCompound abilityNBT; + + public CustomAbilitySavePacket() { + } + + public CustomAbilitySavePacket(NBTTagCompound abilityNBT) { + this.abilityNBT = abilityNBT; + } + + @Override + public Enum getType() { + return EnumRequestPacket.CustomAbilitySave; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.GLOBAL_ABILITY; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeNBT(out, abilityNBT); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + NBTTagCompound compound = ByteBufUtils.readNBT(in); + Ability ability = AbilityController.Instance.fromNBT(compound); + if (ability != null) { + AbilityController.Instance.saveCustomAbility(ability); + NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); + NBTTagCompound responseNbt = ability.writeNBT(); + responseNbt.setString("_uuid", ability.getId()); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, responseNbt); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java index a2bc51d0a..39b60a40f 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java @@ -44,5 +44,6 @@ public final class AnimationsGetPacket extends AbstractPacket { return; NoppesUtilServer.sendAnimationDataAll((EntityPlayerMP) player); + NoppesUtilServer.sendBuiltInAnimationData((EntityPlayerMP) player); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java new file mode 100644 index 000000000..e214689d8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java @@ -0,0 +1,72 @@ +package kamkeel.npcs.network.packets.request.animation; + +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.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; + +import java.io.IOException; + +/** + * Packet to request a built-in animation by name. + * Built-in animations don't have IDs, so we fetch by name. + */ +public final class BuiltInAnimationGetPacket extends AbstractPacket { + public static String packetName = "Request|BuiltInAnimationGet"; + + private String animationName; + + public BuiltInAnimationGetPacket() { + } + + public BuiltInAnimationGetPacket(String animationName) { + this.animationName = animationName; + } + + @Override + public Enum getType() { + return EnumRequestPacket.BuiltInAnimationGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, animationName); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String name = ByteBufUtils.readString(in); + BuiltInAnimation animation = AnimationController.getInstance().getBuiltInAnimation(name); + if (animation != null) { + NBTTagCompound compound = animation.writeToNBT(); + // Mark as built-in so client knows + compound.setBoolean("BuiltIn", true); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java index 0fd7de927..ce94c98f9 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java @@ -84,6 +84,10 @@ public final class BlockScriptPacket extends AbstractPacket { NBTTagCompound compound = ((TileScripted) tile).getNBT(new NBTTagCompound()); compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + + NBTTagCompound loadComplete = new NBTTagCompound(); + loadComplete.setInteger("LoadComplete", 1); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, loadComplete); } else { if (!player.capabilities.isCreativeMode) { return; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java new file mode 100644 index 000000000..700513d78 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java @@ -0,0 +1,106 @@ +package kamkeel.npcs.network.packets.request.script; + +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.enums.EnumRequestPacket; +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.NBTTags; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.ScriptController; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public final class ScriptFilesPacket extends AbstractPacket { + public static String packetName = "Request|ScriptFiles"; + + private String lang; + + public ScriptFilesPacket() { + } + + public ScriptFilesPacket(String lang) { + this.lang = lang; + } + + @Override + public Enum getType() { + return EnumRequestPacket.ScriptFiles; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.SCRIPT_GLOBAL; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeNBT(out, getScriptsNbt(lang)); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + NBTTagCompound compound = ByteBufUtils.readNBT(in); + + String lang = compound.getString("Lang"); + String ext = compound.getString("Ext"); + Map scripts = NBTTags.getStringStringMap(compound.getTagList("Scripts", 10)); + + ScriptController cont = ScriptController.Instance; + if (!cont.languages.containsKey(lang) && ext != null) + cont.languages.put(lang, ext); + + for (Map.Entry script : scripts.entrySet()) { + cont.scripts.put(script.getKey(), script.getValue()); + } + + //Set client side configs + ConfigScript.ScriptingEnabled = compound.getBoolean("ScriptingEnabled"); + ConfigScript.RunLoadedScriptsFirst = compound.getBoolean("LoadedFirst"); + ScriptController.Instance.globalRevision = compound.getInteger("GlobalRevision"); + } + + public NBTTagCompound getScriptsNbt(String lang) { + NBTTagCompound compound = new NBTTagCompound(); + compound.setString("Lang", lang); + + Map scriptss = new HashMap<>(); + String ext = ScriptController.Instance.languages.get(lang); + if (ext != null) { + for (Map.Entry script : ScriptController.Instance.scripts.entrySet()) { + if (script.getKey().endsWith(ext)) + scriptss.put(script.getKey(), script.getValue()); + } + compound.setString("Ext", ext); + } + + //Send server configs to client + compound.setBoolean("ScriptingEnabled", ConfigScript.ScriptingEnabled); + compound.setBoolean("LoadedFirst", ConfigScript.RunLoadedScriptsFirst); + compound.setInteger("GlobalRevision", ScriptController.Instance.globalRevision); + compound.setTag("Scripts", NBTTags.nbtStringStringMap(scriptss)); + return compound; + } + + public static void sendToAll(String lang) { + PacketHandler.Instance.sendToAll(new ScriptFilesPacket(lang)); + } + + public static void sendToPlayer(EntityPlayerMP player, String lang) { + PacketHandler.Instance.sendToPlayer(new ScriptFilesPacket(lang), player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java index b4a8c619e..c1a7c74c5 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java @@ -16,7 +16,7 @@ import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemStack; import noppes.npcs.config.ConfigScript; -import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.item.ScriptCustomItem; @@ -72,10 +72,10 @@ public final class ItemScriptErrorPacket extends AbstractPacket { if (requestedAction == Action.GET) { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : ((ScriptCustomItem) iw).scripts) { + for (IScriptUnit script : ((ScriptCustomItem) iw).scripts) { ++tab; - for (Map.Entry longStringEntry : script.console.entrySet()) { + for (Map.Entry longStringEntry : script.getConsole().entrySet()) { map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); } } @@ -84,8 +84,8 @@ public final class ItemScriptErrorPacket extends AbstractPacket { compound.setTag("ItemScriptConsole", NBTTags.NBTLongStringMap(map)); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); } else { - for (ScriptContainer script : ((ScriptCustomItem) iw).scripts) { - script.console.clear(); + for (IScriptUnit script : ((ScriptCustomItem) iw).scripts) { + script.clearConsole(); } } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java index 478340a63..7d21242bd 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java @@ -75,6 +75,10 @@ public final class ItemScriptPacket extends AbstractPacket { NBTTagCompound compound = iw.getMCNbt(); compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + + NBTTagCompound loadComplete = new NBTTagCompound(); + loadComplete.setInteger("LoadComplete", 1); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, loadComplete); } else { if (!player.capabilities.isCreativeMode) { return; diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java new file mode 100644 index 000000000..e095dc61b --- /dev/null +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -0,0 +1,348 @@ +package kamkeel.npcs.util; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.util.Vec3; +import noppes.npcs.constants.EnumAnimationPart; +import noppes.npcs.controllers.data.AnimationData; +import noppes.npcs.controllers.data.Frame; +import noppes.npcs.controllers.data.FramePart; +import noppes.npcs.entity.EntityCustomNpc; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.entity.data.ModelScalePart; + +/** + * Utility class for calculating world positions based on anchor points. + * Supports both EntityNPCInterface and EntityPlayer with animation and model size awareness. + */ +public class AnchorPointHelper { + + // Default biped model constants (in model units, 1 unit = 1/16 block) + private static final float MODEL_SCALE = 1f / 16f; + + // Shoulder position relative to model origin + private static final float SHOULDER_Y = 2f; + private static final float SHOULDER_X_OFFSET = 5f; // Distance from center to shoulder + + // Palm position (in arm's local space, relative to shoulder) + // Arm is 12 units long; palm is near the end + private static final float PALM_DOWN = 11f; // Distance down the arm to palm center + private static final float PALM_FORWARD = 2f; // Forward offset to palm surface (not center of hand) + private static final float PALM_INWARD = 0f; // Lateral offset (0 = centered on arm) + + // Default height offsets as fraction of entity height + private static final float FRONT_HEIGHT = 0.7f; + private static final float CENTER_HEIGHT = 0.5f; + private static final float ARM_HEIGHT = 0.75f; + private static final float ABOVE_HEAD_HEIGHT = 1.2f; + private static final float CHEST_HEIGHT = 0.65f; + + // Distance in front for FRONT anchor + private static final float DEFAULT_FRONT_DISTANCE = 1.0f; + + // Fallback arm positioning (when no animation, arms hang at sides) + private static final float FALLBACK_LATERAL = 0.35f; // Distance from body center to arm + private static final float FALLBACK_FORWARD = 0.1f; // Slight forward offset + private static final float FALLBACK_ARM_HEIGHT = 0.45f; // Hand height when arms hang down (near hip) + + /** + * Calculate world position for an anchor point on an entity. + */ + public static Vec3 calculateAnchorPosition(EntityLivingBase entity, EnergyAnchorData anchorData) { + return calculateAnchorPosition(entity, anchorData, DEFAULT_FRONT_DISTANCE); + } + + /** + * Calculate world position for an anchor point with custom front offset. + */ + public static Vec3 calculateAnchorPosition(EntityLivingBase entity, EnergyAnchorData anchorData, float frontOffset) { + float scale = getModelScale(entity); + float height = entity.height; + + AnchorPoint anchor = anchorData.anchorPoint; + + float anchorX = anchorData.anchorOffsetX; + float anchorY = anchorData.anchorOffsetY; + float anchorZ = anchorData.anchorOffsetZ; + + double x = entity.posX; + double y = entity.posY; + double z = entity.posZ; + + float headYaw = (float) Math.toRadians(entity.rotationYawHead); + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + + switch (anchor) { + case FRONT: + Vec3 look = entity.getLookVec(); + x += look.xCoord * frontOffset * scale; + y += height * FRONT_HEIGHT + look.yCoord * frontOffset * scale; + z += look.zCoord * frontOffset * scale; + break; + + case CENTER: + y += height * CENTER_HEIGHT; + break; + + case RIGHT_HAND: + return calculateHandPosition(entity, anchorData, true, scale); + + case LEFT_HAND: + return calculateHandPosition(entity, anchorData, false, scale); + + case ABOVE_HEAD: + y += height * ABOVE_HEAD_HEIGHT; + break; + + case CHEST: + y += height * CHEST_HEIGHT; + break; + } + + x += anchorX * scale; + y += anchorY * scale; + z += anchorZ * scale; + + return Vec3.createVectorHelper(x, y, z); + } + + /** + * Get model scale factor for an entity. + * NPCs have configurable model size, players use default scale. + */ + private static float getModelScale(EntityLivingBase entity) { + if (entity instanceof EntityNPCInterface) { + int modelSize = ((EntityNPCInterface) entity).display.modelSize; + return modelSize / 5f; // modelSize 5 = 100% scale + } + return 1f; + } + + // Default arm scale (no scaling) + private static final ModelScalePart DEFAULT_ARM_SCALE = new ModelScalePart(); + + /** + * Get arm scale for an entity. + * EntityCustomNpc has per-part scaling, others use default (1,1,1). + */ + private static ModelScalePart getArmScale(EntityLivingBase entity) { + if (entity instanceof EntityCustomNpc) { + return ((EntityCustomNpc) entity).modelData.modelScale.arms; + } + return DEFAULT_ARM_SCALE; + } + + /** + * Calculate hand position using animation data if available. + */ + private static Vec3 calculateHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) { + // Try to get animated position first + Vec3 animatedPos = getAnimatedHandPosition(entity, anchor, rightHand, scale); + if (animatedPos != null) { + return animatedPos; + } + + // Fallback to static position + return calculateFallbackHandPosition(entity, anchor, rightHand, scale); + } + + /** + * Calculate hand position from animation data. + * Works for both NPCs and Players. + */ + private static Vec3 getAnimatedHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) { + AnimationData animData = AnimationData.getData(entity); + if (animData == null || !animData.isActive() || animData.animation == null) { + return null; + } + + Frame frame = (Frame) animData.animation.currentFrame(); + if (frame == null) { + return null; + } + + EnumAnimationPart armPart = rightHand ? EnumAnimationPart.RIGHT_ARM : EnumAnimationPart.LEFT_ARM; + FramePart part = frame.frameParts.get(armPart); + if (part == null) { + return null; + } + + // Get arm scale for EntityCustomNpc + ModelScalePart armScale = getArmScale(entity); + + // Get rotation and pivot values + float[] rotations = getRotations(part, entity); + float[] pivots = getPivots(part, entity); + + // Shoulder position in model space + float shoulderX = rightHand ? -SHOULDER_X_OFFSET : SHOULDER_X_OFFSET; + float shoulderY = SHOULDER_Y; + float shoulderZ = 0f; + + // Add pivot offsets + shoulderX += pivots[0]; + shoulderY += pivots[1]; + shoulderZ += pivots[2]; + + // Palm offset in arm's local space (before rotation) + // Apply arm scaling to the palm position + float palmX = (rightHand ? -PALM_INWARD : PALM_INWARD) * armScale.scaleX; + float palmY = PALM_DOWN * armScale.scaleY; + float palmZ = PALM_FORWARD * armScale.scaleZ; + + // Apply arm rotations (order: X -> Y -> Z, matching GL's reverse application) + double hx = palmX, hy = palmY, hz = palmZ; + + // X rotation first + double cosX = Math.cos(rotations[0]); + double sinX = Math.sin(rotations[0]); + double newY = hy * cosX - hz * sinX; + double newZ = hy * sinX + hz * cosX; + hy = newY; + hz = newZ; + + // Y rotation second + double cosY = Math.cos(rotations[1]); + double sinY = Math.sin(rotations[1]); + double newX = hx * cosY + hz * sinY; + newZ = -hx * sinY + hz * cosY; + hx = newX; + hz = newZ; + + // Z rotation last + double cosZ = Math.cos(rotations[2]); + double sinZ = Math.sin(rotations[2]); + newX = hx * cosZ - hy * sinZ; + newY = hx * sinZ + hy * cosZ; + hx = newX; + hy = newY; + + // Combine shoulder + rotated hand offset + double modelX = shoulderX + hx; + double modelY = shoulderY + hy; + double modelZ = shoulderZ + hz; + + // Convert to blocks and apply model scale + double blockX = modelX * MODEL_SCALE * scale; + double blockY = modelY * MODEL_SCALE * scale; + double blockZ = modelZ * MODEL_SCALE * scale; + + // Apply body yaw rotation + // Renderer uses glRotate(180 - yaw), so: cos(180-yaw) = -cos(yaw), sin(180-yaw) = sin(yaw) + // Y-axis rotation: x' = x*cos + z*sin, z' = -x*sin + z*cos + // Substituting: x' = -x*cos(yaw) + z*sin(yaw), z' = -x*sin(yaw) - z*cos(yaw) + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + double cosYaw = Math.cos(bodyYaw); + double sinYaw = Math.sin(bodyYaw); + + double worldOffsetX = blockX * cosYaw + blockZ * sinYaw; + double worldOffsetZ = blockX * sinYaw - blockZ * cosYaw; + + // Calculate shoulder height based on entity and scale + float shoulderHeight = entity.height * ARM_HEIGHT; + + // Final world position + double worldX = entity.posX + worldOffsetX; + double worldY = entity.posY + shoulderHeight - blockY; + double worldZ = entity.posZ + worldOffsetZ; + + worldX += anchor.anchorOffsetX * scale; + worldY += anchor.anchorOffsetY * scale; + worldZ += anchor.anchorOffsetZ * scale; + + return Vec3.createVectorHelper(worldX, worldY, worldZ); + } + + /** + * Get rotation values from FramePart. + * Uses interpolated values on client, raw values on server. + */ + private static float[] getRotations(FramePart part, EntityLivingBase entity) { + if (entity.worldObj.isRemote) { + // Client side - use interpolated values (already in radians) + part.interpolateAngles(); + return part.prevRotations; + } else { + // Server side - convert raw degrees to radians + float pi = (float) Math.PI / 180f; + return new float[]{ + part.rotation[0] * pi, + part.rotation[1] * pi, + part.rotation[2] * pi + }; + } + } + + /** + * Get pivot values from FramePart. + * Uses interpolated values on client, raw values on server. + */ + private static float[] getPivots(FramePart part, EntityLivingBase entity) { + if (entity.worldObj.isRemote) { + // Client side - use interpolated values + part.interpolateOffset(); + return part.prevPivots; + } else { + // Server side - use raw values + return part.pivot; + } + } + + /** + * Calculate fallback hand position when no animation is active. + * Represents arms hanging at sides in default idle pose. + */ + private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) { + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + + // Get arm scale for EntityCustomNpc + ModelScalePart armScale = getArmScale(entity); + + // Perpendicular direction for lateral offset (right = +90°, left = -90° from body facing) + float perpYaw = bodyYaw + (rightHand ? (float) (Math.PI / 2) : (float) (-Math.PI / 2)); + + // Calculate lateral offset - arms hang at sides + // Scale by model size, but not by arm scale (lateral distance is fixed by skeleton) + double lateralX = -Math.sin(perpYaw) * FALLBACK_LATERAL * scale; + double lateralZ = Math.cos(perpYaw) * FALLBACK_LATERAL * scale; + + // Small forward offset + double forwardX = -Math.sin(bodyYaw) * FALLBACK_FORWARD * scale; + double forwardZ = Math.cos(bodyYaw) * FALLBACK_FORWARD * scale; + + // Arm length affects vertical position - longer arms = lower hands + double armLengthOffset = armScale.scaleY * 0.3 * scale; + + double x = entity.posX + lateralX + forwardX; + double y = entity.posY + entity.height * FALLBACK_ARM_HEIGHT + armLengthOffset; + double z = entity.posZ + lateralZ + forwardZ; + + x += anchor.anchorOffsetX * scale; + y += anchor.anchorOffsetY * scale; + z += anchor.anchorOffsetZ * scale; + + return Vec3.createVectorHelper(x, y, z); + } + + /** + * Get default height multiplier for an anchor point. + */ + public static float getHeightMultiplier(AnchorPoint anchor) { + switch (anchor) { + case FRONT: + return FRONT_HEIGHT; + case CENTER: + return CENTER_HEIGHT; + case RIGHT_HAND: + case LEFT_HAND: + return ARM_HEIGHT; + case ABOVE_HEAD: + return ABOVE_HEAD_HEIGHT; + case CHEST: + return CHEST_HEIGHT; + default: + return FRONT_HEIGHT; + } + } +} diff --git a/src/main/java/kamkeel/npcs/util/BukkitUtil.java b/src/main/java/kamkeel/npcs/util/BukkitUtil.java index 61b4deff5..344916633 100644 --- a/src/main/java/kamkeel/npcs/util/BukkitUtil.java +++ b/src/main/java/kamkeel/npcs/util/BukkitUtil.java @@ -76,6 +76,7 @@ public class BukkitUtil { /** * Checks if a specific Bukkit plugin is enabled. + * * @param pluginName the plugin name to check * @return true if the plugin is enabled */ @@ -107,6 +108,7 @@ public class BukkitUtil { /** * Gets a Bukkit OfflinePlayer by name. + * * @param playerName the player name * @return the {@link org.bukkit.OfflinePlayer} object, or null if not available */ @@ -124,6 +126,7 @@ public class BukkitUtil { /** * Gets a Bukkit Player by name (online players only). + * * @param playerName the player name * @return the {@link org.bukkit.entity.Player} object, or null if not online or not available */ @@ -140,6 +143,7 @@ public class BukkitUtil { /** * Gets a service provider from Bukkit's ServicesManager. + * * @param serviceClass the service class to get * @return the service provider, or null if not available */ diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java new file mode 100644 index 000000000..0f1400ab3 --- /dev/null +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -0,0 +1,68 @@ +package kamkeel.npcs.util; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import noppes.npcs.LogWriter; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class Register { + private final String registryKey; + protected final String namespace; + protected final Map> entries = new LinkedHashMap<>(); + + public Register(String registryKey, String namespace) { + this.registryKey = registryKey; + this.namespace = namespace; + } + + public T register(String factoryName, Supplier factory) { + entries.put(registryKey + "." + namespace + ":" + factoryName, factory); + return factory.get(); + } + + public static class Abilities extends Register { + public Abilities(String namespace) { + super("ability", namespace); + } + + public void register() { + for (Map.Entry> entry : entries.entrySet()) { + AbilityController.Instance.registerType(entry.getKey(), entry.getValue()); + } + } + } + + public static class Animations extends Register { + private final Class modClass; + private final String animationsPath; + + public Animations(Class modClass, String animationsPath, String namespace) { + super("ability", namespace); + this.animationsPath = animationsPath; + this.modClass = modClass; + } + + @Override + public Animation register(String factoryName, Supplier factory) { + return super.register(factoryName, factory); + } + + public void register() { + try { + String path = "/assets/" + namespace + "/" + animationsPath; + for (Map.Entry> entry : entries.entrySet()) { + String name = entry.getKey().substring(entry.getKey().indexOf(":") + 1); + AnimationController.Instance.loadBuiltInAnimation(modClass, path, name); + } + } catch (Exception e) { + LogWriter.error("Error scanning built-in animations folder", e); + } + } + } +} diff --git a/src/main/java/kamkeel/npcs/util/VaultUtil.java b/src/main/java/kamkeel/npcs/util/VaultUtil.java index 4db7e4693..6eb10ee83 100644 --- a/src/main/java/kamkeel/npcs/util/VaultUtil.java +++ b/src/main/java/kamkeel/npcs/util/VaultUtil.java @@ -141,6 +141,7 @@ public class VaultUtil { /** * Checks if a player has an account in the economy system. + * * @param player the player to check * @return true if the player has an account */ @@ -150,6 +151,7 @@ public class VaultUtil { /** * Checks if a player has an account in the economy system. + * * @param playerName the player name to check * @return true if the player has an account */ @@ -170,6 +172,7 @@ public class VaultUtil { /** * Gets the balance of a player. + * * @param player the player * @return the player's balance, or 0 if not available */ @@ -179,6 +182,7 @@ public class VaultUtil { /** * Gets the balance of a player by name. + * * @param playerName the player name * @return the player's balance, or 0 if not available */ @@ -199,6 +203,7 @@ public class VaultUtil { /** * Checks if a player has at least the specified amount. + * * @param player the player * @param amount the amount to check * @return true if the player has at least the specified amount @@ -209,8 +214,9 @@ public class VaultUtil { /** * Checks if a player has at least the specified amount. + * * @param playerName the player name - * @param amount the amount to check + * @param amount the amount to check * @return true if the player has at least the specified amount */ public static boolean has(String playerName, double amount) { @@ -230,6 +236,7 @@ public class VaultUtil { /** * Withdraws money from a player's account. + * * @param player the player * @param amount the amount to withdraw * @return true if the transaction was successful @@ -240,8 +247,9 @@ public class VaultUtil { /** * Withdraws money from a player's account. + * * @param playerName the player name - * @param amount the amount to withdraw + * @param amount the amount to withdraw * @return true if the transaction was successful */ public static boolean withdrawMoney(String playerName, double amount) { @@ -264,6 +272,7 @@ public class VaultUtil { /** * Adds money to a player's account (deposit). + * * @param player the player * @param amount the amount to add * @return true if the transaction was successful @@ -274,8 +283,9 @@ public class VaultUtil { /** * Adds money to a player's account (deposit). + * * @param playerName the player name - * @param amount the amount to add + * @param amount the amount to add * @return true if the transaction was successful */ public static boolean addMoney(String playerName, double amount) { @@ -299,6 +309,7 @@ public class VaultUtil { /** * Sets a player's balance to the specified amount. * This withdraws all money first, then deposits the new amount. + * * @param player the player * @param amount the new balance * @return true if the transaction was successful @@ -310,8 +321,9 @@ public class VaultUtil { /** * Sets a player's balance to the specified amount. * This withdraws all money first, then deposits the new amount. + * * @param playerName the player name - * @param amount the new balance + * @param amount the new balance * @return true if the transaction was successful */ public static boolean setBalance(String playerName, double amount) { @@ -331,6 +343,7 @@ public class VaultUtil { /** * Formats an amount according to the economy plugin's settings. + * * @param amount the amount to format * @return the formatted string, or the raw number as string if not available */ @@ -347,6 +360,7 @@ public class VaultUtil { /** * Gets the plural currency name (e.g., "Dollars"). + * * @return the plural currency name, or "coins" if not available */ public static String getCurrencyNamePlural() { @@ -362,6 +376,7 @@ public class VaultUtil { /** * Gets the singular currency name (e.g., "Dollar"). + * * @return the singular currency name, or "coin" if not available */ public static String getCurrencyNameSingular() { @@ -377,6 +392,7 @@ public class VaultUtil { /** * Creates an account for the player if one doesn't exist. + * * @param player the player * @return true if the account was created or already exists */ @@ -386,6 +402,7 @@ public class VaultUtil { /** * Creates an account for the player if one doesn't exist. + * * @param playerName the player name * @return true if the account was created or already exists */ @@ -422,6 +439,7 @@ public class VaultUtil { /** * Gets a list of all bank names. + * * @return list of bank names, or empty list if not available */ @SuppressWarnings("unchecked") @@ -438,6 +456,7 @@ public class VaultUtil { /** * Gets the balance of a bank. + * * @param bankName the bank name * @return the bank balance, or 0 if not available */ @@ -455,8 +474,9 @@ public class VaultUtil { /** * Checks if a bank has at least the specified amount. + * * @param bankName the bank name - * @param amount the amount to check + * @param amount the amount to check * @return true if the bank has at least the specified amount */ public static boolean bankHas(String bankName, double amount) { @@ -473,8 +493,9 @@ public class VaultUtil { /** * Withdraws money from a bank. + * * @param bankName the bank name - * @param amount the amount to withdraw + * @param amount the amount to withdraw * @return true if the transaction was successful */ public static boolean bankWithdraw(String bankName, double amount) { @@ -491,8 +512,9 @@ public class VaultUtil { /** * Deposits money into a bank. + * * @param bankName the bank name - * @param amount the amount to deposit + * @param amount the amount to deposit * @return true if the transaction was successful */ public static boolean bankDeposit(String bankName, double amount) { @@ -509,8 +531,9 @@ public class VaultUtil { /** * Transfers money from one player to another. - * @param from the player to take money from - * @param to the player to give money to + * + * @param from the player to take money from + * @param to the player to give money to * @param amount the amount to transfer * @return true if the transfer was successful */ @@ -520,9 +543,10 @@ public class VaultUtil { /** * Transfers money from one player to another. + * * @param fromPlayerName the player name to take money from - * @param toPlayerName the player name to give money to - * @param amount the amount to transfer + * @param toPlayerName the player name to give money to + * @param amount the amount to transfer * @return true if the transfer was successful */ public static boolean transfer(String fromPlayerName, String toPlayerName, double amount) { diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java index 58f931ab8..6fefe01c1 100644 --- a/src/main/java/noppes/npcs/CommonProxy.java +++ b/src/main/java/noppes/npcs/CommonProxy.java @@ -14,6 +14,11 @@ import net.minecraftforge.common.util.FakePlayer; import noppes.npcs.api.IWorld; import noppes.npcs.blocks.tiles.TileNpcContainer; import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.containers.ContainerAuction; +import noppes.npcs.containers.ContainerAuctionBidding; +import noppes.npcs.containers.ContainerAuctionTrades; +import noppes.npcs.containers.ContainerAuctionListing; +import noppes.npcs.containers.ContainerAuctionSell; import noppes.npcs.containers.ContainerAnvilRepair; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; @@ -92,6 +97,18 @@ public class CommonProxy implements IGuiHandler { if (gui == EnumGuiType.PlayerTrader) return new ContainerNPCTrader(npc, player); + if (gui == EnumGuiType.PlayerAuction) + return new ContainerAuctionListing(npc, player); + + if (gui == EnumGuiType.PlayerAuctionSell) + return new ContainerAuctionSell(npc, player); + + if (gui == EnumGuiType.PlayerAuctionTrades) + return new ContainerAuctionTrades(npc, player); + + if (gui == EnumGuiType.PlayerAuctionBidding) + return new ContainerAuctionBidding(npc, player); + if (gui == EnumGuiType.PlayerCarpentryBench) return new ContainerCarpentryBench(player.inventory, player.worldObj, x, y, z); @@ -200,4 +217,7 @@ public class CommonProxy implements IGuiHandler { public boolean isGUIOpen() { return false; } + + public void buildPackageIndex() { + } } diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 41cd8770e..0bc081429 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -5,6 +5,7 @@ import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.Mod.EventHandler; import cpw.mods.fml.common.SidedProxy; import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLLoadCompleteEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import cpw.mods.fml.common.event.FMLServerAboutToStartEvent; import cpw.mods.fml.common.event.FMLServerStartedEvent; @@ -13,13 +14,18 @@ import cpw.mods.fml.common.event.FMLServerStoppedEvent; import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.common.registry.EntityRegistry; import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import foxz.command.CommandNoppes; +import io.github.somehussar.janinoloader.api.IDynamicCompiler; +import io.github.somehussar.janinoloader.api.IDynamicCompilerBuilder; import kamkeel.npcs.addon.AddonManager; import kamkeel.npcs.command.CommandKamkeel; import kamkeel.npcs.command.profile.CommandProfile; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.TelegraphController; +import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.profile.CNPCData; import kamkeel.npcs.developer.Developer; import kamkeel.npcs.network.PacketHandler; @@ -50,6 +56,7 @@ import noppes.npcs.config.ConfigMain; import noppes.npcs.config.LoadConfiguration; import noppes.npcs.config.legacy.LegacyConfig; import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.BankController; import noppes.npcs.controllers.ChunkController; import noppes.npcs.controllers.CustomEffectController; @@ -70,7 +77,6 @@ import noppes.npcs.controllers.SpawnController; import noppes.npcs.controllers.TagController; import noppes.npcs.controllers.TransportController; import noppes.npcs.enchants.EnchantInterface; -import kamkeel.npcs.controllers.data.ability.AbilityController; import noppes.npcs.entity.EntityChairMount; import noppes.npcs.entity.EntityCustomNpc; import noppes.npcs.entity.EntityMagicProjectile; @@ -80,6 +86,11 @@ import noppes.npcs.entity.EntityNpcDragon; import noppes.npcs.entity.EntityNpcPony; import noppes.npcs.entity.EntityNpcSlime; import noppes.npcs.entity.EntityProjectile; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilitySweeper; import noppes.npcs.entity.old.EntityNPCDwarfFemale; import noppes.npcs.entity.old.EntityNPCDwarfMale; import noppes.npcs.entity.old.EntityNPCElfFemale; @@ -99,11 +110,14 @@ import noppes.npcs.entity.old.EntityNpcNagaFemale; import noppes.npcs.entity.old.EntityNpcNagaMale; import noppes.npcs.entity.old.EntityNpcSkeleton; import noppes.npcs.scripted.NpcAPI; +import somehussar.janino.AdvancedClassFilter; import java.io.File; +import java.util.HashSet; import java.util.Set; +import java.util.function.Consumer; -@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.3") +@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta1") public class CustomNpcs { @SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy") @@ -140,10 +154,56 @@ public class CustomNpcs { public static MinecraftServer Server; + private static IDynamicCompiler globalJaninoCompiler; + @SideOnly(Side.CLIENT) + private static IDynamicCompiler clientJaninoCompiler; + @SideOnly(Side.CLIENT) + private static Set> clientClassFilterConsumer; + + static { + if (FMLCommonHandler.instance().getEffectiveSide().isClient()) { + clientClassFilterConsumer = new HashSet<>(); + } + } + + @SideOnly(Side.CLIENT) + public static void addClassesToClientClassFilter(Consumer consumer) { + clientClassFilterConsumer.add(consumer); + } + public CustomNpcs() { instance = this; } + public static IDynamicCompiler getDynamicCompiler() { + if (globalJaninoCompiler == null) { + globalJaninoCompiler = IDynamicCompilerBuilder.createBuilder().getCompiler(); + } + + return globalJaninoCompiler; + } + + @SideOnly(Side.CLIENT) + public static IDynamicCompiler getClientCompiler() { + + if (clientJaninoCompiler == null) { + AdvancedClassFilter filter = new AdvancedClassFilter() + .addRegexes("noppes\\.npcs\\.api\\..*") + .banRegexes(".*ClassLoader.*", + ".*File*.", + "java\\.lang\\.reflect\\..*"); + + for (Consumer consumer : clientClassFilterConsumer) { + consumer.accept(filter); + } + + + clientJaninoCompiler = IDynamicCompilerBuilder.createBuilder().setClassFilter(filter).getCompiler(); + } + + return clientJaninoCompiler; + } + @EventHandler public void load(FMLPreInitializationEvent ev) { PacketHandler.Instance = new PacketHandler(); @@ -227,6 +287,11 @@ public class CustomNpcs { registerNewEntity(EntityChairMount.class, "CustomNpcChairMount", 64, 10, false); registerNewEntity(EntityProjectile.class, "throwableitem", 64, 3, true); registerNewEntity(EntityMagicProjectile.class, "magicprojectile", 64, 3, true); + registerNewEntity(EntityAbilityOrb.class, "abilityorb", 64, 3, true); + registerNewEntity(EntityAbilityDisc.class, "abilitydisc", 64, 3, true); + registerNewEntity(EntityAbilityLaser.class, "abilitylaser", 64, 3, true); + registerNewEntity(EntityAbilityBeam.class, "abilitybeam", 64, 3, true); + registerNewEntity(EntityAbilitySweeper.class, "abilitysweeper", 64, 3, true); new RecipeController(); @@ -259,8 +324,17 @@ public class CustomNpcs { PacketHandler.Instance.registerChannels(); } + @EventHandler + public void loadComplete(FMLLoadCompleteEvent ev) { + proxy.buildPackageIndex(); + } + @EventHandler public void setAboutToStart(FMLServerAboutToStartEvent event) { + globalJaninoCompiler = null; + if (FMLCommonHandler.instance().getSide().isClient()) + clientJaninoCompiler = null; + Server = event.getServer(); ChunkController.Instance.clear(); FactionController.getInstance().load(); @@ -272,8 +346,8 @@ public class CustomNpcs { new SpawnController(); new LinkedNpcController(); new AnimationController(); - new AbilityController(); AbilityController.Instance.load(); + TelegraphController.init(); LinkedItemController.getInstance().load(); @@ -312,6 +386,7 @@ public class CustomNpcs { public void started(FMLServerStartedEvent event) { RecipeController.Instance.load(); new BankController(); + new AuctionController(); DialogController.Instance.load(); QuestController.Instance.load(); ScriptController.HasStart = true; @@ -332,6 +407,15 @@ public class CustomNpcs { ScriptController.Instance.saveForgeScripts(); ScriptController.Instance.savePlayerScripts(); ScriptController.Instance.saveGlobalNpcScripts(); + + // Save auction data synchronously on shutdown + if (AuctionController.Instance != null) { + AuctionController.Instance.save(); + } + + if (FMLCommonHandler.instance().getSide().isClient()) + clientJaninoCompiler = null; + globalJaninoCompiler = null; } @EventHandler diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 119f3e42e..6f99dd8fc 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -67,6 +67,7 @@ public class CustomNpcsPermissions { public static final Permission GLOBAL_NATURALSPAWN = new Permission("customnpcs.global.naturalspawn"); public static final Permission GLOBAL_TAG = new Permission("customnpcs.global.tag"); public static final Permission GLOBAL_ANIMATION = new Permission("customnpcs.global.animation"); + public static final Permission GLOBAL_ABILITY = new Permission("customnpcs.global.ability"); public static final Permission GLOBAL_MAGIC = new Permission("customnpcs.global.magic"); public static final Permission GLOBAL_EFFECT = new Permission("customnpcs.global.effect"); @@ -111,6 +112,9 @@ public class CustomNpcsPermissions { public static final Permission PROFILE_REGION_BYPASS = new Permission("customnpcs.profile.region-bypass"); public static final Permission PROFILE_MAX = new Permission("customnpcs.profile.max.*"); + // Auction + public static final Permission AUCTION_TRADES_MAX = new Permission("customnpcs.auction.trades.*"); + public static CustomNpcsPermissions Instance; private static boolean isEnabled; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index d9655b20a..8c44c482e 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -1,11 +1,13 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityPhase; import kamkeel.npcs.controllers.data.ability.AbilityController; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.AbilitySlot; +import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -17,9 +19,7 @@ import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Random; /** @@ -35,34 +35,60 @@ public class DataAbilities { // CONFIGURATION (saved to NBT) // ═══════════════════════════════════════════════════════════════════ - /** List of abilities this NPC can use */ - private List abilities = new ArrayList<>(); + /** + * List of ability slots this NPC can use (inline or reference). + */ + private List abilitySlots = new ArrayList<>(); - /** Whether the ability system is enabled for this NPC */ + /** + * Whether the ability system is enabled for this NPC + */ public boolean enabled = false; - /** Minimum ticks between ability selections (global cooldown) */ - public int globalCooldown = 20; + /** + * Minimum cooldown ticks between abilities (global minimum) + */ + public int minCooldown = 80; + + /** + * Maximum cooldown ticks between abilities (global maximum) + */ + public int maxCooldown = 200; // ═══════════════════════════════════════════════════════════════════ // RUNTIME STATE (not saved) // ═══════════════════════════════════════════════════════════════════ - /** Currently executing ability (null if none) */ + /** + * Currently executing ability (null if none) + */ private transient Ability currentAbility; - /** Per-ability cooldown timers (ability ID -> world time when cooldown ends) */ - private transient Map cooldowns = new HashMap<>(); + /** + * World time when NPC cooldown ends (can select next ability) + */ + private transient long cooldownEndTime = 0; - /** Ticks until next ability can be selected (global cooldown timer) */ - private transient int globalCooldownTimer = 0; - - /** Last target used for ability execution */ + /** + * Last target used for ability execution + */ private transient EntityLivingBase lastTarget; - /** Recent hit timestamps for hit count condition */ + /** + * Recent hit timestamps for hit count condition + */ private transient List recentHitTimes = new ArrayList<>(); + /** + * Locked rotation values for ACTIVE phase when movement is locked. + * Stored when entering ACTIVE phase, applied every tick to prevent rotation changes. + */ + private transient boolean rotationLocked = false; + private transient float lockedYaw = 0; + private transient float lockedYawHead = 0; + private transient float lockedRenderYawOffset = 0; + private transient float lockedPitch = 0; + // ═══════════════════════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════════════════════ @@ -84,11 +110,6 @@ public class DataAbilities { return; } - // Tick global cooldown - if (globalCooldownTimer > 0) { - globalCooldownTimer--; - } - // Tick current ability if executing if (currentAbility != null && currentAbility.isExecuting()) { tickCurrentAbility(); @@ -126,9 +147,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(); + } + // Play active sound and animation playAbilitySound(currentAbility.getActiveSound()); - playAbilityAnimation(currentAbility.getActiveAnimationId()); + playAbilityAnimation(currentAbility.getActiveAnimation()); // Fire execute event (cancelable) AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent( @@ -140,59 +167,103 @@ public class DataAbilities { // Call onExecute currentAbility.onExecute(npc, target, npc.worldObj); + + // Check if ability completed during onExecute (signalCompletion was called) + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(target); + return; + } } currentAbility.onActiveTick(npc, target, npc.worldObj, currentAbility.getCurrentTick()); + + // Check if ability completed during onActiveTick (signalCompletion was called) + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(target); + return; + } break; - case RECOVERY: - // Recovery phase - just wait + case DAZED: + // Dazed phase - NPC cannot attack, just wait for daze to end break; case IDLE: - // Ability completed - currentAbility.onComplete(npc, target); - - // Fire complete event - AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent( - npc.wrappedNPC, currentAbility, target); - NpcAPI.EVENT_BUS.post(completeEvent); - - onAbilityComplete(); + // Ability completed (reached via tick() phase transition from DAZED) + handleAbilityCompletion(target); break; } - // Clear AI pathfinding if ability is controlling movement - // But DON'T zero motion - let the ability set it if needed + // Apply movement control if ability is still executing if (currentAbility != null && currentAbility.isExecuting()) { - if (isAbilityControllingMovement()) { - // Clear navigator so AI doesn't fight with ability movement - npc.getNavigator().clearPathEntity(); + applyMovementControl(); + } + } - // Only zero motion if lockMovement is true AND ability doesn't have its own movement - // This allows stationary abilities to freeze the NPC, while movement abilities can set their own motion - if (currentAbility.isLockMovement() && !currentAbility.hasAbilityMovement()) { - npc.motionX = 0; - npc.motionZ = 0; - } + /** + * Apply movement control based on lock movement settings. + * Called every tick during ability execution. + */ + private void applyMovementControl() { + if (currentAbility == null) return; + + // Check if movement should be locked for the current phase + if (currentAbility.isMovementLockedForCurrentPhase()) { + // Clear navigator to prevent AI pathfinding + npc.getNavigator().clearPathEntity(); + + // Zero motion if ability doesn't have its own movement + // This allows abilities like Charge/Slam to control their own motion + if (!currentAbility.hasAbilityMovement()) { + npc.motionX = 0; + npc.motionZ = 0; } } } + /** + * Handle ability completion - called when ability phase becomes IDLE. + * This can happen from: + * - signalCompletion() called in onExecute() or onActiveTick() + * - tick() transitioning from DAZED to IDLE + */ + private void handleAbilityCompletion(EntityLivingBase target) { + if (currentAbility == null) return; + + // Call onComplete callback + currentAbility.onComplete(npc, target); + + // Fire complete event + AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent( + npc.wrappedNPC, currentAbility, target); + NpcAPI.EVENT_BUS.post(completeEvent); + + // Release rotation lock before cleanup + releaseLockedRotation(); + + // Clean up and roll cooldown (this sets currentAbility to null) + onAbilityComplete(); + } + /** * Called when an ability completes. + * Calculates cooldown as: random(minCooldown, maxCooldown) + ability's cooldown offset */ private void onAbilityComplete() { if (currentAbility != null) { // Stop any ability animation stopAbilityAnimation(); - // Start per-ability cooldown - startCooldown(currentAbility); + // Release rotation lock + releaseLockedRotation(); + + // Calculate cooldown: random(min, max) + ability offset + int abilityCooldownOffset = currentAbility.getCooldownTicks(); + rollCooldown(); + // Add ability-specific cooldown offset + cooldownEndTime += abilityCooldownOffset; + currentAbility = null; lastTarget = null; - - // Start global cooldown - globalCooldownTimer = globalCooldown; } } @@ -224,13 +295,14 @@ public class DataAbilities { * Check if an ability can be selected right now. */ public boolean canSelectAbility() { - if (!enabled || abilities.isEmpty()) { + if (!enabled || abilitySlots.isEmpty()) { return false; } if (currentAbility != null && currentAbility.isExecuting()) { return false; } - if (globalCooldownTimer > 0) { + // Check if still on cooldown + if (npc.worldObj.getTotalWorldTime() < cooldownEndTime) { return false; } return true; @@ -243,7 +315,7 @@ public class DataAbilities { List eligible = new ArrayList<>(); int totalWeight = 0; - for (Ability ability : abilities) { + for (Ability ability : getAbilities()) { if (isAbilityEligible(ability, target)) { eligible.add(ability); totalWeight += ability.getWeight(); @@ -276,11 +348,6 @@ public class DataAbilities { return false; } - // Check cooldown - if (isOnCooldown(ability)) { - return false; - } - // Check if already executing if (ability.isExecuting()) { return false; @@ -325,7 +392,7 @@ public class DataAbilities { playAbilitySound(ability.getWindUpSound()); // Play wind up animation if configured - playAbilityAnimation(ability.getWindUpAnimationId()); + playAbilityAnimation(ability.getWindUpAnimation()); return true; } @@ -341,17 +408,25 @@ public class DataAbilities { /** * Play an animation on the NPC by ID. + * Public so abilities can trigger animations directly if needed. + * + * @param animation The animation to play, or null if none */ - private void playAbilityAnimation(int animationId) { - if (animationId < 0) return; + public void playAbilityAnimation(Animation animation) { + if (animation == null) return; if (AnimationController.Instance == null) return; - Animation animation = AnimationController.Instance.animations.get(animationId); - if (animation != null) { - npc.display.animationData.setEnabled(true); - npc.display.animationData.setAnimation(animation); - npc.display.animationData.updateClient(); - } + npc.display.animationData.setEnabled(true); + npc.display.animationData.setAnimation(animation); + npc.display.animationData.updateClient(); + } + + public void playAbilityAnimation(int animation) { + if (animation < 0) return; + if (AnimationController.Instance == null) return; + if (AnimationController.Instance.get(animation) == null) return; + + playAbilityAnimation((Animation) AnimationController.Instance.get(animation)); } /** @@ -390,37 +465,25 @@ public class DataAbilities { // ═══════════════════════════════════════════════════════════════════ /** - * Check if an ability is on cooldown. + * Check if NPC is on cooldown (cannot use any ability). */ - public boolean isOnCooldown(Ability ability) { - Long endTime = cooldowns.get(ability.getId()); - if (endTime == null) { - return false; - } - return npc.worldObj.getTotalWorldTime() < endTime; + public boolean isOnCooldown() { + return npc.worldObj.getTotalWorldTime() < cooldownEndTime; } /** - * Start cooldown for an ability. + * Get remaining cooldown ticks. */ - private void startCooldown(Ability ability) { - long endTime = npc.worldObj.getTotalWorldTime() + ability.getCooldownTicks(); - cooldowns.put(ability.getId(), endTime); + public long getRemainingCooldown() { + long remaining = cooldownEndTime - npc.worldObj.getTotalWorldTime(); + return remaining > 0 ? remaining : 0; } /** - * Reset cooldown for a specific ability. + * Reset cooldown (allow immediate ability use). */ - public void resetCooldown(String abilityId) { - cooldowns.remove(abilityId); - } - - /** - * Reset all cooldowns. - */ - public void resetAllCooldowns() { - cooldowns.clear(); - globalCooldownTimer = 0; + public void resetCooldown() { + cooldownEndTime = 0; } // ═══════════════════════════════════════════════════════════════════ @@ -428,7 +491,7 @@ public class DataAbilities { // ═══════════════════════════════════════════════════════════════════ /** - * Handle damage taken - may interrupt current ability. + * Handle damage taken - may interrupt current ability or trigger guard counter. * * @param source The damage source * @param amount The damage amount @@ -438,6 +501,14 @@ public class DataAbilities { // Track hit for hit count condition recordHit(); + // Check if currently executing a Guard ability and notify it of damage taken + if (currentAbility instanceof AbilityGuard) { + AbilityGuard guard = (AbilityGuard) currentAbility; + if (guard.isGuarding() && source.getEntity() instanceof EntityLivingBase) { + guard.onDamageTaken(npc, (EntityLivingBase) source.getEntity(), source, amount); + } + } + if (currentAbility == null || !currentAbility.isExecuting()) { return false; } @@ -450,11 +521,32 @@ public class DataAbilities { return false; } + /** + * Get the damage reduction factor if a Guard ability is currently active. + * + * @return The damage reduction factor (0.0 = no reduction, 1.0 = full immunity), or 0 if not guarding + */ + public float getGuardDamageReduction() { + if (currentAbility instanceof AbilityGuard) { + return ((AbilityGuard) currentAbility).getDamageReductionFactor(); + } + return 0.0f; + } + /** * Record a hit for the hit count condition. + * Includes periodic cleanup to prevent memory leak. */ private void recordHit() { - recentHitTimes.add(npc.worldObj.getTotalWorldTime()); + long currentTime = npc.worldObj.getTotalWorldTime(); + recentHitTimes.add(currentTime); + + // Cleanup old entries periodically to prevent unbounded growth + // Keep only hits from last 5 minutes (6000 ticks) max + if (recentHitTimes.size() > 50) { + long cutoff = currentTime - 6000; + recentHitTimes.removeIf(time -> time < cutoff); + } } /** @@ -480,6 +572,8 @@ public class DataAbilities { /** * Interrupt the currently executing ability. + * If interrupted during WINDUP, the ability will transition to DAZED phase. + * When DAZED completes, the ability will move to IDLE and onAbilityComplete() is called. */ public void interruptCurrentAbility(DamageSource source, float damage) { if (currentAbility != null) { @@ -489,15 +583,19 @@ public class DataAbilities { // Stop any ability animation stopAbilityAnimation(); + // Release rotation lock + releaseLockedRotation(); + // Fire interrupt event AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent( npc.wrappedNPC, currentAbility, lastTarget, source, damage); NpcAPI.EVENT_BUS.post(interruptEvent); currentAbility.onInterrupt(npc, source, damage); - currentAbility.interrupt(); - currentAbility = null; - lastTarget = null; + currentAbility.interrupt(); // Transitions to DAZED if interrupted during WINDUP + + // Don't clear currentAbility - let it tick through DAZED phase + // When DAZED ends and phase becomes IDLE, onAbilityComplete() will be called } } @@ -508,12 +606,57 @@ public class DataAbilities { if (currentAbility != null) { removeTelegraph(currentAbility); stopAbilityAnimation(); + releaseLockedRotation(); currentAbility.interrupt(); currentAbility = null; lastTarget = null; } } + /** + * Force start a specific ability, bypassing normal selection and cooldown. + * If an ability is currently executing, it will be cancelled. + * + * @param ability The ability to start + * @param target The target entity (can be null for self-targeted abilities) + * @return true if the ability was started successfully + */ + public boolean forceStartAbility(Ability ability, EntityLivingBase target) { + if (ability == null || npc.worldObj.isRemote) { + return false; + } + + // Stop any currently executing ability + stopCurrentAbility(); + + // Reset the ability state + ability.reset(); + + // Start the ability directly + return startAbility(ability, target); + } + + /** + * Execute an ability on this NPC by key (built-in name or custom UUID). + * The NPC does NOT need to have this ability assigned. + * + * @param key The ability key (built-in name or custom UUID) + * @param target The target entity (can be null for self-targeted abilities) + * @return true if the ability was started successfully + */ + public boolean executeAbility(String key, EntityLivingBase target) { + if (key == null || key.isEmpty() || npc.worldObj.isRemote) { + return false; + } + + Ability resolved = AbilityController.Instance.resolveAbility(key); + if (resolved == null) { + return false; + } + + return forceStartAbility(resolved, target); + } + // ═══════════════════════════════════════════════════════════════════ // SCRIPT EVENTS // ═══════════════════════════════════════════════════════════════════ @@ -532,12 +675,12 @@ public class DataAbilities { * Fire an ability hit event. Called by abilities when they hit an entity. * Returns null if the event was cancelled, otherwise returns the (possibly modified) event. * - * @param ability The ability doing the hit - * @param target The original target of the ability - * @param hitEntity The entity being hit - * @param damage The damage amount - * @param knockback The horizontal knockback - * @param knockbackUp The vertical knockback (deprecated, ignored) + * @param ability The ability doing the hit + * @param target The original target of the ability + * @param hitEntity The entity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback + * @param knockbackUp The vertical knockback * @return The event (with possibly modified values), or null if cancelled */ public AbilityEvent.HitEvent fireHitEvent(Ability ability, EntityLivingBase target, @@ -565,54 +708,109 @@ public class DataAbilities { /** * Reset all runtime state (cooldowns, current ability). * Called when NPC respawns or combat ends. + * Rolls cooldown so NPC doesn't immediately use an ability. */ public void reset() { stopCurrentAbility(); - resetAllCooldowns(); - // Reset execution state on all abilities - for (Ability ability : abilities) { + // Roll cooldown so NPC doesn't immediately attack after reset + rollCooldown(); + + // Reset execution state on all resolved abilities + for (Ability ability : getAbilities()) { ability.reset(); } } + /** + * Roll a new cooldown using the min/max range. + * Called after ability completes and on reset. + */ + private void rollCooldown() { + int baseCooldown = minCooldown; + if (maxCooldown > minCooldown) { + baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); + } + cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown; + } + // ═══════════════════════════════════════════════════════════════════ // ABILITY LIST MANAGEMENT // ═══════════════════════════════════════════════════════════════════ + /** + * Get resolved abilities from all slots, filtering out broken references. + */ public List getAbilities() { - return abilities; + List resolved = new ArrayList<>(); + for (AbilitySlot slot : abilitySlots) { + Ability a = slot.getAbility(); + if (a != null) { + resolved.add(a); + } + } + return resolved; } + /** Get the raw slot list (for GUI access). */ + public List getAbilitySlots() { + return abilitySlots; + } + + /** Add an inline ability. */ public void addAbility(Ability ability) { - abilities.add(ability); + abilitySlots.add(AbilitySlot.inline(ability)); + } + + /** Add a reference ability by key (built-in name or custom UUID). */ + public void addAbilityReference(String key) { + abilitySlots.add(AbilitySlot.reference(key)); } public void removeAbility(int index) { - if (index >= 0 && index < abilities.size()) { - abilities.remove(index); + if (index >= 0 && index < abilitySlots.size()) { + abilitySlots.remove(index); } } public void removeAbility(String id) { - abilities.removeIf(a -> a.getId().equals(id)); + abilitySlots.removeIf(slot -> { + if (slot.isReference()) { + return slot.getReferenceId().equals(id); + } + Ability a = slot.getAbility(); + return a != null && a.getId().equals(id); + }); } public Ability getAbility(String id) { - for (Ability ability : abilities) { - if (ability.getId().equals(id)) { - return ability; + for (AbilitySlot slot : abilitySlots) { + Ability a = slot.getAbility(); + if (a != null && a.getId().equals(id)) { + return a; } } return null; } + /** Check if a slot at a given index is a reference. */ + public boolean isSlotReference(int index) { + if (index < 0 || index >= abilitySlots.size()) return false; + return abilitySlots.get(index).isReference(); + } + + /** Convert a reference slot to inline. Returns false if resolution fails. */ + public boolean convertToInline(int index) { + if (index < 0 || index >= abilitySlots.size()) return false; + return abilitySlots.get(index).convertToInline(); + } + public void clearAbilities() { - abilities.clear(); + abilitySlots.clear(); } public boolean isEmpty() { - return abilities.isEmpty(); + return abilitySlots.isEmpty(); } // ═══════════════════════════════════════════════════════════════════ @@ -636,11 +834,11 @@ public class DataAbilities { /** * Check if ability is controlling NPC movement (AI pathfinding should be blocked). * Called by AI tasks to determine if they should try to path. - * + *

* Rules: - * - WINDUP phase: Block AI if lockMovement is true - * - ACTIVE phase: Block AI if lockMovement OR hasAbilityMovement is true - * - RECOVERY phase: Block AI if lockMovement is true + * - WINDUP phase: Block AI if movement is locked during windup + * - ACTIVE phase: Block AI if movement is locked during active OR hasAbilityMovement is true + * - DAZED phase: Always block AI (NPC is stunned) */ public boolean isAbilityControllingMovement() { if (currentAbility == null || !currentAbility.isExecuting()) { @@ -651,19 +849,19 @@ public class DataAbilities { switch (phase) { case WINDUP: - // During windup, only block if lockMovement is set - // This allows NPC to keep chasing while winding up if lockMovement=false - return currentAbility.isLockMovement(); + // During windup, only block if movement is locked during windup + // This allows NPC to keep chasing while winding up if not locked + return currentAbility.isMovementLockedDuringWindup(); case ACTIVE: // During active phase, block if either: - // - lockMovement is true (stationary ability) + // - movement is locked during active (stationary ability) // - hasAbilityMovement is true (ability is moving the NPC itself) - return currentAbility.isLockMovement() || currentAbility.hasAbilityMovement(); + return currentAbility.isMovementLockedDuringActive() || currentAbility.hasAbilityMovement(); - case RECOVERY: - // During recovery, block if lockMovement is set - return currentAbility.isLockMovement(); + case DAZED: + // During dazed phase, always block - NPC is stunned from interrupt + return true; default: return false; @@ -671,12 +869,21 @@ public class DataAbilities { } /** - * Check if NPC movement should be blocked due to ability execution. - * @deprecated Use isAbilityControllingMovement() instead + * Check if NPC's look direction should be locked to target. + * Only locks during ACTIVE phase when movement is locked during active. + * This allows the NPC to freely track target during WINDUP and DAZED. */ - @Deprecated - public boolean isMovementBlocked() { - return isAbilityControllingMovement(); + public boolean shouldLockLookDirection() { + if (currentAbility == null || !currentAbility.isExecuting()) { + return false; + } + + // Only lock look direction during ACTIVE phase + if (currentAbility.getPhase() != AbilityPhase.ACTIVE) { + return false; + } + + return currentAbility.isMovementLockedDuringActive(); } /** @@ -690,17 +897,72 @@ public class DataAbilities { return phase == AbilityPhase.WINDUP || phase == AbilityPhase.ACTIVE; } + // ═══════════════════════════════════════════════════════════════════ + // ROTATION LOCKING + // ═══════════════════════════════════════════════════════════════════ + + /** + * Capture current rotation values to lock NPC's look direction. + * Called when entering ACTIVE phase with movement locked during active. + */ + private void captureLockedRotation() { + lockedYaw = npc.rotationYaw; + lockedYawHead = npc.rotationYawHead; + lockedRenderYawOffset = npc.renderYawOffset; + lockedPitch = npc.rotationPitch; + rotationLocked = true; + } + + /** + * Release the rotation lock. + * Called when leaving ACTIVE phase or ability completes. + */ + private void releaseLockedRotation() { + rotationLocked = false; + } + + /** + * 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. + */ + public void applyLockedRotation() { + if (!rotationLocked) { + return; + } + + // Force rotation back to locked values + npc.rotationYaw = lockedYaw; + npc.rotationYawHead = lockedYawHead; + npc.renderYawOffset = lockedRenderYawOffset; + npc.rotationPitch = lockedPitch; + + // Also set prev values to prevent interpolation jitter + npc.prevRotationYaw = lockedYaw; + npc.prevRotationYawHead = lockedYawHead; + npc.prevRenderYawOffset = lockedRenderYawOffset; + npc.prevRotationPitch = lockedPitch; + } + + /** + * Check if rotation is currently locked. + */ + public boolean isRotationLocked() { + return rotationLocked; + } + // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setBoolean("AbilitiesEnabled", enabled); - compound.setInteger("AbilityGlobalCooldown", globalCooldown); + compound.setInteger("AbilityMinCooldown", minCooldown); + compound.setInteger("AbilityMaxCooldown", maxCooldown); NBTTagList abilityList = new NBTTagList(); - for (Ability ability : abilities) { - abilityList.appendTag(ability.writeNBT()); + for (AbilitySlot slot : abilitySlots) { + abilityList.appendTag(slot.writeNBT()); } compound.setTag("Abilities", abilityList); @@ -709,16 +971,16 @@ public class DataAbilities { public void readFromNBT(NBTTagCompound compound) { enabled = compound.getBoolean("AbilitiesEnabled"); - globalCooldown = compound.hasKey("AbilityGlobalCooldown") ? - compound.getInteger("AbilityGlobalCooldown") : 20; + minCooldown = compound.getInteger("AbilityMinCooldown"); + maxCooldown = compound.getInteger("AbilityMaxCooldown"); - abilities.clear(); + abilitySlots.clear(); NBTTagList abilityList = compound.getTagList("Abilities", 10); for (int i = 0; i < abilityList.tagCount(); i++) { NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i); - Ability ability = AbilityController.Instance.fromNBT(abilityNBT); - if (ability != null) { - abilities.add(ability); + AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT); + if (slot != null) { + abilitySlots.add(slot); } } } diff --git a/src/main/java/noppes/npcs/DataAdvanced.java b/src/main/java/noppes/npcs/DataAdvanced.java index 45ecfe4a8..f03714506 100644 --- a/src/main/java/noppes/npcs/DataAdvanced.java +++ b/src/main/java/noppes/npcs/DataAdvanced.java @@ -18,6 +18,7 @@ import noppes.npcs.roles.JobGuard; import noppes.npcs.roles.JobHealer; import noppes.npcs.roles.JobItemGiver; import noppes.npcs.roles.JobSpawner; +import noppes.npcs.roles.RoleAuctioneer; import noppes.npcs.roles.RoleBank; import noppes.npcs.roles.RoleCompanion; import noppes.npcs.roles.RoleFollower; @@ -218,6 +219,8 @@ public class DataAdvanced { npc.roleInterface = new RoleCompanion(npc); else if (role == EnumRoleType.Mount && !(npc.roleInterface instanceof RoleMount)) npc.roleInterface = new RoleMount(npc); + else if (role == EnumRoleType.Auctioneer && !(npc.roleInterface instanceof RoleAuctioneer)) + npc.roleInterface = new RoleAuctioneer(npc); } public void setJob(int i) { diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 9cb5b1669..5f9d1388f 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -38,9 +38,7 @@ import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.Dialog; -import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.controllers.data.ForgeDataScript; -import noppes.npcs.controllers.data.INpcScriptHandler; import noppes.npcs.controllers.data.IScriptBlockHandler; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.Party; @@ -64,6 +62,7 @@ import noppes.npcs.scripted.event.RecipeScriptEvent; import noppes.npcs.scripted.event.player.CustomGuiEvent; import noppes.npcs.scripted.event.player.DialogEvent; import noppes.npcs.scripted.event.player.FactionEvent; +import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import noppes.npcs.scripted.event.player.PlayerEvent; import noppes.npcs.scripted.event.player.PlayerEvent.ChatEvent; import noppes.npcs.scripted.event.player.PlayerEvent.ContainerOpen; @@ -85,7 +84,7 @@ public class EventHooks { } public static void onScriptItemInit(IItemCustomizable item) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); if (handler != null && !handler.isClient()) { ItemEvent.InitEvent event = new ItemEvent.InitEvent(item); handler.callScript(EnumScriptType.INIT, event); @@ -94,7 +93,7 @@ public class EventHooks { } public static void onScriptItemUpdate(IItemCustomizable item, EntityLivingBase player) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); if (handler != null && !handler.isClient()) { ItemEvent.UpdateEvent event = new ItemEvent.UpdateEvent(item, NpcAPI.Instance().getIEntity(player)); handler.callScript(EnumScriptType.TICK, event); @@ -103,7 +102,7 @@ public class EventHooks { } public static boolean onScriptItemTossed(IItemCustomizable item, EntityPlayer player, EntityItem entity) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.TossedEvent event = new ItemEvent.TossedEvent(item, (IPlayer) NpcAPI.Instance().getIEntity(player), NpcAPI.Instance().getIEntity(entity)); if (handler != null) { handler.callScript(EnumScriptType.TOSSED, event); @@ -112,7 +111,7 @@ public class EventHooks { } public static boolean onScriptItemPickedUp(IItemCustomizable item, EntityPlayer player) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.PickedUpEvent event = new ItemEvent.PickedUpEvent(item, (IPlayer) NpcAPI.Instance().getIEntity(player)); if (handler != null) { handler.callScript(EnumScriptType.PICKEDUP, event); @@ -121,7 +120,7 @@ public class EventHooks { } public static boolean onScriptItemSpawn(IItemCustomizable item, EntityItem entity) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.SpawnEvent event = new ItemEvent.SpawnEvent(item, NpcAPI.Instance().getIEntity(entity)); if (handler != null) { handler.callScript(EnumScriptType.SPAWN, event); @@ -130,7 +129,7 @@ public class EventHooks { } public static boolean onScriptItemInteract(IItemCustomizable item, noppes.npcs.scripted.event.ItemEvent.InteractEvent event) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); if (handler != null) { handler.callScript(EnumScriptType.INTERACT, event); } @@ -138,7 +137,7 @@ public class EventHooks { } public static boolean onScriptItemRightClick(IItemCustomizable item, noppes.npcs.scripted.event.ItemEvent.RightClickEvent event) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); if (handler != null) { handler.callScript(EnumScriptType.RIGHT_CLICK, event); } @@ -146,7 +145,7 @@ public class EventHooks { } public static boolean onScriptItemAttack(IItemCustomizable item, noppes.npcs.scripted.event.ItemEvent.AttackEvent event) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); if (handler != null) { handler.callScript(EnumScriptType.ATTACK, event); } @@ -154,7 +153,7 @@ public class EventHooks { } public static boolean onStartUsingCustomItem(IItemCustomizable item, IPlayer player, int duration) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.StartUsingItem event = new ItemEvent.StartUsingItem(item, player, duration); if (handler != null) { handler.callScript(EnumScriptType.START_USING_ITEM, event); @@ -163,7 +162,7 @@ public class EventHooks { } public static boolean onUsingCustomItem(IItemCustomizable item, IPlayer player, int duration) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.UsingItem event = new ItemEvent.UsingItem(item, player, duration); if (handler != null) { handler.callScript(EnumScriptType.USING_ITEM, event); @@ -172,7 +171,7 @@ public class EventHooks { } public static boolean onStopUsingCustomItem(IItemCustomizable item, IPlayer player, int duration) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.StopUsingItem event = new ItemEvent.StopUsingItem(item, player, duration); if (handler != null) { handler.callScript(EnumScriptType.STOP_USING_ITEM, event); @@ -181,7 +180,7 @@ public class EventHooks { } public static boolean onFinishUsingCustomItem(IItemCustomizable item, IPlayer player, int duration) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.FinishUsingItem event = new ItemEvent.FinishUsingItem(item, player, duration); if (handler != null) { handler.callScript(EnumScriptType.FINISH_USING_ITEM, event); @@ -190,7 +189,7 @@ public class EventHooks { } public static void onRepairCustomItem(IItemCustomizable item, IPlayer player, IItemStack left, IItemStack right, float anvilBreakChance) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.RepairItem event = new ItemEvent.RepairItem(item, player, left, right, anvilBreakChance); if (handler != null) { handler.callScript(EnumScriptType.REPAIR_ITEM, event); @@ -199,7 +198,7 @@ public class EventHooks { } public static void onBreakCustomItem(IItemCustomizable item, IPlayer player) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.BreakItem event = new ItemEvent.BreakItem(item, player); if (handler != null) { handler.callScript(EnumScriptType.BREAK_ITEM, event); @@ -242,7 +241,7 @@ public class EventHooks { public static void onLinkedItemVersionChange(IItemLinked item, int version, int prevVersion) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); LinkedItemEvent.VersionChangeEvent event = new LinkedItemEvent.VersionChangeEvent(item, version, prevVersion); if (handler != null) { handler.callScript(EnumScriptType.LINKED_ITEM_VERSION, event); @@ -251,7 +250,7 @@ public class EventHooks { } public static void onLinkedItemBuild(IItemLinked item) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); LinkedItemEvent.BuildEvent event = new LinkedItemEvent.BuildEvent(item); if (handler != null) { handler.callScript(EnumScriptType.LINKED_ITEM_BUILD, event); @@ -298,7 +297,6 @@ public class EventHooks { NpcEvent.DialogClosedEvent event = new NpcEvent.DialogClosedEvent(npc.wrappedNPC, player, dialogId, optionId, dialog); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.DIALOG_CLOSE, event); npc.script.callScript(EnumScriptType.DIALOG_CLOSE, event, "player", event.getPlayer(), "dialog", event.getDialogId(), "option", event.getOptionId(), "dialogObj", event.getDialog()); - NpcAPI.EVENT_BUS.post(event); } @@ -421,7 +419,9 @@ public class EventHooks { EntityLivingBase thrower = projectile.getThrower(); if (thrower != null) { if (thrower instanceof EntityNPCInterface) { - ((EntityNPCInterface) thrower).script.callScript(EnumScriptType.PROJECTILE_TICK, event); + EntityNPCInterface npc = (EntityNPCInterface) thrower; + + npc.script.callScript(EnumScriptType.PROJECTILE_TICK, event); } } @@ -439,7 +439,9 @@ public class EventHooks { EntityLivingBase thrower = projectile.getThrower(); if (thrower != null) { if (thrower instanceof EntityNPCInterface) { - ((EntityNPCInterface) thrower).script.callScript(EnumScriptType.PROJECTILE_IMPACT, event); + EntityNPCInterface npc = (EntityNPCInterface) thrower; + + npc.script.callScript(EnumScriptType.PROJECTILE_IMPACT, event); } } @@ -489,21 +491,21 @@ public class EventHooks { public static boolean onEffectAdded(IPlayer player, IPlayerEffect effect) { PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); PlayerEvent.EffectEvent.Added event = new PlayerEvent.EffectEvent.Added(player, effect); - handler.callScript(EffectScript.ScriptType.OnEffectAdd.function, event); + handler.callScript(EnumScriptType.ON_EFFECT_ADD.function, event); return NpcAPI.EVENT_BUS.post(event); } public static boolean onEffectTick(IPlayer player, IPlayerEffect effect) { PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); PlayerEvent.EffectEvent.Ticked event = new PlayerEvent.EffectEvent.Ticked(player, effect); - handler.callScript(EffectScript.ScriptType.OnEffectTick.function, event); + handler.callScript(EnumScriptType.ON_EFFECT_TICK.function, event); return NpcAPI.EVENT_BUS.post(event); } public static boolean onEffectRemove(IPlayer player, IPlayerEffect effect, EffectEvent.ExpirationType type) { PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); PlayerEvent.EffectEvent.Removed event = new PlayerEvent.EffectEvent.Removed(player, effect, type); - handler.callScript(EffectScript.ScriptType.OnEffectRemove.function, event); + handler.callScript(EnumScriptType.ON_EFFECT_REMOVE.function, event); return NpcAPI.EVENT_BUS.post(event); } @@ -691,6 +693,11 @@ public class EventHooks { return NpcAPI.EVENT_BUS.post(event); } + public static boolean onPlayerAbilityHit(PlayerDataScript handler, PlayerAbilityEvent.HitEvent event) { + handler.callScript(EnumScriptType.ABILITY_HIT, event); + return NpcAPI.EVENT_BUS.post(event); + } + public static void onPlayerChangeDim(PlayerDataScript handler, IPlayer player, int fromDim, int toDim) { PlayerEvent.ChangedDimension event = new PlayerEvent.ChangedDimension(player, fromDim, toDim); handler.callScript(EnumScriptType.CHANGED_DIM, event); diff --git a/src/main/java/noppes/npcs/NBTTags.java b/src/main/java/noppes/npcs/NBTTags.java index ebf30ab94..6eec4c14d 100644 --- a/src/main/java/noppes/npcs/NBTTags.java +++ b/src/main/java/noppes/npcs/NBTTags.java @@ -6,8 +6,8 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagDouble; import net.minecraft.nbt.NBTTagInt; import net.minecraft.nbt.NBTTagList; -import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; import java.util.ArrayList; import java.util.HashMap; @@ -540,38 +540,33 @@ public class NBTTags { } } - public static List GetScriptOld(NBTTagList list, IScriptHandler handler) { - ArrayList scripts = new ArrayList(); + public static List GetScriptOld(NBTTagList list, IScriptHandler handler) { + ArrayList scripts = new ArrayList<>(); for (int i = 0; i < list.tagCount(); ++i) { NBTTagCompound compoundd = list.getCompoundTagAt(i); - ScriptContainer script = new ScriptContainer(handler); - script.readFromNBT(compoundd); + IScriptUnit script = IScriptUnit.createFromNBT(compoundd, handler); scripts.add(script); } return scripts; } - public static List GetScript(NBTTagCompound compound, IScriptHandler handler) { - ArrayList scripts = new ArrayList<>(); + public static List GetScript(NBTTagCompound compound, IScriptHandler handler) { + ArrayList scripts = new ArrayList<>(); for (int i = 0; i < compound.getInteger("TotalScripts"); ++i) { NBTTagCompound containerCompound = compound.getCompoundTag("Tab" + i); - ScriptContainer script = new ScriptContainer(handler); - script.readFromNBT(containerCompound); + IScriptUnit script = IScriptUnit.createFromNBT(containerCompound, handler); scripts.add(script); } return scripts; } - public static NBTTagList NBTScript(List scripts) { + public static NBTTagList NBTScript(List scripts) { NBTTagList list = new NBTTagList(); - Iterator var2 = scripts.iterator(); - - while (var2.hasNext()) { - ScriptContainer script = (ScriptContainer) var2.next(); + for (IScriptUnit script : scripts) { NBTTagCompound compound = new NBTTagCompound(); script.writeToNBT(compound); list.appendTag(compound); diff --git a/src/main/java/noppes/npcs/NoppesStringUtils.java b/src/main/java/noppes/npcs/NoppesStringUtils.java index cd7031ef0..ddc11d5c6 100644 --- a/src/main/java/noppes/npcs/NoppesStringUtils.java +++ b/src/main/java/noppes/npcs/NoppesStringUtils.java @@ -4,7 +4,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.StatCollector; import noppes.npcs.entity.EntityNPCInterface; -import java.awt.*; +import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.ClipboardOwner; import java.awt.datatransfer.DataFlavor; diff --git a/src/main/java/noppes/npcs/NoppesUtilPlayer.java b/src/main/java/noppes/npcs/NoppesUtilPlayer.java index c78ffdf46..7f9753104 100644 --- a/src/main/java/noppes/npcs/NoppesUtilPlayer.java +++ b/src/main/java/noppes/npcs/NoppesUtilPlayer.java @@ -306,18 +306,18 @@ public class NoppesUtilPlayer { if (dialog == null) return; DialogOption option = dialog.options.get(optionId); - if (EventHooks.onDialogOption(player, new DialogEvent.DialogOption((IPlayer) NpcAPI.Instance().getIEntity(player), dialog))) + if (EventHooks.onDialogOption(player, new DialogEvent.DialogOption((IPlayer) NpcAPI.Instance().getIEntity(player), dialogId, optionId, dialog))) return; if (!npc.isRemote()) { EventHooks.onNPCDialogClosed(npc, player, dialogId, optionId + 1, dialog); if (!dialog.hasDialogs(player) && !dialog.hasOtherOptions()) { - EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialog)); + EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialogId, optionId, dialog)); return; } if (option == null || option.optionType == EnumOptionType.DialogOption && (!option.isAvailable(player) || !option.hasDialog()) || option.optionType == EnumOptionType.Disabled || option.optionType == EnumOptionType.QuitOption) { - EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialog)); + EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialogId, optionId, dialog)); return; } } diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 08c04f6f4..0799e7a6c 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -163,7 +163,16 @@ public class NoppesUtilServer { for (Animation animation : AnimationController.getInstance().animations.values()) { map.put(animation.name, animation.id); } - sendScrollData(player, map, EnumScrollData.OPTIONAL); + sendScrollData(player, map, EnumScrollData.ANIMATIONS); + } + + public static void sendBuiltInAnimationData(EntityPlayerMP player) { + Map map = new HashMap(); + for (String name : AnimationController.getInstance().builtInAnimations.keySet()) { + // Use -1 as a marker for built-in animations (they have no ID) + map.put(AnimationController.getInstance().builtInAnimations.get(name).getName(), -1); + } + sendScrollData(player, map, EnumScrollData.BUILTIN_ANIMATIONS); } public static void sendTagDataAll(EntityPlayerMP player) { @@ -241,7 +250,7 @@ public class NoppesUtilServer { if (EventHooks.onNPCDialog(npc, player, dialog.id, optionId, dialog)) { return; } - if (EventHooks.onDialogOpen(player, new DialogEvent.DialogOpen((IPlayer) NpcAPI.Instance().getIEntity(player), dialog))) { + if (EventHooks.onDialogOpen(player, new DialogEvent.DialogOpen((IPlayer) NpcAPI.Instance().getIEntity(player), dialog.id, optionId, dialog))) { return; } } @@ -776,6 +785,20 @@ public class NoppesUtilServer { sendScrollData(player, map, EnumScrollData.ABILITY_TYPES); } + public static void sendCustomAbilitiesData(EntityPlayerMP player) { + HashMap map = new HashMap(); + kamkeel.npcs.controllers.data.ability.AbilityController controller = kamkeel.npcs.controllers.data.ability.AbilityController.Instance; + if (controller != null) { + int index = 0; + for (String uuid : controller.getCustomAbilityIds()) { + String name = controller.getCustomAbilityName(uuid); + if (name == null || name.isEmpty()) name = uuid; + map.put(name + "\t" + uuid, index++); + } + } + sendScrollData(player, map, EnumScrollData.CUSTOM_ABILITIES); + } + public static DialogOption setNpcDialog(int slot, int dialogId, EntityPlayer player) throws IOException { return setNpcDialog(slot, dialogId, getEditingNpc(player)); } diff --git a/src/main/java/noppes/npcs/ScriptForgeEventHandler.java b/src/main/java/noppes/npcs/ScriptForgeEventHandler.java index 649557b43..abc5a80ee 100644 --- a/src/main/java/noppes/npcs/ScriptForgeEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptForgeEventHandler.java @@ -36,6 +36,10 @@ public class ScriptForgeEventHandler { @SubscribeEvent public void forgeEntity(Event event) { if (CustomNpcs.getServer() != null && ScriptController.Instance.forgeScripts.isEnabled()) { + if (FMLCommonHandler.instance().getEffectiveSide() == Side.CLIENT) { + return; + } + if (event instanceof TickEvent && !(((TickEvent) event).side == Side.SERVER && ((TickEvent) event).phase == TickEvent.Phase.START)) { return; } diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index e9531296a..0541f64c4 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -62,7 +62,6 @@ import noppes.npcs.items.ItemNpcTool; import noppes.npcs.quests.QuestItem; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.item.ScriptCustomizableItem; -import noppes.npcs.scripted.item.ScriptItemStack; import static noppes.npcs.config.ConfigMain.TrackedQuestUpdateFrequency; @@ -98,6 +97,13 @@ public class ScriptPlayerEventHandler { if (PlayerDataController.Instance != null) { PlayerData playerData = PlayerData.get(player); playerData.actionManager.tick(); + playerData.abilityData.tick(player); + + // Lock player movement during ability phases that require it + if (playerData.abilityData.isMovementLocked()) { + player.motionX = 0; + player.motionZ = 0; + } if (playerData.updateClient) { SyncController.syncPlayerData((EntityPlayerMP) player, true); @@ -549,7 +555,7 @@ public class ScriptPlayerEventHandler { if (event.source.getEntity() instanceof EntityPlayerMP) { EntityPlayerMP playerMP = (EntityPlayerMP) event.source.getEntity(); - if(playerMP.getHeldItem() != null) { + if (playerMP.getHeldItem() != null) { IItemStack itemStack = NpcAPI.Instance().getIItemStack(playerMP.getHeldItem()); if (itemStack instanceof ScriptCustomizableItem) { ScriptCustomizableItem sci = (ScriptCustomizableItem) itemStack; diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index 47a1148df..b6671b72a 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -51,7 +51,6 @@ import noppes.npcs.controllers.ServerCloneController; import noppes.npcs.controllers.data.Animation; import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.controllers.data.Line; -import noppes.npcs.controllers.data.LinkedItem; import noppes.npcs.controllers.data.MarkData; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; @@ -60,7 +59,6 @@ import noppes.npcs.controllers.data.Quest; import noppes.npcs.controllers.data.QuestData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.items.ItemExcalibur; -import noppes.npcs.items.ItemLinked; import noppes.npcs.items.ItemShield; import noppes.npcs.items.ItemSoulstoneEmpty; import noppes.npcs.quests.QuestKill; diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 3648f00d7..2639c6ab5 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -6,16 +6,13 @@ import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; +import noppes.npcs.controllers.AuctionController; import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldServer; -import noppes.npcs.client.AnalyticsTracking; +import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.PlayerData; -import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; - -import java.net.InetAddress; -import java.net.UnknownHostException; +import noppes.npcs.entity.EntityNPCInterface; public class ServerTickHandler { @@ -23,6 +20,11 @@ public class ServerTickHandler { public void onServerTick(TickEvent.ServerTickEvent event) { if (event.phase == Phase.END) { ActionManager.GLOBAL.tick(); + + // Process auction system tick + if (AuctionController.Instance != null) { + AuctionController.Instance.onServerTick(); + } } } @@ -38,25 +40,6 @@ public class ServerTickHandler { @SubscribeEvent public void playerLogin(PlayerEvent.PlayerLoggedInEvent event) { EntityPlayerMP player = (EntityPlayerMP) event.player; - // Temporary Disabled -// if (serverName == null) { -// String e = "local"; -// MinecraftServer server = MinecraftServer.getServer(); -// if (server.isDedicatedServer()) { -// try { -// e = InetAddress.getByName(server.getServerHostname()).getCanonicalHostName(); -// } catch (UnknownHostException e1) { -// e = MinecraftServer.getServer().getServerHostname(); -// } -// if (server.getPort() != 25565) -// e += ":" + server.getPort(); -// } -// if (e == null || e.startsWith("192.168") || e.contains("127.0.0.1") || e.startsWith("localhost")) -// e = "local"; -// serverName = e; -// } -// AnalyticsTracking.sendData(event.player, "join", serverName); - PlayerData playerData = PlayerData.get(event.player); if (playerData != null) { playerData.onLogin(); @@ -65,6 +48,12 @@ public class ServerTickHandler { ProfileController.Instance.login(player); SyncController.beginLogin(player); SyncController.syncEffects(player); + ScriptController.Instance.syncClientScripts(player); + + // Send auction notifications on login + if (AuctionController.Instance != null) { + AuctionController.Instance.onPlayerLogin(player); + } } @SubscribeEvent @@ -79,6 +68,11 @@ public class ServerTickHandler { playerData.onLogout(); } + // Clear auction permission cache on logout + if (AuctionController.Instance != null) { + AuctionController.Instance.onPlayerLogout(event.player.getUniqueID()); + } + // Save and unload the player's profile data on logout ProfileController.Instance.logout(event.player); } diff --git a/src/main/java/noppes/npcs/ai/CombatHandler.java b/src/main/java/noppes/npcs/ai/CombatHandler.java index e3eb85fbe..750725551 100644 --- a/src/main/java/noppes/npcs/ai/CombatHandler.java +++ b/src/main/java/noppes/npcs/ai/CombatHandler.java @@ -179,10 +179,10 @@ public class CombatHandler { } /** - * Check if NPC movement should be blocked due to ability execution. + * Check if ability is controlling NPC movement (AI pathfinding should be blocked). */ - public boolean isMovementBlocked() { - return npc.abilities.isMovementBlocked(); + public boolean isAbilityControllingMovement() { + return npc.abilities.isAbilityControllingMovement(); } /** diff --git a/src/main/java/noppes/npcs/blocks/BlockWaypoint.java b/src/main/java/noppes/npcs/blocks/BlockWaypoint.java index 7e0fd2601..7e2dd87d3 100644 --- a/src/main/java/noppes/npcs/blocks/BlockWaypoint.java +++ b/src/main/java/noppes/npcs/blocks/BlockWaypoint.java @@ -41,8 +41,14 @@ public class BlockWaypoint extends BlockContainer { @Override public void onBlockPlacedBy(World world, int i, int j, int k, EntityLivingBase entityliving, ItemStack item) { - if (entityliving instanceof EntityPlayer && world.isRemote) { - CustomNpcs.proxy.openGui(i, j, k, EnumGuiType.Waypoint, (EntityPlayer) entityliving); + if (entityliving instanceof EntityPlayer) { + if (!world.isRemote) { + // Server-side: sync TileEntity data to client and open GUI (same as onBlockActivated) + TileEntity tile = world.getTileEntity(i, j, k); + NBTTagCompound compound = new NBTTagCompound(); + tile.writeToNBT(compound); + PacketHandler.Instance.sendToPlayer(new GuiWaypointPacket(compound), (EntityPlayerMP) entityliving); + } } } diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index 8babf8936..64dbb767b 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -3,6 +3,7 @@ package noppes.npcs.blocks.tiles; import cpw.mods.fml.common.eventhandler.Event; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; import net.minecraft.block.Block; import net.minecraft.client.renderer.texture.ITickable; import net.minecraft.item.Item; @@ -25,9 +26,11 @@ import noppes.npcs.api.IBlock; import noppes.npcs.api.block.ITextPlane; import noppes.npcs.client.renderer.blocks.BlockScriptedRenderer; import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptBlockHandler; +import noppes.npcs.controllers.data.IScriptHandlerPacket; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.entity.data.DataTimers; import noppes.npcs.scripted.BlockScriptedWrapper; import noppes.npcs.util.ValueUtil; @@ -36,11 +39,9 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; -public class TileScripted extends TileEntity implements IScriptBlockHandler { - public List scripts = new ArrayList<>(); +public class TileScripted extends TileEntity implements IScriptBlockHandler, IScriptHandlerPacket { + public List scripts = new ArrayList<>(); public Map tempData = new HashMap<>(); @@ -161,6 +162,36 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { text6.setNBT(compound.getCompoundTag("Text6")); } + @Override + public ScriptContext getContext() { + return ScriptContext.BLOCK; + } + + @Override + public void requestData() { + BlockScriptPacket.Get(this.xCoord, this.yCoord, this.zCoord); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + BlockScriptPacket.Save(this.xCoord, this.yCoord, this.zCoord, nbt); + } + + @Override + public GuiDataResult setGuiData(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + return new GuiDataResult(GuiDataKind.LOAD_COMPLETE, -1); + } + + setNBT(compound); + return new GuiDataResult(GuiDataKind.METADATA, -1); + } + + @Override + public void sync() { + sendSavePacket(-1, getScripts().size(), getNBT(new NBTTagCompound())); + } + @Override public void writeToNBT(NBTTagCompound compound) { super.writeToNBT(compound); @@ -367,7 +398,7 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { EventHooks.onScriptBlockInit(this); } - for (ScriptContainer script : scripts) { + for (IScriptUnit script : scripts) { script.run(type, event); } } @@ -408,34 +439,15 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { } @Override - public void setScripts(List list) { + public void setScripts(List list) { scripts = list; } @Override - public List getScripts() { + public List getScripts() { return scripts; } - @Override - public Map getConsoleText() { - Map map = new TreeMap<>(); - int tab = 0; - for (ScriptContainer script : getScripts()) { - tab++; - for (Entry entry : script.console.entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - @Override - public void clearConsole() { - for (ScriptContainer script : getScripts()) { - script.console.clear(); - } - } @SideOnly(Side.CLIENT) @Override diff --git a/src/main/java/noppes/npcs/client/AnalyticsTracking.java b/src/main/java/noppes/npcs/client/AnalyticsTracking.java deleted file mode 100644 index ba745346d..000000000 --- a/src/main/java/noppes/npcs/client/AnalyticsTracking.java +++ /dev/null @@ -1,53 +0,0 @@ -package noppes.npcs.client; - -import net.minecraft.entity.player.EntityPlayer; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.UUID; - -public class AnalyticsTracking { - - public static void sendData(final EntityPlayer player, final String event, final String data) { - Runnable r = new Runnable() { - - @Override - public void run() { - try { - UUID uuid = player.getUniqueID(); - - String analyticsPostData = "v=1" + - "&tid=UA-29079943-5" + - "&cid=" + uuid.toString() + - "&t=event" + - "&ec=customnpcs_1.7d" + - "&ea=" + event + - "&el=" + data + - "&ev=300"; - - URL url = new URL("http://www.google-analytics.com/collect"); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(10000); - connection.setReadTimeout(10000); - connection.setDoOutput(true); - connection.setUseCaches(false); - connection.setRequestMethod("POST"); - connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - connection.setRequestProperty("Content-Length", Integer.toString(analyticsPostData.getBytes().length)); - OutputStream dataOutput = connection.getOutputStream(); - dataOutput.write(analyticsPostData.getBytes()); - dataOutput.close(); - connection.getInputStream().close(); - } catch (IOException e) { - - } - - } - }; - Thread t = new Thread(r); - t.start(); - } - -} diff --git a/src/main/java/noppes/npcs/client/AuctionClientConfig.java b/src/main/java/noppes/npcs/client/AuctionClientConfig.java new file mode 100644 index 000000000..7d6854e4c --- /dev/null +++ b/src/main/java/noppes/npcs/client/AuctionClientConfig.java @@ -0,0 +1,66 @@ +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 auction configuration synced from server. + * Prevents client from using local ConfigMarket which may differ from server settings. + * + * IMPORTANT: This class is CLIENT-ONLY. Server-side NBT writing is done in + * AuctionConfigSync to avoid @SideOnly crashes on dedicated servers. + */ +@SideOnly(Side.CLIENT) +public class AuctionClientConfig { + // Cached values from server + private static long listingFee = 0; + private static String currencyName = "Coins"; + private static int auctionDurationHours = 24; + private static double minBidIncrement = 0.05; + private static int maxActiveListings = 8; + private static int claimExpirationDays = 20; + private static boolean auctionEnabled = false; + + /** + * Update cached config from server NBT data. + * Called when receiving login packet. + */ + public static void readFromNBT(NBTTagCompound compound) { + if (compound == null) return; + + auctionEnabled = compound.getBoolean("AuctionEnabled"); + listingFee = compound.getLong("ListingFee"); + currencyName = compound.getString("CurrencyName"); + auctionDurationHours = compound.getInteger("AuctionDurationHours"); + minBidIncrement = compound.getDouble("MinBidIncrement"); + maxActiveListings = compound.getInteger("MaxActiveListings"); + claimExpirationDays = compound.getInteger("ClaimExpirationDays"); + + if (currencyName.isEmpty()) { + currencyName = "Gold"; + } + } + + /** + * Reset to defaults (called on disconnect). + */ + public static void reset() { + listingFee = 0; + currencyName = "Coins"; + auctionDurationHours = 24; + minBidIncrement = 0.05; + maxActiveListings = 8; + claimExpirationDays = 20; + auctionEnabled = false; + } + + // Getters + public static boolean isAuctionEnabled() { return auctionEnabled; } + public static long getListingFee() { return listingFee; } + public static String getCurrencyName() { return currencyName; } + public static int getAuctionDurationHours() { return auctionDurationHours; } + public static double getMinBidIncrement() { return minBidIncrement; } + public static int getMaxActiveListings() { return maxActiveListings; } + public static int getClaimExpirationDays() { return claimExpirationDays; } +} diff --git a/src/main/java/noppes/npcs/client/ClientCacheHandler.java b/src/main/java/noppes/npcs/client/ClientCacheHandler.java index 2c6a368c2..30b14be83 100644 --- a/src/main/java/noppes/npcs/client/ClientCacheHandler.java +++ b/src/main/java/noppes/npcs/client/ClientCacheHandler.java @@ -1,5 +1,6 @@ package noppes.npcs.client; +import kamkeel.npcs.network.enums.EnumSyncType; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.controllers.MusicController; @@ -15,7 +16,6 @@ import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.controllers.data.SkinOverlay; -import kamkeel.npcs.network.enums.EnumSyncType; import noppes.npcs.util.CacheHashMap; import java.util.EnumMap; diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 4e64f8351..705e261c5 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -6,6 +6,8 @@ import cpw.mods.fml.client.registry.RenderingRegistry; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.ObfuscationReflectionHelper; import kamkeel.npcs.addon.client.DBCClient; +import kamkeel.npcs.client.renderer.TelegraphRenderer; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.model.ModelBiped; @@ -21,7 +23,9 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.Container; import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; import net.minecraft.stats.Achievement; +import net.minecraft.tileentity.TileEntity; import net.minecraft.util.ChatComponentText; import net.minecraft.util.ReportedException; import net.minecraft.util.ResourceLocation; @@ -83,6 +87,7 @@ import noppes.npcs.client.gui.global.GuiNPCManageAnimations; import noppes.npcs.client.gui.global.GuiNPCManageBanks; import noppes.npcs.client.gui.global.GuiNPCManageDialogs; import noppes.npcs.client.gui.global.GuiNPCManageEffects; +import noppes.npcs.client.gui.global.GuiNpcManageAbilities; import noppes.npcs.client.gui.global.GuiNPCManageFactions; import noppes.npcs.client.gui.global.GuiNPCManageLinked; import noppes.npcs.client.gui.global.GuiNPCManageQuest; @@ -104,6 +109,10 @@ import noppes.npcs.client.gui.player.GuiCrate; import noppes.npcs.client.gui.player.GuiMailbox; import noppes.npcs.client.gui.player.GuiMailmanWrite; import noppes.npcs.client.gui.player.GuiNPCBankChest; +import noppes.npcs.client.gui.player.GuiAuctionBidding; +import noppes.npcs.client.gui.player.GuiAuctionTrades; +import noppes.npcs.client.gui.player.GuiAuctionListing; +import noppes.npcs.client.gui.player.GuiAuctionSell; import noppes.npcs.client.gui.player.GuiNPCTrader; import noppes.npcs.client.gui.player.GuiNpcAnvil; import noppes.npcs.client.gui.player.GuiNpcCarpentryBench; @@ -120,9 +129,9 @@ import noppes.npcs.client.gui.roles.GuiNpcFollowerSetup; import noppes.npcs.client.gui.roles.GuiNpcItemGiver; import noppes.npcs.client.gui.roles.GuiNpcTraderSetup; import noppes.npcs.client.gui.roles.GuiNpcTransporter; -import noppes.npcs.client.gui.script.GuiScriptBlock; import noppes.npcs.client.gui.script.GuiScriptGlobal; -import noppes.npcs.client.gui.script.GuiScriptItem; +import noppes.npcs.client.gui.script.GuiScriptInterface; +import noppes.npcs.client.gui.util.script.PackageFinder; import noppes.npcs.client.model.ModelNPCGolem; import noppes.npcs.client.model.ModelNpcCrystal; import noppes.npcs.client.model.ModelNpcDragon; @@ -136,8 +145,17 @@ import noppes.npcs.client.renderer.RenderNpcCrystal; import noppes.npcs.client.renderer.RenderNpcDragon; import noppes.npcs.client.renderer.RenderNpcSlime; import noppes.npcs.client.renderer.RenderProjectile; -import kamkeel.npcs.client.renderer.TelegraphRenderer; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; +import kamkeel.npcs.client.renderer.RenderAbilityOrb; +import kamkeel.npcs.client.renderer.RenderAbilityDisc; +import kamkeel.npcs.client.renderer.RenderAbilityLaser; +import kamkeel.npcs.client.renderer.RenderAbilityBeam; +import kamkeel.npcs.client.renderer.RenderAbilitySweeper; +import kamkeel.npcs.client.renderer.lightning.LightningHandler; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilitySweeper; import noppes.npcs.client.renderer.blocks.BlockBannerRenderer; import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer; import noppes.npcs.client.renderer.blocks.BlockBeamRenderer; @@ -191,6 +209,11 @@ import noppes.npcs.containers.ContainerNPCFollower; import noppes.npcs.containers.ContainerNPCFollowerHire; import noppes.npcs.containers.ContainerNPCFollowerSetup; import noppes.npcs.containers.ContainerNPCInv; +import noppes.npcs.containers.ContainerAuction; +import noppes.npcs.containers.ContainerAuctionBidding; +import noppes.npcs.containers.ContainerAuctionTrades; +import noppes.npcs.containers.ContainerAuctionListing; +import noppes.npcs.containers.ContainerAuctionSell; import noppes.npcs.containers.ContainerNPCTrader; import noppes.npcs.containers.ContainerNPCTraderSetup; import noppes.npcs.containers.ContainerNpcItemGiver; @@ -211,6 +234,7 @@ import noppes.npcs.entity.data.ModelPartData; import noppes.npcs.items.ItemLinked; import noppes.npcs.items.ItemNpcTool; import noppes.npcs.items.ItemScripted; +import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.input.Keyboard; import tconstruct.client.tabs.InventoryTabCustomNpc; import tconstruct.client.tabs.InventoryTabVanilla; @@ -239,6 +263,11 @@ public class ClientProxy extends CommonProxy { RenderingRegistry.registerEntityRenderingHandler(EntityNpcDragon.class, new RenderNpcDragon(new ModelNpcDragon(0.0F), 0.5F)); RenderingRegistry.registerEntityRenderingHandler(EntityNpcSlime.class, new RenderNpcSlime(new ModelNpcSlime(16), new ModelNpcSlime(0), 0.25F)); RenderingRegistry.registerEntityRenderingHandler(EntityProjectile.class, new RenderProjectile()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityOrb.class, new RenderAbilityOrb()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityDisc.class, new RenderAbilityDisc()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityLaser.class, new RenderAbilityLaser()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityBeam.class, new RenderAbilityBeam()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilitySweeper.class, new RenderAbilitySweeper()); RenderingRegistry.registerEntityRenderingHandler(EntityCustomNpc.class, new RenderCustomNpc()); @@ -291,7 +320,7 @@ public class ClientProxy extends CommonProxy { Minecraft mc = Minecraft.getMinecraft(); NPCButton = new KeyBinding("NPC Inventory", Keyboard.KEY_N, "key.categories.customnpc"); - SpecialKey = new KeyBinding("key.customnpcs.special", Keyboard.KEY_C, "key.categories.customnpc"); + SpecialKey = new KeyBinding("key.customnpcs.special", Keyboard.KEY_B, "key.categories.customnpc"); ClientRegistry.registerKeyBinding(NPCButton); ClientRegistry.registerKeyBinding(SpecialKey); @@ -312,6 +341,9 @@ public class ClientProxy extends CommonProxy { TelegraphManager.initClient(); MinecraftForge.EVENT_BUS.register(new TelegraphRenderer()); + // Lightning effect rendering system + MinecraftForge.EVENT_BUS.register(new LightningHandler()); + if (ConfigClient.InventoryGuiEnabled) { MinecraftForge.EVENT_BUS.register(new TabRegistry()); if (TabRegistry.getTabList().isEmpty()) { @@ -466,6 +498,20 @@ public class ClientProxy extends CommonProxy { else if (gui == EnumGuiType.ManageEffects) return new GuiNPCManageEffects(npc); + else if (gui == EnumGuiType.ManageAbilities) { + EntityCustomNpc abilityNpc; + if (npc != null) { + abilityNpc = new EntityCustomNpc(npc.worldObj); + abilityNpc.copyDataFrom(npc, true); + abilityNpc.display.showName = 1; + abilityNpc.display.showBossBar = 0; + } else { + abilityNpc = new EntityCustomNpc(Minecraft.getMinecraft().theWorld); + abilityNpc.display.texture = "customnpcs:textures/entity/humanmale/AnimationBody.png"; + } + return new GuiNpcManageAbilities(abilityNpc); + } + else if (gui == EnumGuiType.MainMenuGlobal) return new GuiNPCGlobalMainMenu(npc); @@ -481,6 +527,18 @@ public class ClientProxy extends CommonProxy { else if (gui == EnumGuiType.PlayerTrader) return new GuiNPCTrader(npc, (ContainerNPCTrader) container); + else if (gui == EnumGuiType.PlayerAuction) + return new GuiAuctionListing(npc, (ContainerAuctionListing) container); + + else if (gui == EnumGuiType.PlayerAuctionSell) + return new GuiAuctionSell(npc, (ContainerAuctionSell) container); + + else if (gui == EnumGuiType.PlayerAuctionTrades) + return new GuiAuctionTrades(npc, (ContainerAuctionTrades) container); + + else if (gui == EnumGuiType.PlayerAuctionBidding) + return new GuiAuctionBidding(npc, (ContainerAuctionBidding) container); + else if (gui == EnumGuiType.PlayerBankSmall || gui == EnumGuiType.PlayerBankUnlock || gui == EnumGuiType.PlayerBankUprade || gui == EnumGuiType.PlayerBankLarge) return new GuiNPCBankChest(npc, (ContainerNPCBankInterface) container); @@ -491,7 +549,7 @@ public class ClientProxy extends CommonProxy { return new GuiScript(npc); else if (gui == EnumGuiType.ScriptItem) - return new GuiScriptItem(); + return GuiScriptInterface.create(null, new ScriptCustomItem(new ItemStack(CustomItems.scripted_item))); else if (gui == EnumGuiType.PlayerCarpentryBench) return new GuiNpcCarpentryBench((ContainerCarpentryBench) container); @@ -566,7 +624,7 @@ public class ClientProxy extends CommonProxy { return new GuiCustom((ContainerCustomGui) container); else if (gui == EnumGuiType.ScriptBlock) - return new GuiScriptBlock(x, y, z); + return getScriptBlockGui(x, y, z); else if (gui == EnumGuiType.Paintbrush) return new GuiNpcPaintbrush(); @@ -618,9 +676,22 @@ public class ClientProxy extends CommonProxy { if (guiscreen != null) { minecraft.displayGuiScreen((GuiScreen) guiscreen); } - } + + private GuiScreen getScriptBlockGui(int x, int y, int z) { + World world = Minecraft.getMinecraft().theWorld; + if (world == null) + return null; + + TileEntity tile = world.getTileEntity(x, y, z); + if (!(tile instanceof TileScripted)) + return null; + + return GuiScriptInterface.create(null, (TileScripted) tile); + } + + @Override public void spawnParticle(EntityLivingBase player, String string, Object... ob) { if (string.equals("Spell")) { @@ -744,6 +815,13 @@ public class ClientProxy extends CommonProxy { return Minecraft.getMinecraft().currentScreen != null; } + public void buildPackageIndex() { + try { + PackageFinder.init(Thread.currentThread().getContextClassLoader()); + } catch (IOException e) { + } + } + public static class FontContainer { public StringCache textFont = null; public boolean useCustomFont = true; diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 7c5ec98f2..203cef3e2 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -5,11 +5,13 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.InputEvent; import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; +import kamkeel.npcs.client.renderer.lightning.LightningBolt; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.InputDevicePacket; -import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; +import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import net.minecraft.client.Minecraft; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; @@ -18,7 +20,6 @@ import net.minecraft.world.World; import noppes.npcs.CustomNpcs; import noppes.npcs.client.controllers.MusicController; import noppes.npcs.client.controllers.ScriptSoundController; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import noppes.npcs.client.gui.hud.ClientHudManager; import noppes.npcs.client.gui.hud.CompassHudComponent; import noppes.npcs.client.gui.hud.EnumHudComponent; @@ -115,6 +116,9 @@ public class ClientTickHandler { if (TelegraphManager.ClientInstance != null) { TelegraphManager.ClientInstance.tick(mc.theWorld); } + + // Update lightning bolts for ability effects + LightningBolt.updateAll(); if (Minecraft.getMinecraft().thePlayer != null && (prevWidth != mc.displayWidth || prevHeight != mc.displayHeight)) { prevWidth = mc.displayWidth; prevHeight = mc.displayHeight; diff --git a/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java b/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java index 132805af1..2f2bc3115 100644 --- a/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java +++ b/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java @@ -11,7 +11,7 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.File; import java.io.InputStream; diff --git a/src/main/java/noppes/npcs/client/controllers/MusicController.java b/src/main/java/noppes/npcs/client/controllers/MusicController.java index 98c30f2ac..172152f51 100644 --- a/src/main/java/noppes/npcs/client/controllers/MusicController.java +++ b/src/main/java/noppes/npcs/client/controllers/MusicController.java @@ -199,6 +199,7 @@ public class MusicController { /** * Stops a specific sound by its resource key. * This is used to stop dialog sounds without affecting Bard music. + * * @param soundKey The sound resource key to stop */ public void stopSound(String soundKey) { diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index ea9941b69..8d960e142 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -4,25 +4,19 @@ import kamkeel.npcs.network.packets.request.script.NPCScriptPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiConfirmOpenLink; import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; import net.minecraft.server.MinecraftServer; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; -import noppes.npcs.client.gui.script.GuiNPCEventScripts; +import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiMenuTopButton; -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.GuiNpcTextArea; import noppes.npcs.client.gui.util.GuiScriptTextArea; -import noppes.npcs.client.gui.util.ICustomScrollListener; -import noppes.npcs.client.gui.util.IGuiData; -import noppes.npcs.client.gui.util.IJTextAreaListener; -import noppes.npcs.client.gui.util.ITextChangeListener; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.DataScript; @@ -30,81 +24,162 @@ import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; -public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCallback, ICustomScrollListener, IJTextAreaListener, ITextChangeListener { +/** + * NPC Script GUI - extends GuiScriptInterface but with unique behavior: + * - Uses scroll-based tab selection instead of top buttons + * - Shows settings/script toggle + * - Uses DataScript with getNPCScript(index) access pattern + * - Has console filtering by hook type + */ +public class GuiScript extends GuiScriptInterface { public boolean showScript = false; - private int activeTab = 0; public DataScript script; - public Map> languages = new HashMap>(); private static int activeConsole = 0; - boolean loaded = false; public GuiScript(EntityNPCInterface npc) { - super(npc); - script = npc.script; - drawDefaultBackground = true; - closeOnEsc = true; - xSize = 420; + super(); + this.npc = npc; + this.script = npc.script; + this.handler = script; + this.useScrollTabs = true; + this.useSettingsToggle = true; + + // Initialize hook list for NPC scripts + hookList.add("script.init"); + hookList.add("script.update"); + hookList.add("script.interact"); + hookList.add("dialog.dialog"); + hookList.add("script.damaged"); + hookList.add("script.killed"); + hookList.add("script.attack"); + hookList.add("script.target"); + hookList.add("script.collide"); + hookList.add("script.kills"); + hookList.add("script.dialog_closed"); + hookList.add("script.timer"); + hookList.add("script.targetLost"); + hookList.add("script.projectileTick"); + hookList.add("script.projectileImpact"); - setBackground("menubg.png"); NPCScriptPacket.Get(); } + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.NPC; + } + + @Override + protected int getActiveScriptIndex() { + return this.activeTab; + } + + @Override + protected ScriptContainer getCurrentContainer() { + return script.getNPCScript(activeTab); + } + + @Override public void initGui() { super.initGui(); - guiTop += 10; - GuiMenuTopButton top; - addTopButton(top = new GuiMenuTopButton(13, guiLeft + 4, guiTop - 17, "script.scripts")); - addTopButton(new GuiMenuTopButton(16, guiLeft + (xSize - 102), guiTop - 17, "eventscript.eventScripts")); - addTopButton(new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X")); - top.active = showScript; - addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings")); - top.active = !showScript; - addTopButton(new GuiMenuTopButton(15, top, "gui.website")); + this.guiTop += 10; - List list = new ArrayList(); - list.add("script.init"); - list.add("script.update"); - list.add("script.interact"); - list.add("dialog.dialog"); - list.add("script.damaged"); - list.add("script.killed"); - list.add("script.attack"); - list.add("script.target"); - list.add("script.collide"); - list.add("script.kills"); - list.add("script.dialog_closed"); - list.add("script.timer"); - list.add("script.targetLost"); - list.add("script.projectileTick"); - list.add("script.projectileImpact"); + // ==================== TOP BUTTONS (hidden in fullscreen) ==================== + if (!isFullscreen) { + GuiMenuTopButton top; + addTopButton(top = new GuiMenuTopButton(13, guiLeft + 4, guiTop - 17, "script.scripts")); + addTopButton(new GuiMenuTopButton(16, guiLeft + (xSize - 102), guiTop - 17, "eventscript.eventScripts")); + addTopButton(new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X")); + top.active = showScript; + addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings")); + top.active = !showScript; + addTopButton(new GuiMenuTopButton(15, top, "gui.website")); + } if (showScript) { + initScriptView(); + } else { + initSettingsView(); + } + } + + private void initScriptView() { + ScriptContainer container = getCurrentContainer(); + + // ==================== CALCULATE VIEWPORT BOUNDS ==================== + int editorX, editorY, editorWidth, editorHeight; + + if (isFullscreen) { + // Fullscreen: viewport fills screen with configured padding + FullscreenConfig.paddingTop = 30; + FullscreenConfig.paddingBottom = 20; + FullscreenConfig.paddingLeft = 20; + FullscreenConfig.paddingRight = 20; + editorX = FullscreenConfig.paddingLeft; + editorY = FullscreenConfig.paddingTop; + editorWidth = this.width - FullscreenConfig.paddingLeft - FullscreenConfig.paddingRight; + editorHeight = this.height - FullscreenConfig.paddingTop - FullscreenConfig.paddingBottom; + } else { + // Normal: fixed layout with hooks on left + editorX = guiLeft + 74; + editorY = guiTop + 4; + editorWidth = 239; + editorHeight = 208; + } + + // ==================== HOOKS SCROLL (hidden in fullscreen) ==================== + if (!isFullscreen) { addLabel(new GuiNpcLabel(0, "script.hooks", guiLeft + 4, guiTop + 5)); + GuiCustomScroll hooks = new GuiCustomScroll(this, 1); hooks.setSize(68, 198); hooks.guiLeft = guiLeft + 4; hooks.guiTop = guiTop + 14; - hooks.setUnsortedList(list); + hooks.setUnsortedList(hookList); hooks.selected = activeTab; addScroll(hooks); + } - ScriptContainer container = script.getNPCScript(activeTab); + // ==================== SCRIPT TEXT AREA ==================== + int idx = getActiveScriptIndex(); + GuiScriptTextArea activeArea = getActiveScriptArea(); + if (activeArea == null) { + activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.script); + activeArea.setListener(this); + this.closeOnEsc(activeArea::closeOnEsc); + textAreas.put(idx, activeArea); + } else { + activeArea.init(editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.script); + } - GuiScriptTextArea ta = new GuiScriptTextArea(this, 2, guiLeft + 74, guiTop + 4, 239, 208, container == null ? "" : container.script); - ta.enableCodeHighlighting(); - ta.setListener(this); - this.addTextField(ta); + activeArea.setLanguage(script.getLanguage()); + activeArea.setScriptContext(getScriptContext()); + // Setup fullscreen key binding + GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { + if (e.isPress()) { + toggleFullscreen(); + } + }); + + activeArea.enableCodeHighlighting(); + addTextField(activeArea); + + // Initialize fullscreen button + int scrollbarOffset = activeArea.hasVerticalScrollbar() ? -8 : -2; + fullscreenButton.initGui(editorX + editorWidth, editorY, scrollbarOffset); + + // ==================== RIGHT PANEL (hidden in fullscreen) ==================== + if (!isFullscreen) { addButton(new GuiNpcButton(102, guiLeft + 315, guiTop + 4, 50, 20, "gui.clear")); addButton(new GuiNpcButton(101, guiLeft + 366, guiTop + 4, 50, 20, "gui.paste")); addButton(new GuiNpcButton(100, guiLeft + 315, guiTop + 25, 50, 20, "gui.copy")); - addButton(new GuiNpcButton(107, guiLeft + 315, guiTop + 70, 80, 20, "script.loadscript")); GuiCustomScroll scroll = new GuiCustomScroll(this, 0).setUnselectable(); @@ -114,55 +189,52 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall if (container != null) scroll.setList(container.scripts); addScroll(scroll); - } else { - addLabel(new GuiNpcLabel(0, "script.console", guiLeft + 4, guiTop + 16)); + } + } + + + private void initSettingsView() { + addLabel(new GuiNpcLabel(0, "script.console", guiLeft + 4, guiTop + 16)); + if (getTopButton(14) != null) getTopButton(14).active = true; - addTextField(new GuiNpcTextArea(2, this, guiLeft + 4, guiTop + 26, 226, 186, getConsoleText())); - getTextField(2).canEdit = false; - addButton(new GuiNpcButton(100, guiLeft + 232, guiTop + 170, 56, 20, "gui.copy")); - addButton(new GuiNpcButton(102, guiLeft + 232, guiTop + 192, 56, 20, "gui.clear")); - List l = new ArrayList(); - l.add("All"); - l.addAll(list); - addButton(new GuiNpcButton(105, guiLeft + 60, guiTop + 4, 80, 20, l.toArray(new String[l.size()]), activeConsole)); + GuiNpcTextArea consoleArea = new GuiNpcTextArea(2, this, guiLeft + 4, guiTop + 26, 226, 186, getConsoleText()); + consoleArea.canEdit = false; + addTextField(consoleArea); - addLabel(new GuiNpcLabel(1, "script.language", guiLeft + 232, guiTop + 30)); - addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, languages.keySet().toArray(new String[languages.keySet().size()]), getScriptIndex())); - getButton(103).enabled = languages.size() > 0; + addButton(new GuiNpcButton(100, guiLeft + 232, guiTop + 170, 56, 20, "gui.copy")); + addButton(new GuiNpcButton(102, guiLeft + 232, guiTop + 192, 56, 20, "gui.clear")); - addLabel(new GuiNpcLabel(2, "gui.enabled", guiLeft + 232, guiTop + 53)); - addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, new String[]{"gui.no", "gui.yes"}, script.enabled ? 1 : 0)); + List consoleOptions = new ArrayList<>(); + consoleOptions.add("All"); + consoleOptions.addAll(hookList); + addButton(new GuiNpcButton(105, guiLeft + 60, guiTop + 4, 80, 20, + consoleOptions.toArray(new String[0]), activeConsole)); - if (MinecraftServer.getServer() != null) - addButton(new GuiNpcButton(106, guiLeft + 232, guiTop + 71, 150, 20, "script.openfolder")); - } + addLabel(new GuiNpcLabel(1, "script.language", guiLeft + 232, guiTop + 30)); + List languageOptions = getLanguageOptions(); + addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, + languageOptions.toArray(new String[0]), getLanguageIndex(languageOptions))); + getButton(103).enabled = languageOptions.size() > 0; + + addLabel(new GuiNpcLabel(2, "gui.enabled", guiLeft + 232, guiTop + 53)); + addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, + new String[]{"gui.no", "gui.yes"}, script.enabled ? 1 : 0)); + + if (MinecraftServer.getServer() != null) + addButton(new GuiNpcButton(106, guiLeft + 232, guiTop + 71, 150, 20, "script.openfolder")); } - private int getScriptIndex() { - int i = 0; - for (String language : languages.keySet()) { - if (language.equalsIgnoreCase(script.scriptLanguage)) - return i; - i++; - } - return 0; - } - - private String getConsoleText() { + @Override + protected String getConsoleText() { Map map = this.script.getOldConsoleText(); StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); + for (Map.Entry entry : map.entrySet()) { + builder.insert(0, new Date(entry.getKey()) + entry.getValue() + "\n"); } - return builder.toString(); } - @Override public void confirmClicked(boolean result, int id) { NoppesUtil.openGUI(player, this); @@ -187,8 +259,6 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall } } } - - displayGuiScreen(this); } @@ -204,12 +274,11 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall initGui(); } if (guibutton.id == 15) { - GuiConfirmOpenLink guiyesno = new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true); - mc.displayGuiScreen(guiyesno); + displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); } if (guibutton.id == 16) { close(); - mc.displayGuiScreen(new GuiNPCEventScripts(npc)); + GuiScriptInterface.open(this, this.script); } if (guibutton.id == 17) { close(); @@ -218,12 +287,12 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall NoppesStringUtils.setClipboardContents(getTextField(2).getText()); } if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - displayGuiScreen(guiyesno); + displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), + StatCollector.translateToLocal("gui.sure"), 101)); } if (guibutton.id == 102) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - displayGuiScreen(guiyesno); + displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), + StatCollector.translateToLocal("gui.sure"), 102)); } if (guibutton.id == 103) { script.scriptLanguage = ((GuiNpcButton) guibutton).displayString; @@ -236,21 +305,20 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall initGui(); } if (guibutton.id == 106) { - // TODO: Uses ScriptController.Instance.dir (shared script repository); this GUI is gated behind the scripter tool - // and CustomNpcsPermissions.TOOL_SCRIPTER so only authorized editors touch the controller-backed scripts. NoppesUtil.openFolder(ScriptController.Instance.dir); } if (guibutton.id == 107) { - ScriptContainer container = script.getNPCScript(activeTab); + ScriptContainer container = getCurrentContainer(); if (container == null) script.setNPCScript(activeTab, container = new ScriptContainer(this.script)); setSubGui(new GuiScriptList(languages.get(script.scriptLanguage), container)); } } - private void setScript() { + @Override + protected void setScript() { if (showScript) { - ScriptContainer container = script.getNPCScript(activeTab); + ScriptContainer container = getCurrentContainer(); if (container == null) script.setNPCScript(activeTab, container = new ScriptContainer(this.script)); String text = getTextField(2).getText(); @@ -263,19 +331,7 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall @Override public void setGuiData(NBTTagCompound compound) { script.readFromNBT(compound); - NBTTagList data = compound.getTagList("Languages", 10); - Map> languages = new HashMap>(); - for (int i = 0; i < data.tagCount(); i++) { - NBTTagCompound comp = data.getCompoundTagAt(i); - List scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - for (int j = 0; j < list.tagCount(); j++) { - scripts.add(list.getStringTagAt(j)); - } - languages.put(comp.getString("Language"), scripts); - } - this.languages = languages; - initGui(); + loadLanguagesData(compound); loaded = true; } @@ -296,19 +352,23 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall } } - public void textUpdate(String text) { - ScriptContainer container = script.getNPCScript(activeTab); - if (container != null) - container.script = text; + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw fullscreen button when in script view (GuiScript uses 0-based activeTab) + if (showScript) { + fullscreenButton.draw(mouseX, mouseY); + } } @Override - public void saveText(String text) { - ScriptContainer container = script.getNPCScript(activeTab); - if (container != null) - container.script = text; - initGui(); + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + // Check fullscreen button first when in script view + if (showScript && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + return; + } + super.mouseClicked(mouseX, mouseY, mouseButton); } - } diff --git a/src/main/java/noppes/npcs/client/gui/GuiScriptList.java b/src/main/java/noppes/npcs/client/gui/GuiScriptList.java index 0d03d13cd..a13f2a056 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScriptList.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScriptList.java @@ -6,6 +6,7 @@ import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.data.IScriptUnit; import java.util.ArrayList; import java.util.List; @@ -13,16 +14,20 @@ import java.util.List; public class GuiScriptList extends SubGuiInterface { private GuiCustomScroll scroll1; private GuiCustomScroll scroll2; - private ScriptContainer container; + private IScriptUnit scriptUnit; private List scripts; public GuiScriptList(List scripts, ScriptContainer container) { - this.container = container; + this(scripts, (IScriptUnit) container); + } + + public GuiScriptList(List scripts, IScriptUnit scriptUnit) { + this.scriptUnit = scriptUnit; setBackground("menubg.png"); xSize = 346; ySize = 216; if (scripts == null) - scripts = new ArrayList(); + scripts = new ArrayList<>(); this.scripts = scripts; } @@ -46,10 +51,10 @@ public class GuiScriptList extends SubGuiInterface { scroll2.guiTop = guiTop + 14; this.addScroll(scroll2); addLabel(new GuiNpcLabel(2, "script.loadedScripts", guiLeft + 200, guiTop + 4)); - List temp = new ArrayList(scripts); - temp.removeAll(container.scripts); + List temp = new ArrayList<>(scripts); + temp.removeAll(scriptUnit.getExternalScripts()); scroll1.setList(temp); - scroll2.setList(container.scripts); + scroll2.setList(scriptUnit.getExternalScripts()); addButton(new GuiNpcButton(1, guiLeft + 145, guiTop + 40, 55, 20, ">")); addButton(new GuiNpcButton(2, guiLeft + 145, guiTop + 62, 55, 20, "<")); @@ -64,7 +69,7 @@ public class GuiScriptList extends SubGuiInterface { GuiNpcButton button = (GuiNpcButton) guibutton; if (button.id == 1) { if (scroll1.hasSelected()) { - container.scripts.add(scroll1.getSelected()); + scriptUnit.getExternalScripts().add(scroll1.getSelected()); scroll1.selected = -1; scroll2.selected = -1; initGui(); @@ -72,22 +77,22 @@ public class GuiScriptList extends SubGuiInterface { } if (button.id == 2) { if (scroll2.hasSelected()) { - container.scripts.remove(scroll2.getSelected()); + scriptUnit.getExternalScripts().remove(scroll2.getSelected()); scroll2.selected = -1; initGui(); } } if (button.id == 3) { - container.scripts.clear(); + scriptUnit.getExternalScripts().clear(); for (String script : scripts) { - container.scripts.add(script); + scriptUnit.getExternalScripts().add(script); } scroll1.selected = -1; scroll2.selected = -1; initGui(); } if (button.id == 4) { - container.scripts.clear(); + scriptUnit.getExternalScripts().clear(); scroll1.selected = -1; scroll2.selected = -1; initGui(); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java b/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java index 947825f5a..be8c07f8a 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java @@ -25,10 +25,10 @@ public class SubGuiColorSelector extends SubGuiInterface implements ITextfieldLi public int color; public SubGuiColorSelector(int color) { - xSize = 176; + xSize = 182; ySize = 222; this.color = color; - setBackground("smallbg.png"); + setBackground("menubg.png"); } @Override @@ -40,7 +40,7 @@ public class SubGuiColorSelector extends SubGuiInterface implements ITextfieldLi this.addTextField(textfield = new GuiNpcTextField(0, this, guiLeft + 53, guiTop + 20, 70, 20, getColor())); textfield.setTextColor(color); - addButton(new GuiNpcButton(66, guiLeft + 112, guiTop + 198, 60, 20, "gui.done")); + addButton(new GuiNpcButton(66, guiLeft + 112, guiTop + 175, 60, 20, "gui.done")); } public String getColor() { diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java index 19903229b..cfde598de 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java @@ -7,7 +7,7 @@ import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.client.gui.global.GuiNPCManageEffects; -import noppes.npcs.client.gui.script.GuiScriptEffect; +import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.GuiMenuTopButton; import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -17,6 +17,7 @@ import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.client.renderer.ImageData; import noppes.npcs.controllers.data.CustomEffect; +import noppes.npcs.controllers.data.EffectScript; import org.lwjgl.opengl.GL11; import java.util.ArrayList; @@ -202,10 +203,7 @@ public class SubGuiEffectGeneral extends SubGuiInterface implements ITextfieldLi } if (id == -2) { PacketClient.sendClient(new EffectSavePacket(effect.writeToNBT(false), originalName)); - GuiScriptEffect scriptGUI = new GuiScriptEffect(parent, effect); - scriptGUI.setWorldAndResolution(mc, width, height); - scriptGUI.initGui(); - mc.currentScreen = scriptGUI; + GuiScriptInterface.open(parent, new EffectScript(effect.id)); } if (id == 10) { GuiNpcButtonYesNo button = (GuiNpcButtonYesNo) guibutton; diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java index 72481f133..807a3e719 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java @@ -98,7 +98,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getTextField(18).setVisible(dialog.renderGradual); getLabel(18).enabled = dialog.renderGradual; - addTextField(new GuiNpcTextField(19, this, guiLeft + 125, y += 22, 40, 20, String.valueOf(dialog.textPitch))); + addTextField(new GuiNpcTextField(19, this, guiLeft + 125, y += 22, 40, 20, "" + dialog.textPitch)); addLabel(new GuiNpcLabel(19, "dialog.textPitch", guiLeft + 4, y + 5)); getTextField(19).floatsOnly = true; getTextField(19).setVisible(dialog.renderGradual); @@ -110,48 +110,48 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis addButton(new GuiNpcButton(24, guiLeft + 192, y, 50, 20, new String[]{"display.bottom", "display.top"}, dialog.alignment)); addLabel(new GuiNpcLabel(27, "display.alignment", guiLeft + 126, y + 5)); - addTextField(new GuiNpcTextField(11, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.textWidth))); - addTextField(new GuiNpcTextField(12, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.textHeight))); + addTextField(new GuiNpcTextField(11, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.textWidth)); + addTextField(new GuiNpcTextField(12, this, guiLeft + 165, y, 40, 20, "" + dialog.textHeight)); addLabel(new GuiNpcLabel(12, "gui.widthHeight", guiLeft + 4, y + 5)); getTextField(11).integersOnly = true; getTextField(11).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(12).integersOnly = true; getTextField(12).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(13, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.textOffsetX))); - addTextField(new GuiNpcTextField(14, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.textOffsetY))); + addTextField(new GuiNpcTextField(13, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.textOffsetX)); + addTextField(new GuiNpcTextField(14, this, guiLeft + 165, y, 40, 20, "" + dialog.textOffsetY)); addLabel(new GuiNpcLabel(15, "dialog.textOffset", guiLeft + 4, y + 5)); getTextField(13).integersOnly = true; getTextField(13).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.titleOffsetX))); - addTextField(new GuiNpcTextField(16, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.titleOffsetY))); + addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.titleOffsetX)); + addTextField(new GuiNpcTextField(16, this, guiLeft + 165, y, 40, 20, "" + dialog.titleOffsetY)); addLabel(new GuiNpcLabel(18, "dialog.titleOffset", guiLeft + 4, y + 5)); getTextField(15).integersOnly = true; getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(16).integersOnly = true; getTextField(16).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(17, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.optionOffsetX))); - addTextField(new GuiNpcTextField(18, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.optionOffsetY))); + addTextField(new GuiNpcTextField(17, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.optionOffsetX)); + addTextField(new GuiNpcTextField(18, this, guiLeft + 165, y, 40, 20, "" + dialog.optionOffsetY)); addLabel(new GuiNpcLabel(21, "dialog.optionOffset", guiLeft + 4, y + 5)); getTextField(17).integersOnly = true; getTextField(17).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(18).integersOnly = true; getTextField(18).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(19, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.optionSpaceX))); - addTextField(new GuiNpcTextField(20, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.optionSpaceY))); + addTextField(new GuiNpcTextField(19, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.optionSpaceX)); + addTextField(new GuiNpcTextField(20, this, guiLeft + 165, y, 40, 20, "" + dialog.optionSpaceY)); addLabel(new GuiNpcLabel(24, "dialog.optionSpacing", guiLeft + 4, y + 5)); getTextField(19).integersOnly = true; getTextField(19).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(20).integersOnly = true; getTextField(20).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(21, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.npcOffsetX))); - addTextField(new GuiNpcTextField(22, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.npcOffsetY))); + addTextField(new GuiNpcTextField(21, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.npcOffsetX)); + addTextField(new GuiNpcTextField(22, this, guiLeft + 165, y, 40, 20, "" + dialog.npcOffsetY)); addLabel(new GuiNpcLabel(25, "dialog.npcOffset", guiLeft + 4, y + 5)); getTextField(21).integersOnly = true; getTextField(21).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); @@ -161,7 +161,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getTextField(22).setVisible(!dialog.hideNPC); getLabel(25).enabled = !dialog.hideNPC; - addTextField(new GuiNpcTextField(23, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.npcScale))); + addTextField(new GuiNpcTextField(23, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.npcScale)); addLabel(new GuiNpcLabel(26, "dialog.npcScale", guiLeft + 4, y + 5)); getTextField(23).floatsOnly = true; getTextField(23).setMinMaxDefaultFloat(-Float.MAX_VALUE, Float.MAX_VALUE, 0); @@ -182,30 +182,30 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis DialogImage dialogImage = getSelectedImage(); addButton(new GuiNpcButton(2, guiLeft + xSize + 24, guiTop + 5, 20, 20, "-")); - addTextField(new GuiNpcTextField(10, this, guiLeft + 17, y += 25, 30, 20, String.valueOf(dialogImage.id))); + addTextField(new GuiNpcTextField(10, this, guiLeft + 17, y += 25, 30, 20, "" + dialogImage.id)); addTextField(new GuiNpcTextField(11, this, guiLeft + 92, y, 150, 20, dialogImage.texture)); addLabel(new GuiNpcLabel(10, "gui.id", guiLeft + 4, y + 5)); addLabel(new GuiNpcLabel(11, "display.texture", guiLeft + 50, y + 5)); getTextField(10).integersOnly = true; - addTextField(new GuiNpcTextField(12, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.x))); - addTextField(new GuiNpcTextField(13, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.y))); + addTextField(new GuiNpcTextField(12, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.x)); + addTextField(new GuiNpcTextField(13, this, guiLeft + 165, y, 40, 20, "" + dialogImage.y)); addLabel(new GuiNpcLabel(12, "gui.position", guiLeft + 4, y + 5)); getTextField(12).integersOnly = true; getTextField(12).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(13).integersOnly = true; getTextField(13).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.width))); - addTextField(new GuiNpcTextField(15, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.height))); + addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.width)); + addTextField(new GuiNpcTextField(15, this, guiLeft + 165, y, 40, 20, "" + dialogImage.height)); addLabel(new GuiNpcLabel(13, "gui.widthHeight", guiLeft + 4, y + 5)); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(15).integersOnly = true; getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(16, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.textureX))); - addTextField(new GuiNpcTextField(17, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.textureY))); + addTextField(new GuiNpcTextField(16, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.textureX)); + addTextField(new GuiNpcTextField(17, this, guiLeft + 165, y, 40, 20, "" + dialogImage.textureY)); addLabel(new GuiNpcLabel(14, "dialog.textureOffset", guiLeft + 4, y + 5)); getTextField(16).integersOnly = true; getTextField(16).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); @@ -229,9 +229,9 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getButton(19).setVisible(dialogImage.imageType == 2); getLabel(16).enabled = dialogImage.imageType == 2; - addTextField(new GuiNpcTextField(20, this, guiLeft + 33, y += 25, 30, 20, String.valueOf(dialogImage.scale))); - addTextField(new GuiNpcTextField(21, this, guiLeft + 102, y, 30, 20, String.valueOf(dialogImage.alpha))); - addTextField(new GuiNpcTextField(22, this, guiLeft + 183, y, 45, 20, String.valueOf(dialogImage.rotation))); + addTextField(new GuiNpcTextField(20, this, guiLeft + 33, y += 25, 30, 20, "" + dialogImage.scale)); + addTextField(new GuiNpcTextField(21, this, guiLeft + 102, y, 30, 20, "" + dialogImage.alpha)); + addTextField(new GuiNpcTextField(22, this, guiLeft + 183, y, 45, 20, "" + dialogImage.rotation)); addLabel(new GuiNpcLabel(17, "model.scale", guiLeft + 4, y + 5)); addLabel(new GuiNpcLabel(18, "display.alpha", guiLeft + 72, y + 5)); addLabel(new GuiNpcLabel(19, "movement.rotation", guiLeft + 140, y + 5)); @@ -306,13 +306,13 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis Collections.sort(ids); ArrayList strings = new ArrayList<>(); for (int i : ids) { - strings.add(String.valueOf(i)); + strings.add("" + i); } imageScroll.setUnsortedList(strings); if (changedId != -1) { - selected = imageScroll.getList().indexOf(String.valueOf(changedId)); - imageScroll.setSelected(String.valueOf(changedId)); + selected = imageScroll.getList().indexOf("" + changedId); + imageScroll.setSelected("" + changedId); changedId = -1; } @@ -402,7 +402,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis dialog.dialogImages.put(addId, new DialogImage(addId)); updateScrollData(); - selected = imageScroll.getList().indexOf(String.valueOf(addId)); + selected = imageScroll.getList().indexOf("" + addId); } if (button.id == 2) { if (imageScroll.getSelected() != null) { @@ -598,8 +598,8 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis } if (textfield.id == 10) { - if (imageScroll.getList().contains(String.valueOf(textfield.getInteger()))) { - textfield.setText(String.valueOf(dialogImage.id)); + if (imageScroll.getList().contains("" + textfield.getInteger())) { + textfield.setText("" + dialogImage.id); return; } diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java index 10420a65d..c5a0769a3 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java @@ -71,7 +71,7 @@ public class SubGuiNpcPartyOptions extends SubGuiInterface implements ITextfield y += 23; //min party size - number field GuiNpcTextField minField = new GuiNpcTextField(21, this, guiLeft + 60, guiTop + 10 + y, - 30, 20, String.valueOf(options.minPartySize)); + 30, 20, "" + options.minPartySize); minField.integersOnly = true; minField.setMinMaxDefault(1, Integer.MAX_VALUE, 1); addTextField(minField); @@ -79,7 +79,7 @@ public class SubGuiNpcPartyOptions extends SubGuiInterface implements ITextfield //max party size - number field GuiNpcTextField maxField = new GuiNpcTextField(20, this, guiLeft + 160, guiTop + 10 + y, - 30, 20, String.valueOf(options.maxPartySize)); + 30, 20, "" + options.maxPartySize); maxField.integersOnly = true; maxField.setMinMaxDefault(1, Integer.MAX_VALUE, 4); addTextField(maxField); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java b/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java index 9025023b1..c3d85c8ad 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java @@ -36,7 +36,7 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Fields - //////////////////////////////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////////////////////////////// // Current tab (10=Quest, 11=Dialog, 12=Transport, 13=Bank, 14=Faction, 15=Magic) private int currentTab = 10; @@ -99,7 +99,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Constructor - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// public SubGuiPlayerData(String playerName) { this.playerName = playerName; xSize = 420; @@ -112,7 +113,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Layout Helpers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// private int getPaddedLeft() { return guiLeft + leftPadding; } @@ -135,7 +137,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Component Setup Helpers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// private void setupTopButtons() { // Show General Display Information GuiMenuTopButton playerTab = new GuiMenuTopButton(-10, guiLeft + 4, guiTop - 10, playerName); @@ -192,7 +195,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // initGui() & Tab Initialization - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void initGui() { super.initGui(); @@ -489,7 +493,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Drawing Helpers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { super.drawScreen(mouseX, mouseY, partialTicks); @@ -525,7 +530,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Mouse & Resizing Handlers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void mouseClicked(int mouseX, int mouseY, int mouseButton) { int hitMargin = 3; @@ -672,7 +678,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Key & Search Field Handling - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void keyTyped(char c, int i) { super.keyTyped(c, i); @@ -894,7 +901,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Action Handling - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void actionPerformed(GuiButton button) { if (button.id == -5) { @@ -1043,7 +1051,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Data Setting Methods (IPlayerDataInfo) - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void setQuestData(Map questCategories, Map questActive, Map questFinished) { this.questCatData = new HashMap<>(questCategories); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java index c15f9162f..29c33352b 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java @@ -36,9 +36,9 @@ public class SubGuiSpawningOptions extends SubGuiInterface implements ITextfield addButton(new GuiNpcButtonYesNo(13, guiLeft + 120, y += 22, data.airSpawning)); addLabel(new GuiNpcLabel(13, "spawning.airSpawning", guiLeft + 4, y + 5)); - addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 22, 40, 20, String.valueOf(data.spawnHeightMin))); + addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMin)); addLabel(new GuiNpcLabel(14, "spawning.minHeight", guiLeft + 4, y + 5)); - addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 22, 40, 20, String.valueOf(data.spawnHeightMax))); + addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMax)); addLabel(new GuiNpcLabel(15, "spawning.maxHeight", guiLeft + 4, y + 5)); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index 584bd8238..59046ef77 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilitySlot; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; @@ -10,7 +11,18 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.resources.I18n; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNPCInterface2; +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.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.IScrollData; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.constants.EnumScrollData; import noppes.npcs.entity.EntityNPCInterface; @@ -35,12 +47,13 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Display name to typeId mapping for the available types scroll private final HashMap displayNameToTypeId = new HashMap<>(); - // NPC's current abilities - private final List npcAbilities = new ArrayList<>(); + // NPC's current ability slots + private final List npcSlots = new ArrayList<>(); // Settings private boolean abilitiesEnabled = false; - private int globalCooldown = 20; + private int minCooldown = 20; + private int maxCooldown = 60; private String search = ""; private int selectedAbilityIndex = -1; @@ -61,12 +74,18 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addLabel(new GuiNpcLabel(100, "gui.enabled", guiLeft + 5, y + 5)); addButton(new GuiNpcButton(100, guiLeft + 55, y, 50, 20, new String[]{"gui.no", "gui.yes"}, abilitiesEnabled ? 1 : 0)); - // Global cooldown (spaced out since combat only is removed) - addLabel(new GuiNpcLabel(101, "ability.globalCooldown", guiLeft + 140, y + 5)); - GuiNpcTextField cooldownField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 230, y, 50, 20, String.valueOf(globalCooldown)); - cooldownField.setIntegersOnly(); - cooldownField.setMinMaxDefault(0, 10000, 20); - addTextField(cooldownField); + // Cooldown range: Min Cooldown and Max Cooldown (spaced apart more) + addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 115, y + 5)); + GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 165, y, 40, 20, "" + minCooldown); + minField.setIntegersOnly(); + minField.setMinMaxDefault(0, 10000, 20); + addTextField(minField); + + addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 260, y + 5)); + GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 310, y, 40, 20, "" + maxCooldown); + maxField.setIntegersOnly(); + maxField.setMinMaxDefault(0, 10000, 60); + addTextField(maxField); y += 28; @@ -105,6 +124,14 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Load button (under the add/remove arrows) addButton(new GuiNpcButton(75, centerX, y + 90, 40, 20, "gui.load")); + // On/Off toggle for selected ability (under Load button) + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability selectedAbility = npcSlots.get(selectedAbilityIndex).getAbility(); + GuiNpcButton toggleBtn = new GuiNpcButton(77, centerX, y + 112, 40, 20, + new String[]{"gui.off", "gui.on"}, selectedAbility.isEnabled() ? 1 : 0); + addButton(toggleBtn); + } + // Right side buttons: Edit and Up/Down carrots with Save button addButton(new GuiNpcButton(72, guiLeft + 210, y + 145, 55, 20, "gui.edit")); getButton(72).setEnabled(selectedAbilityIndex >= 0); @@ -113,7 +140,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addButton(new GuiNpcButton(73, guiLeft + 270, y + 145, 20, 20, "<")); addButton(new GuiNpcButton(74, guiLeft + 292, y + 145, 20, 20, ">")); getButton(73).setEnabled(selectedAbilityIndex > 0); - getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size() - 1); + getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1); // Save button (to the right of carrots, only enabled when ability is selected) addButton(new GuiNpcButton(76, guiLeft + 320, y + 145, 50, 20, "gui.save")); @@ -140,10 +167,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC Ability newAbility = AbilityController.Instance.create(typeId); if (newAbility != null) { newAbility.setId(UUID.randomUUID().toString()); - // Don't set custom name - let it default to type name - npcAbilities.add(newAbility); - // Auto-select the newly added ability - selectedAbilityIndex = npcAbilities.size() - 1; + npcSlots.add(AbilitySlot.inline(newAbility)); + selectedAbilityIndex = npcSlots.size() - 1; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); initGui(); @@ -156,8 +181,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Remove ability if (id == 71) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - npcAbilities.remove(selectedAbilityIndex); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + npcSlots.remove(selectedAbilityIndex); selectedAbilityIndex = -1; updateNpcAbilitiesList(); initGui(); @@ -168,9 +193,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Edit ability if (id == 72) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability ability = npcAbilities.get(selectedAbilityIndex); - setSubGui(ability.createConfigGui(this)); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + // Reference slot - ask clone-and-modify vs modify-parent + setSubGui(new SubGuiAbilityEditMode()); + } else { + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } } return; } @@ -178,9 +211,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Move up (carrot left = earlier in list) if (id == 73) { if (selectedAbilityIndex > 0) { - Ability temp = npcAbilities.get(selectedAbilityIndex); - npcAbilities.set(selectedAbilityIndex, npcAbilities.get(selectedAbilityIndex - 1)); - npcAbilities.set(selectedAbilityIndex - 1, temp); + AbilitySlot temp = npcSlots.get(selectedAbilityIndex); + npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex - 1)); + npcSlots.set(selectedAbilityIndex - 1, temp); selectedAbilityIndex--; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); @@ -192,10 +225,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Move down (carrot right = later in list) if (id == 74) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size() - 1) { - Ability temp = npcAbilities.get(selectedAbilityIndex); - npcAbilities.set(selectedAbilityIndex, npcAbilities.get(selectedAbilityIndex + 1)); - npcAbilities.set(selectedAbilityIndex + 1, temp); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1) { + AbilitySlot temp = npcSlots.get(selectedAbilityIndex); + npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex + 1)); + npcSlots.set(selectedAbilityIndex + 1, temp); selectedAbilityIndex++; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); @@ -211,15 +244,31 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC return; } - // Save selected ability + // Save selected ability as custom preset if (id == 76) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability abilityToSave = npcAbilities.get(selectedAbilityIndex); - if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) { - // Need a name to save - open edit dialog first - setSubGui(abilityToSave.createConfigGui(this)); - } else { - setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave)); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability abilityToSave = npcSlots.get(selectedAbilityIndex).getAbility(); + if (abilityToSave != null) { + if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) { + setSubGui(abilityToSave.createConfigGui(this)); + } else { + setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave)); + } + } + } + return; + } + + // Toggle selected ability on/off + if (id == 77) { + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability ability = npcSlots.get(selectedAbilityIndex).getAbility(); + if (ability != null) { + ability.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); + updateNpcAbilitiesList(); + selectAbilityByIndex(selectedAbilityIndex); + initGui(); + save(); } } return; @@ -242,16 +291,29 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void setGuiData(NBTTagCompound compound) { - abilitiesEnabled = compound.getBoolean("AbilitiesEnabled"); - globalCooldown = compound.hasKey("AbilityGlobalCooldown") ? compound.getInteger("AbilityGlobalCooldown") : 20; + // Only process NPC ability data, not saved ability preset data + // SubGuiAbilityLoad also implements IGuiData and may receive responses + // that get forwarded here - ignore those by checking for expected keys + if (!compound.hasKey("Abilities")) { + return; + } - npcAbilities.clear(); + abilitiesEnabled = compound.getBoolean("AbilitiesEnabled"); + + minCooldown = compound.getInteger("AbilityMinCooldown"); + maxCooldown = compound.getInteger("AbilityMaxCooldown"); + if (minCooldown == 0 && maxCooldown == 0) { + minCooldown = 20; + maxCooldown = 60; + } + + npcSlots.clear(); NBTTagList abilityList = compound.getTagList("Abilities", 10); for (int i = 0; i < abilityList.tagCount(); i++) { NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i); - Ability ability = AbilityController.Instance.fromNBT(abilityNBT); - if (ability != null) { - npcAbilities.add(ability); + AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT); + if (slot != null) { + npcSlots.add(slot); } } @@ -294,7 +356,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC private void updateNpcAbilitiesList() { List list = new ArrayList<>(); - for (int i = 0; i < npcAbilities.size(); i++) { + for (int i = 0; i < npcSlots.size(); i++) { list.add(getAbilityListEntry(i)); } if (npcAbilitiesScroll != null) { @@ -303,26 +365,41 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } /** - * Creates the display entry for an ability in the list. - * Format: "[●] 1. Custom Name (Type)" or "[●] 1. Type" if no custom name - * Circle is green (§a) for enabled, red (§c) for disabled + * Creates the display entry for an ability slot in the list. + * Inline: "[●] 1. Custom Name (Type)" or "[●] 1. Type" + * Reference: "[●] 1. > Name (Type)" in yellow + * Broken: "[●] 1. > [Missing: id...]" in red */ private String getAbilityListEntry(int index) { - Ability ability = npcAbilities.get(index); + AbilitySlot slot = npcSlots.get(index); + Ability ability = slot.getAbility(); + + if (ability == null) { + // Broken reference + String refId = slot.getReferenceId(); + String shortId = refId != null && refId.length() > 8 ? refId.substring(0, 8) + "..." : refId; + return "\u00A7c\u25CF\u00A7r " + (index + 1) + ". \u00A7c> [Missing: " + shortId + "]\u00A7r"; + } + String typeName = getDisplayName(ability.getTypeId()); String customName = ability.getName(); - - // Color prefix: green circle for enabled, red for disabled String colorPrefix = ability.isEnabled() ? "\u00A7a\u25CF\u00A7r " : "\u00A7c\u25CF\u00A7r "; + String nameDisplay; if (customName != null && !customName.isEmpty() && !customName.equals(typeName)) { - return colorPrefix + (index + 1) + ". " + customName + " (" + typeName + ")"; + nameDisplay = customName + " (" + typeName + ")"; + } else { + nameDisplay = typeName; } - return colorPrefix + (index + 1) + ". " + typeName; + + if (slot.isReference()) { + return colorPrefix + (index + 1) + ". \u00A7e> " + nameDisplay + "\u00A7r"; + } + return colorPrefix + (index + 1) + ". " + nameDisplay; } private void selectAbilityByIndex(int index) { - if (index >= 0 && index < npcAbilities.size()) { + if (index >= 0 && index < npcSlots.size()) { String entry = getAbilityListEntry(index); if (npcAbilitiesScroll != null) { npcAbilitiesScroll.setSelected(entry); @@ -341,11 +418,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC public void save() { NBTTagCompound compound = new NBTTagCompound(); compound.setBoolean("AbilitiesEnabled", abilitiesEnabled); - compound.setInteger("AbilityGlobalCooldown", globalCooldown); + compound.setInteger("AbilityMinCooldown", minCooldown); + compound.setInteger("AbilityMaxCooldown", maxCooldown); NBTTagList abilityList = new NBTTagList(); - for (Ability ability : npcAbilities) { - abilityList.appendTag(ability.writeNBT()); + for (AbilitySlot slot : npcSlots) { + abilityList.appendTag(slot.writeNBT()); } compound.setTag("Abilities", abilityList); @@ -353,15 +431,29 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } /** - * Called from SubGuiAbilityLoad when a saved ability is loaded. - * Adds the ability to the NPC's ability list. + * Called from SubGuiAbilityLoad when a cloned ability is loaded. + * Adds as an inline slot. */ public void loadAbility(Ability loadedAbility) { if (loadedAbility != null) { loadedAbility.setId(UUID.randomUUID().toString()); - npcAbilities.add(loadedAbility); - // Auto-select the newly added ability - selectedAbilityIndex = npcAbilities.size() - 1; + npcSlots.add(AbilitySlot.inline(loadedAbility)); + selectedAbilityIndex = npcSlots.size() - 1; + updateNpcAbilitiesList(); + selectAbilityByIndex(selectedAbilityIndex); + initGui(); + save(); + } + } + + /** + * Called from SubGuiAbilityLoad when a reference is loaded. + * Adds as a reference slot. + */ + public void loadAbilityReference(String referenceId) { + if (referenceId != null && !referenceId.isEmpty()) { + npcSlots.add(AbilitySlot.reference(referenceId)); + selectedAbilityIndex = npcSlots.size() - 1; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); initGui(); @@ -401,24 +493,68 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability ability = npcAbilities.get(selectedAbilityIndex); - setSubGui(ability.createConfigGui(this)); + if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + setSubGui(new SubGuiAbilityEditMode()); + } else { + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } } } @Override public void unFocused(GuiNpcTextField textField) { if (textField.id == 101) { - globalCooldown = textField.getInteger(); + minCooldown = textField.getInteger(); + // Ensure min <= max + if (minCooldown > maxCooldown) { + maxCooldown = minCooldown; + initGui(); + } + save(); + } else if (textField.id == 102) { + maxCooldown = textField.getInteger(); + // Ensure min <= max + if (maxCooldown < minCooldown) { + minCooldown = maxCooldown; + initGui(); + } save(); } } @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiAbilityConfig) { - // Ability was saved in the SubGui, refresh list + if (subgui instanceof SubGuiAbilityEditMode) { + int mode = ((SubGuiAbilityEditMode) subgui).getResult(); + if (mode < 0) return; // cancelled + + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) { + // Convert reference to inline, then edit + if (slot.convertToInline()) { + Ability ability = slot.getAbility(); + if (ability != null) { + ability.setId(UUID.randomUUID().toString()); + setSubGui(ability.createConfigGui(this)); + } + updateNpcAbilitiesList(); + save(); + } + } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) { + // Edit the resolved ability directly (changes propagate to controller) + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } + } + } else if (subgui instanceof SubGuiAbilityConfig) { updateNpcAbilitiesList(); save(); } @@ -428,8 +564,15 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC * Called from SubGuiAbilityConfig when ability is saved. */ public void onAbilitySaved(Ability ability) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - npcAbilities.set(selectedAbilityIndex, ability); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + // For reference slots, save back to controller + // (The resolved copy was edited in-place) + } else { + // For inline slots, replace with the updated ability + npcSlots.set(selectedAbilityIndex, AbilitySlot.inline(ability)); + } updateNpcAbilitiesList(); save(); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index c88c18095..04fa3f7f9 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -1,161 +1,108 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.Condition; -import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.FieldDefinitionsBuilder; +import kamkeel.npcs.controllers.data.ability.gui.FieldType; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.SubGuiColorSelector; -import noppes.npcs.client.gui.select.GuiAnimationSelection; -import noppes.npcs.client.gui.select.GuiSoundSelection; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +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.GuiScrollWindow; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; import java.util.List; -/** - * Base SubGui for editing ability configuration with tabbed interface. - * Uses GuiMenuTopButton for navigation tabs. - * - * Tabs: - * - General: Name, Weight, Lock Movement, Interruptible, Timing - * - Type: Type-specific settings per Ability (override in subclasses) - * - Target: Min/Max Range, Targeting Mode, Conditions (up to 3) - * - Effects: Sounds, Animations, Telegraph settings, Colors - * - * Subclasses should override initTypeTab() and handleTypeButton()/handleTypeTextField() - * to provide type-specific settings UI. - */ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldListener, ISubGuiListener { - // Tab constants - protected static final int TAB_GENERAL = 0; - protected static final int TAB_TYPE = 1; - protected static final int TAB_TARGET = 2; - protected static final int TAB_EFFECTS = 3; + private static final int DECLARATIVE_ID_START = 1000; + private static final int CLEAR_ID_START = 2000; + private static final int LABEL_ID_START = 3000; - // Core references - protected final Ability ability; - protected final IAbilityConfigCallback callback; - protected int activeTab = TAB_GENERAL; + // Fixed tab indices + private static final int TAB_GENERAL = 0; + private static final int TAB_TYPE = 1; + private static final int TAB_TARGET = 2; + private static final int TAB_EFFECTS = 3; + // Custom tabs start at index 4 - // ═══════════════════════════════════════════════════════════════════════════ - // CACHED VALUES - General Tab - // ═══════════════════════════════════════════════════════════════════════════ - protected String name; - protected boolean enabled; - protected int weight; - protected boolean lockMovement; - protected boolean interruptible; + private static final int L_LABEL_X = 5; + private static final int ROW_H = 24; - // Timing - protected int cooldownTicks; - protected int windUpTicks; - protected int activeTicks; - protected int recoveryTicks; + private final Ability ability; + private final IAbilityConfigCallback callback; + private int activeTab = TAB_GENERAL; - // ═══════════════════════════════════════════════════════════════════════════ - // CACHED VALUES - Target Tab - // ═══════════════════════════════════════════════════════════════════════════ - protected float minRange; - protected float maxRange; - protected TargetingMode targetingMode; - protected List conditions; + private List fieldDefs; + private List customTabNames; - // ═══════════════════════════════════════════════════════════════════════════ - // CACHED VALUES - Effects Tab - // ═══════════════════════════════════════════════════════════════════════════ - protected String windUpSound; - protected String activeSound; - protected int windUpAnimationId; - protected int activeAnimationId; - protected String windUpAnimationName = null; - protected String activeAnimationName = null; + private List conditions; + private int editingConditionIndex = -1; - // Telegraph - protected boolean showTelegraph; - protected int windUpColor; - protected int activeColor; + private FieldDefinitionsBuilder builder; + private FieldDef activeSubGuiField = null; - // Editing state - protected int editingColorId = 0; - protected int editingSoundId = 0; - protected int editingAnimationId = 0; - protected int editingConditionIndex = -1; - - // ═══════════════════════════════════════════════════════════════════════════ - // ABILITY TYPE FEATURES - // ═══════════════════════════════════════════════════════════════════════════ - protected final boolean supportsTelegraph; - protected final boolean targetingModeLocked; - protected final boolean hasTypeSettings; + // Scroll position preservation per tab + private float[] tabScrollY; public SubGuiAbilityConfig(Ability ability, IAbilityConfigCallback callback) { this.ability = ability; this.callback = callback; - - // Cache values from ability - General - this.name = ability.getName() != null ? ability.getName() : ""; - this.enabled = ability.isEnabled(); - this.weight = ability.getWeight(); - this.lockMovement = ability.isLockMovement(); - this.interruptible = ability.isInterruptible(); - - // Timing - this.cooldownTicks = ability.getCooldownTicks(); - this.windUpTicks = ability.getWindUpTicks(); - this.activeTicks = ability.getActiveTicks(); - this.recoveryTicks = ability.getRecoveryTicks(); - - // Target - this.minRange = ability.getMinRange(); - this.maxRange = ability.getMaxRange(); - this.targetingMode = ability.getTargetingMode(); this.conditions = new ArrayList<>(ability.getConditions()); - // Effects - this.windUpSound = ability.getWindUpSound(); - this.activeSound = ability.getActiveSound(); - this.windUpAnimationId = ability.getWindUpAnimationId(); - this.activeAnimationId = ability.getActiveAnimationId(); + this.fieldDefs = ability.getAllFieldDefinitions(); + discoverCustomTabs(); - // Telegraph - this.showTelegraph = ability.isShowTelegraph(); - this.windUpColor = ability.getWindUpColor(); - this.activeColor = ability.getActiveColor(); - - // Determine ability type features - TelegraphType defaultType = ability.getTelegraphType(); - this.supportsTelegraph = defaultType != null && defaultType != TelegraphType.NONE; - this.targetingModeLocked = ability.isTargetingModeLocked(); - this.hasTypeSettings = ability.hasTypeSettings(); - - setBackground("menubg.png"); + setBackground("menubg.png", 217); xSize = 356; ySize = 200; } + private void discoverCustomTabs() { + customTabNames = new ArrayList<>(); + for (FieldDef def : fieldDefs) { + if (def.getTab() == TabTarget.CUSTOM && def.getCustomTabName() != null + && !customTabNames.contains(def.getCustomTabName())) { + customTabNames.add(def.getCustomTabName()); + } + } + tabScrollY = new float[4 + customTabNames.size()]; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // INIT GUI + // ═══════════════════════════════════════════════════════════════════════════ + @Override public void initGui() { + // Save scroll position before super clears scroll windows + GuiScrollWindow oldSw = getScrollableGui(0); + if (oldSw != null && activeTab < tabScrollY.length) { + tabScrollY[activeTab] = oldSw.nextScrollY; + } + super.initGui(); - // ═══════════════════════════════════════════════════════════════════════ - // TOP TABS using GuiMenuTopButton - // ═══════════════════════════════════════════════════════════════════════ + // Tab buttons — always show General, Type, Target, Effects GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "menu.general"); generalTab.active = (activeTab == TAB_GENERAL); addTopButton(generalTab); - GuiMenuTopButton lastTab = generalTab; - if (hasTypeSettings) { - GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "gui.type"); - typeTab.active = (activeTab == TAB_TYPE); - addTopButton(typeTab); - lastTab = typeTab; - } + GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "gui.type"); + typeTab.active = (activeTab == TAB_TYPE); + addTopButton(typeTab); + lastTab = typeTab; GuiMenuTopButton targetTab = new GuiMenuTopButton(92, lastTab, "script.target"); targetTab.active = (activeTab == TAB_TARGET); @@ -165,210 +112,122 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi GuiMenuTopButton effectsTab = new GuiMenuTopButton(93, lastTab, "ability.tab.effects"); effectsTab.active = (activeTab == TAB_EFFECTS); addTopButton(effectsTab); + lastTab = effectsTab; + + // Custom tabs (Visual, etc.) + for (int i = 0; i < customTabNames.size(); i++) { + GuiMenuTopButton ct = new GuiMenuTopButton(94 + i, lastTab, customTabNames.get(i)); + ct.active = (activeTab == 4 + i); + addTopButton(ct); + lastTab = ct; + } - // Close button (X) in top menu bar - use -1000 to avoid any ID conflicts GuiMenuTopButton closeBtn = new GuiMenuTopButton(-1000, guiLeft + xSize - 22, guiTop - 17, "X"); addTopButton(closeBtn); - int contentY = guiTop + 5; + // Scroll window + int swX = guiLeft + 4; + int swY = guiTop + 5; + int swW = xSize - 8; + int swH = ySize - 10; - // ═══════════════════════════════════════════════════════════════════════ - // TAB CONTENT - // ═══════════════════════════════════════════════════════════════════════ - switch (activeTab) { - case TAB_GENERAL: - initGeneralTab(contentY); - break; - case TAB_TYPE: - initTypeTab(contentY); - break; - case TAB_TARGET: - initTargetTab(contentY); - break; - case TAB_EFFECTS: - initEffectsTab(contentY); - break; + GuiScrollWindow sw = new GuiScrollWindow(this, swX, swY, swW, swH, 0); + sw.backgroundColor = 0x88000000; + addScrollableGui(0, sw); + + int y = 5; + int labelCounter = LABEL_ID_START; + + // General tab: type label at top + if (activeTab == TAB_GENERAL) { + sw.addLabel(new GuiNpcLabel(labelCounter++, "gui.type", 5, y + 5, 0xFFFFFF)); + sw.addLabel(new GuiNpcLabel(labelCounter++, ability.getTypeId(), 55, y + 5, 0xFFFFFF)); + y += ROW_H; } - // ═══════════════════════════════════════════════════════════════════════ - // BOTTOM ROW - Enabled toggle - // ═══════════════════════════════════════════════════════════════════════ - addLabel(new GuiNpcLabel(99, "gui.enabled", guiLeft + 8, guiTop + ySize - 18)); - GuiNpcButton enabledBtn = new GuiNpcButton(2, guiLeft + 55, guiTop + ySize - 24, 40, 20, new String[]{"gui.no", "gui.yes"}, enabled ? 1 : 0); - enabledBtn.setTextColor(enabled ? 0x00FF00 : 0xFF0000); - addButton(enabledBtn); + // Build declarative fields using the builder + List tabFields = getVisibleFieldsForTab(activeTab); + builder = new FieldDefinitionsBuilder(this, sw, fontRendererObj) + .startIds(DECLARATIVE_ID_START, CLEAR_ID_START, labelCounter) + .startY(y); + + y = builder.build(tabFields); + + // Conditions on target tab + if (activeTab == TAB_TARGET) { + y = renderConditions(sw, y, builder.getNextLabelId()); + } + + sw.maxScrollY = Math.max(y - swH, 0); + + // Restore scroll position + if (activeTab < tabScrollY.length) { + float restored = Math.min(tabScrollY[activeTab], sw.maxScrollY); + sw.nextScrollY = restored; + sw.scrollY = restored; + } } - // ═══════════════════════════════════════════════════════════════════════════ - // GENERAL TAB - // ═══════════════════════════════════════════════════════════════════════════ + private List getVisibleFieldsForTab(int tabIndex) { + List result = new ArrayList<>(); - private void initGeneralTab(int startY) { - int y = startY; - int col1LabelX = guiLeft + 8; - int col1FieldX = guiLeft + 70; - int col2LabelX = guiLeft + 180; - int col2FieldX = guiLeft + 248; - - // Row 1: Type (read-only) + Name - addLabel(new GuiNpcLabel(1, "gui.type", col1LabelX, y + 5)); - addLabel(new GuiNpcLabel(2, ability.getTypeId(), col1FieldX, y + 5)); - - addLabel(new GuiNpcLabel(3, "gui.name", col2LabelX, y + 5)); - GuiNpcTextField nameField = new GuiNpcTextField(1, this, fontRendererObj, col2FieldX, y, 100, 20, name); - addTextField(nameField); - - y += 24; - - // Row 2: Weight + Lock Movement - addLabel(new GuiNpcLabel(4, "ability.weight", col1LabelX, y + 5)); - GuiNpcTextField weightField = new GuiNpcTextField(3, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(weight)); - weightField.setIntegersOnly(); - weightField.setMinMaxDefault(1, 1000, 10); - addTextField(weightField); - - addLabel(new GuiNpcLabel(5, "ability.lockMove", col2LabelX, y + 5)); - addButton(new GuiNpcButton(16, col2FieldX, y, 40, 20, new String[]{"gui.no", "gui.yes"}, lockMovement ? 1 : 0)); - - y += 24; - - // Row 3: Interruptible - addLabel(new GuiNpcLabel(6, "ability.interruptible", col1LabelX, y + 5)); - addButton(new GuiNpcButton(14, guiLeft + 85, y, 40, 20, new String[]{"gui.no", "gui.yes"}, interruptible ? 1 : 0)); - - y += 28; - - // Separator - Timing section header - addLabel(new GuiNpcLabel(7, "ability.timing", col1LabelX, y)); - y += 14; - - // Row 4: Windup Ticks + Active Ticks - addLabel(new GuiNpcLabel(10, "ability.windup", col1LabelX, y + 5)); - GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks)); - windupField.setIntegersOnly(); - windupField.setMinMaxDefault(0, 1000, 20); - addTextField(windupField); - - addLabel(new GuiNpcLabel(11, "ability.active", col2LabelX, y + 5)); - GuiNpcTextField activeField = new GuiNpcTextField(12, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(activeTicks)); - activeField.setIntegersOnly(); - activeField.setMinMaxDefault(1, 1000, 10); - addTextField(activeField); - - y += 24; - - // Row 5: Recovery Ticks + Cooldown Ticks - addLabel(new GuiNpcLabel(12, "ability.recovery", col1LabelX, y + 5)); - GuiNpcTextField recoveryField = new GuiNpcTextField(13, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(recoveryTicks)); - recoveryField.setIntegersOnly(); - recoveryField.setMinMaxDefault(0, 1000, 20); - addTextField(recoveryField); - - addLabel(new GuiNpcLabel(13, "ability.cooldown", col2LabelX, y + 5)); - GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(cooldownTicks)); - cooldownField.setIntegersOnly(); - cooldownField.setMinMaxDefault(0, 10000, 100); - addTextField(cooldownField); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // TYPE TAB - Override in subclasses for type-specific settings - // ═══════════════════════════════════════════════════════════════════════════ - - /** - * Override this method to render type-specific settings in the Type tab. - * Use the helper methods createFloatField(), createIntField(), etc. - * Button/TextField IDs should start at 100. - * - * @param startY The Y position to start rendering from - */ - protected void initTypeTab(int startY) { - // Default: show message that no type settings are available - addLabel(new GuiNpcLabel(100, "ability.noTypeSettings", guiLeft + 8, startY + 5)); - } - - /** - * Override this method to handle button clicks for type-specific buttons. - * Button IDs 100+ are reserved for type-specific use. - */ - protected void handleTypeButton(int id, GuiNpcButton button) { - // Default: no-op - } - - /** - * Override this method to handle text field changes for type-specific fields. - * Field IDs 100+ are reserved for type-specific use. - */ - protected void handleTypeTextField(int id, GuiNpcTextField field) { - // Default: no-op - } - - // ═══════════════════════════════════════════════════════════════════════════ - // TARGET TAB - // ═══════════════════════════════════════════════════════════════════════════ - - private void initTargetTab(int startY) { - int y = startY; - int col1LabelX = guiLeft + 8; - int col1FieldX = guiLeft + 70; - int col2LabelX = guiLeft + 180; - int col2FieldX = guiLeft + 248; - - // Row 1: Min Range + Max Range - addLabel(new GuiNpcLabel(20, "ability.minRange", col1LabelX, y + 5)); - GuiNpcTextField minRangeField = new GuiNpcTextField(5, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf((int) minRange)); - minRangeField.setIntegersOnly(); - minRangeField.setMinMaxDefault(0, 100, 0); - addTextField(minRangeField); - - addLabel(new GuiNpcLabel(21, "ability.maxRange", col2LabelX, y + 5)); - GuiNpcTextField maxRangeField = new GuiNpcTextField(6, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf((int) maxRange)); - maxRangeField.setIntegersOnly(); - maxRangeField.setMinMaxDefault(1, 100, 20); - addTextField(maxRangeField); - - y += 24; - - // Row 2: Targeting Mode (if not locked) - addLabel(new GuiNpcLabel(22, "ability.targeting", col1LabelX, y + 5)); - if (!targetingModeLocked) { - String[] targetingModes = getAvailableTargetingModes(); - int selectedIndex = getTargetingModeIndex(targetingMode); - addButton(new GuiNpcButton(4, col1FieldX, y, 90, 20, targetingModes, selectedIndex)); + if (tabIndex >= 4) { + // Custom tab — match by custom tab name + int customIndex = tabIndex - 4; + if (customIndex >= 0 && customIndex < customTabNames.size()) { + String tabName = customTabNames.get(customIndex); + for (FieldDef def : fieldDefs) { + if (def.getTab() == TabTarget.CUSTOM + && tabName.equals(def.getCustomTabName()) + && def.isVisible()) { + result.add(def); + } + } + } } else { - String modeName = "ability.target." + targetingMode.name().toLowerCase(); - addLabel(new GuiNpcLabel(23, modeName, col1FieldX, y + 5)); + // Fixed tab + TabTarget target; + switch (tabIndex) { + case TAB_GENERAL: target = TabTarget.GENERAL; break; + case TAB_TYPE: target = TabTarget.TYPE; break; + case TAB_TARGET: target = TabTarget.TARGET; break; + case TAB_EFFECTS: target = TabTarget.EFFECTS; break; + default: target = TabTarget.TYPE; break; + } + for (FieldDef def : fieldDefs) { + if (def.getTab() == target && def.isVisible()) { + result.add(def); + } + } } - y += 28; + return result; + } - // ───────────────────────────────────────────────────────────────────── - // CONDITIONS SECTION (up to 3) - // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(24, "ability.conditions", col1LabelX, y)); - y += 14; + // ═══════════════════════════════════════════════════════════════════════════ + // CONDITIONS (Target tab) + // ═══════════════════════════════════════════════════════════════════════════ + + private int renderConditions(GuiScrollWindow sw, int y, int labelCounter) { + y += 3; + sw.addLabel(new GuiNpcLabel(labelCounter, "ability.conditions", L_LABEL_X, y + 2, 0xFFFF55)); + y += 15; - // Display existing conditions (similar to Marks pattern) for (int i = 0; i < conditions.size() && i < 3; i++) { Condition cond = conditions.get(i); String condName = getConditionDisplayName(cond); - - // Condition type button - addButton(new GuiNpcButton(50 + i * 10, col1LabelX, y, 140, 20, condName)); - - // Edit button - addButton(new GuiNpcButton(51 + i * 10, col1LabelX + 145, y, 40, 20, "gui.edit")); - - // Delete button - addButton(new GuiNpcButton(52 + i * 10, col1LabelX + 190, y, 20, 20, "X")); - + sw.addButton(new GuiNpcButton(50 + i * 10, L_LABEL_X, y, 140, 20, condName)); + sw.addButton(new GuiNpcButton(51 + i * 10, L_LABEL_X + 145, y, 40, 20, "gui.edit")); + sw.addButton(new GuiNpcButton(52 + i * 10, L_LABEL_X + 190, y, 20, 20, "X")); y += 22; } - // Add button (if less than 3 conditions) if (conditions.size() < 3) { - addButton(new GuiNpcButton(80, col1LabelX, y, 50, 20, "gui.add")); + sw.addButton(new GuiNpcButton(80, L_LABEL_X, y, 50, 20, "gui.add")); + y += ROW_H; } + + return y; } private String getConditionDisplayName(Condition cond) { @@ -376,301 +235,165 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi String typeId = cond.getTypeId(); switch (typeId) { case "hp_above": - return StatCollector.translateToLocal("condition.hp_above") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.hp_above") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "hp_below": - return StatCollector.translateToLocal("condition.hp_below") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "target_hp_above": - return StatCollector.translateToLocal("condition.target_hp_above") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.target_hp_above") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "target_hp_below": - return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "hit_count": - Condition.ConditionHitCount hitCount = (Condition.ConditionHitCount) cond; - return StatCollector.translateToLocal("condition.hit_count") + ": " + hitCount.getRequiredHits() + "/" + hitCount.getWithinTicks() + "t"; + Condition.ConditionHitCount hc = (Condition.ConditionHitCount) cond; + return StatCollector.translateToLocal("condition.hit_count") + ": " + hc.getRequiredHits() + "/" + hc.getWithinTicks() + "t"; default: return typeId; } } private float getConditionThreshold(Condition cond) { - // Read threshold from NBT since inner classes don't expose it NBTTagCompound nbt = cond.writeNBT(); return nbt.hasKey("threshold") ? nbt.getFloat("threshold") : 0.5f; } - private String[] getAvailableTargetingModes() { - TargetingMode[] modes = ability.getAllowedTargetingModes(); - if (modes == null) { - modes = TargetingMode.values(); - } - String[] result = new String[modes.length]; - for (int i = 0; i < modes.length; i++) { - result[i] = "ability.target." + modes[i].name().toLowerCase(); - } - return result; - } - - private int getTargetingModeIndex(TargetingMode mode) { - TargetingMode[] modes = ability.getAllowedTargetingModes(); - if (modes == null) { - return mode.ordinal(); - } - for (int i = 0; i < modes.length; i++) { - if (modes[i] == mode) return i; - } - return 0; - } - - // ═══════════════════════════════════════════════════════════════════════════ - // EFFECTS TAB - // ═══════════════════════════════════════════════════════════════════════════ - - private void initEffectsTab(int startY) { - int y = startY; - int col1LabelX = guiLeft + 8; - int col1FieldX = guiLeft + 85; - int col1ClearX = guiLeft + 160; - int col2LabelX = guiLeft + 185; - int col2FieldX = guiLeft + 255; - - // ───────────────────────────────────────────────────────────────────── - // SOUNDS SECTION - // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(30, "advanced.sounds", col1LabelX, y)); - y += 14; - - // Wind Up Sound - addLabel(new GuiNpcLabel(31, "ability.windUpSound", col1LabelX, y + 5)); - String windUpSoundDisplay = windUpSound != null && !windUpSound.isEmpty() ? truncateString(windUpSound, 10) : StatCollector.translateToLocal("gui.none"); - addButton(new GuiNpcButton(30, col1FieldX, y, 70, 20, windUpSoundDisplay)); - addButton(new GuiNpcButton(35, col1ClearX, y, 20, 20, "X")); - - // Active Sound - addLabel(new GuiNpcLabel(32, "ability.activeSound", col2LabelX, y + 5)); - String activeSoundDisplay = activeSound != null && !activeSound.isEmpty() ? truncateString(activeSound, 10) : StatCollector.translateToLocal("gui.none"); - addButton(new GuiNpcButton(31, col2FieldX, y, 70, 20, activeSoundDisplay)); - addButton(new GuiNpcButton(36, col2FieldX + 75, y, 20, 20, "X")); - - y += 26; - - // ───────────────────────────────────────────────────────────────────── - // ANIMATIONS SECTION - // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(33, "menu.animations", col1LabelX, y)); - y += 14; - - // Wind Up Animation - addLabel(new GuiNpcLabel(34, "ability.windUpAnim", col1LabelX, y + 5)); - String windUpAnimName = getAnimationName(windUpAnimationId, true); - addButton(new GuiNpcButton(32, col1FieldX, y, 70, 20, windUpAnimName)); - addButton(new GuiNpcButton(37, col1ClearX, y, 20, 20, "X")); - - // Active Animation - addLabel(new GuiNpcLabel(35, "ability.activeAnim", col2LabelX, y + 5)); - String activeAnimName = getAnimationName(activeAnimationId, false); - addButton(new GuiNpcButton(33, col2FieldX, y, 70, 20, activeAnimName)); - addButton(new GuiNpcButton(38, col2FieldX + 75, y, 20, 20, "X")); - - y += 26; - - // ───────────────────────────────────────────────────────────────────── - // TELEGRAPH SECTION (only if supported) - // ───────────────────────────────────────────────────────────────────── - if (supportsTelegraph) { - addLabel(new GuiNpcLabel(40, "ability.telegraph", col1LabelX, y)); - y += 14; - - // Show Telegraph - addLabel(new GuiNpcLabel(41, "ability.showTelegraph", col1LabelX, y + 5)); - addButton(new GuiNpcButton(20, guiLeft + 95, y, 40, 20, new String[]{"gui.no", "gui.yes"}, showTelegraph ? 1 : 0)); - - // Telegraph Type (separate column) - String typeKey = ability.getTelegraphType().name().toLowerCase(); - addLabel(new GuiNpcLabel(42, "ability.telegraphType", guiLeft + 200, y + 5)); - String telegraphLangKey = typeKey.equals("none") ? "gui.none" : "telegraph." + typeKey; - addLabel(new GuiNpcLabel(43, telegraphLangKey, guiLeft + 280, y + 5)); - - y += 24; - - // Colors - addLabel(new GuiNpcLabel(44, "ability.windUpColor", col1LabelX, y + 5)); - String windUpHex = String.format("%06X", windUpColor & 0xFFFFFF); - GuiNpcButton windUpColorBtn = new GuiNpcButton(22, guiLeft + 95, y, 55, 20, windUpHex); - windUpColorBtn.setEnabled(showTelegraph); - windUpColorBtn.setTextColor(windUpColor & 0xFFFFFF); - addButton(windUpColorBtn); - - addLabel(new GuiNpcLabel(45, "ability.activeColor", guiLeft + 200, y + 5)); - String activeHex = String.format("%06X", activeColor & 0xFFFFFF); - GuiNpcButton activeColorBtn = new GuiNpcButton(24, guiLeft + 280, y, 55, 20, activeHex); - activeColorBtn.setEnabled(showTelegraph); - activeColorBtn.setTextColor(activeColor & 0xFFFFFF); - addButton(activeColorBtn); - } - } - - private String truncateString(String str, int maxLen) { - if (str == null) return ""; - if (str.length() <= maxLen) return str; - int lastSlash = str.lastIndexOf('/'); - if (lastSlash >= 0 && str.length() - lastSlash <= maxLen) { - return "..." + str.substring(lastSlash); - } - return "..." + str.substring(str.length() - maxLen + 3); - } - - private String getAnimationName(int animId, boolean isWindUp) { - if (animId < 0) return StatCollector.translateToLocal("gui.none"); - String cachedName = isWindUp ? windUpAnimationName : activeAnimationName; - if (cachedName != null && !cachedName.isEmpty()) { - return truncateString(cachedName, 8); - } - return "ID: " + animId; - } - // ═══════════════════════════════════════════════════════════════════════════ // BUTTON EVENTS // ═══════════════════════════════════════════════════════════════════════════ @Override + @SuppressWarnings({"unchecked", "rawtypes"}) public void buttonEvent(GuiButton guibutton) { int id = guibutton.id; - // Tab switching - if (id == 90) { - activeTab = TAB_GENERAL; - initGui(); - return; - } - if (id == 91 && hasTypeSettings) { - activeTab = TAB_TYPE; - initGui(); - return; - } - if (id == 92) { - activeTab = TAB_TARGET; - initGui(); - return; - } - if (id == 93) { - activeTab = TAB_EFFECTS; + // Tab switching — fixed tabs + if (id == 90) { activeTab = TAB_GENERAL; initGui(); return; } + if (id == 91) { activeTab = TAB_TYPE; initGui(); return; } + if (id == 92) { activeTab = TAB_TARGET; initGui(); return; } + if (id == 93) { activeTab = TAB_EFFECTS; initGui(); return; } + // Custom tabs (94+) + if (id >= 94 && id < 94 + customTabNames.size()) { + activeTab = 4 + (id - 94); initGui(); return; } + if (id == -1000) { close(); return; } - // Enabled button (bottom left) - if (id == 2) { - enabled = ((GuiNpcButton) guibutton).getValue() == 1; - initGui(); - } - - // General tab buttons - else if (id == 14) { - interruptible = ((GuiNpcButton) guibutton).getValue() == 1; - } - else if (id == 16) { - lockMovement = ((GuiNpcButton) guibutton).getValue() == 1; - } - - // Target tab - Targeting mode - else if (id == 4) { - TargetingMode[] modes = ability.getAllowedTargetingModes(); - int idx = ((GuiNpcButton) guibutton).getValue(); - if (modes != null && idx < modes.length) { - targetingMode = modes[idx]; - } else { - targetingMode = TargetingMode.values()[idx]; - } - } - - // Close button (X in top menu) - use -1000 to avoid any ID conflicts - else if (id == -1000) { - close(); - return; - } - - // Target tab - Condition buttons (50-79) - else if (id >= 50 && id < 80) { + // Condition buttons (50-80) + if (id >= 50 && id < 80) { int condIndex = (id - 50) / 10; int action = (id - 50) % 10; - if (action == 0 || action == 1) { - // Click on condition name or Edit button - open edit subgui if (condIndex < conditions.size()) { editingConditionIndex = condIndex; setSubGui(new SubGuiConditionEdit(conditions.get(condIndex))); } } else if (action == 2) { - // Delete button if (condIndex < conditions.size()) { conditions.remove(condIndex); initGui(); } } + return; } - // Add condition button - else if (id == 80) { + if (id == 80) { if (conditions.size() < 3) { editingConditionIndex = conditions.size(); setSubGui(new SubGuiConditionEdit(null)); } + return; } - // Effects tab - Telegraph - else if (id == 20) { - showTelegraph = ((GuiNpcButton) guibutton).getValue() == 1; - initGui(); - } - else if (id == 22) { - editingColorId = 22; - setSubGui(new SubGuiColorSelector(windUpColor)); - } - else if (id == 24) { - editingColorId = 24; - setSubGui(new SubGuiColorSelector(activeColor)); + // Clear buttons (effects delete or SUB_GUI clear) + if (id >= CLEAR_ID_START) { + // Check effects list delete first + int[] meta = builder.getEffectWidgetMeta().get(id); + if (meta != null && meta[1] == 3) { + FieldDef def = builder.getClearFieldMap().get(id); + if (def != null && def.getType() == FieldType.EFFECTS_LIST) { + handleEffectButton(def, meta, guibutton); + return; + } + } + // SUB_GUI clear + FieldDef def = builder.getClearFieldMap().get(id); + if (def != null && def.hasClearAction()) { + def.getClearAction().run(); + initGui(); + } + return; } - // Effects tab - Sound buttons - else if (id == 30) { - editingSoundId = 30; - setSubGui(new GuiSoundSelection(windUpSound)); - } - else if (id == 31) { - editingSoundId = 31; - setSubGui(new GuiSoundSelection(activeSound)); - } - // Sound clear buttons - else if (id == 35) { - windUpSound = ""; - initGui(); - } - else if (id == 36) { - activeSound = ""; - initGui(); - } - // Animation buttons - else if (id == 32) { - editingAnimationId = 32; - setSubGui(new GuiAnimationSelection(windUpAnimationId)); - } - else if (id == 33) { - editingAnimationId = 33; - setSubGui(new GuiAnimationSelection(activeAnimationId)); - } - // Animation clear buttons - else if (id == 37) { - windUpAnimationId = -1; - windUpAnimationName = null; - initGui(); - } - else if (id == 38) { - activeAnimationId = -1; - activeAnimationName = null; - initGui(); - } + // Declarative field buttons + if (id >= DECLARATIVE_ID_START) { + FieldDef def = builder.getButtonFieldMap().get(id); + if (def == null) return; - // Type-specific buttons (100+) - delegate to subclass - else if (id >= 100) { - handleTypeButton(id, (GuiNpcButton) guibutton); + // Effects list buttons (type, amp, add) + int[] meta = builder.getEffectWidgetMeta().get(id); + if (meta != null && def.getType() == FieldType.EFFECTS_LIST) { + handleEffectButton(def, meta, guibutton); + return; + } + + switch (def.getType()) { + case BOOLEAN: + def.setValue(((GuiNpcButton) guibutton).getValue() == 1); + initGui(); + break; + case ENUM: { + int idx = ((GuiNpcButton) guibutton).getValue(); + Class> ec = def.getEnumClass(); + if (ec != null) { + Enum[] constants = ec.getEnumConstants(); + if (idx >= 0 && idx < constants.length) def.setValue(constants[idx]); + } + initGui(); + break; + } + case STRING_ENUM: { + int idx = ((GuiNpcButton) guibutton).getValue(); + String[] values = def.getStringEnumValues(); + if (values != null && idx >= 0 && idx < values.length) def.setStringEnumValue(values[idx]); + initGui(); + break; + } + case SUB_GUI: + activeSubGuiField = def; + if (def.getSubGuiFactory() != null) setSubGui(def.getSubGuiFactory().get()); + break; + } + } + } + + @SuppressWarnings("unchecked") + private void handleEffectButton(FieldDef def, int[] meta, GuiButton btn) { + List effects = (List) def.getValue(); + if (effects == null) return; + int effectIdx = meta[0]; + int action = meta[1]; + + switch (action) { + case 0: // Type changed + if (effectIdx < effects.size()) { + effects.get(effectIdx).setType(AbilityEffect.EffectType.fromOrdinal(((GuiNpcButton) btn).getValue())); + } + break; + case 2: // Amp changed + if (effectIdx < effects.size()) { + effects.get(effectIdx).setAmplifier(((GuiNpcButton) btn).getValue()); + } + break; + case 3: // Delete + if (effectIdx < effects.size()) { + effects.remove(effectIdx); + initGui(); + } + break; + case 4: // Add + if (effects.size() < 5) { + effects.add(new AbilityEffect(AbilityEffect.EffectType.SLOWNESS, 60, 0)); + initGui(); + } + break; } } @@ -679,80 +402,57 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // ═══════════════════════════════════════════════════════════════════════════ @Override + @SuppressWarnings("unchecked") public void unFocused(GuiNpcTextField textField) { int id = textField.id; + if (id < DECLARATIVE_ID_START) return; - // General tab - if (id == 1) { - name = textField.getText(); - } else if (id == 3) { - weight = textField.getInteger(); + // Check effects list duration fields + int[] meta = builder.getEffectWidgetMeta().get(id); + if (meta != null && meta[1] == 1) { + FieldDef def = builder.getTextFieldMap().get(id); + if (def != null && def.getType() == FieldType.EFFECTS_LIST) { + List effects = (List) def.getValue(); + if (effects != null && meta[0] < effects.size()) { + effects.get(meta[0]).setDurationTicks(textField.getInteger()); + } + return; + } } - // Target tab - else if (id == 5) { - minRange = textField.getInteger(); - } else if (id == 6) { - maxRange = textField.getInteger(); - } + FieldDef def = builder.getTextFieldMap().get(id); + if (def == null) return; - // Timing (General tab) - else if (id == 10) { - cooldownTicks = textField.getInteger(); - } else if (id == 11) { - windUpTicks = textField.getInteger(); - } else if (id == 12) { - activeTicks = textField.getInteger(); - } else if (id == 13) { - recoveryTicks = textField.getInteger(); - } - - // Type-specific fields (100+) - delegate to subclass - else if (id >= 100) { - handleTypeTextField(id, textField); + switch (def.getType()) { + case FLOAT: + try { def.setValue(Float.parseFloat(textField.getText())); } catch (NumberFormatException ignored) {} + break; + case INT: + try { def.setValue(Integer.parseInt(textField.getText())); } catch (NumberFormatException ignored) {} + break; + case STRING: + def.setValue(textField.getText()); + break; } } // ═══════════════════════════════════════════════════════════════════════════ - // SUBGUI CLOSED HANDLER + // SUB GUI CLOSED // ═══════════════════════════════════════════════════════════════════════════ @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiColorSelector) { - SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; - int rgb = colorSelector.color & 0x00FFFFFF; - if (editingColorId == 22) { - windUpColor = 0x80000000 | rgb; - } else if (editingColorId == 24) { - activeColor = 0xC0000000 | rgb; + if (activeSubGuiField != null) { + FieldDef def = activeSubGuiField; + activeSubGuiField = null; + if (def.getSubGuiResultHandler() != null) { + def.getSubGuiResultHandler().accept(subgui); } - editingColorId = 0; initGui(); - } else if (subgui instanceof GuiSoundSelection) { - GuiSoundSelection soundSelector = (GuiSoundSelection) subgui; - if (soundSelector.selectedResource != null) { - String sound = soundSelector.selectedResource.toString(); - if (editingSoundId == 30) { - windUpSound = sound; - } else if (editingSoundId == 31) { - activeSound = sound; - } - } - editingSoundId = 0; - initGui(); - } else if (subgui instanceof GuiAnimationSelection) { - GuiAnimationSelection animSelector = (GuiAnimationSelection) subgui; - if (editingAnimationId == 32) { - windUpAnimationId = animSelector.selectedAnimationId; - windUpAnimationName = animSelector.getSelectedName(); - } else if (editingAnimationId == 33) { - activeAnimationId = animSelector.selectedAnimationId; - activeAnimationName = animSelector.getSelectedName(); - } - editingAnimationId = 0; - initGui(); - } else if (subgui instanceof SubGuiConditionEdit) { + return; + } + + if (subgui instanceof SubGuiConditionEdit) { SubGuiConditionEdit condEdit = (SubGuiConditionEdit) subgui; Condition result = condEdit.getResult(); if (result != null && editingConditionIndex >= 0) { @@ -768,135 +468,33 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } // ═══════════════════════════════════════════════════════════════════════════ - // CLOSE - Save before closing + // CLOSE / APPLY // ═══════════════════════════════════════════════════════════════════════════ @Override public void close() { - // Apply all cached values to the ability before closing applyToAbility(); - - // Notify callback that ability was saved callback.onAbilitySaved(ability); - - // Close the GUI super.close(); } - // ═══════════════════════════════════════════════════════════════════════════ - // APPLY TO ABILITY - // ═══════════════════════════════════════════════════════════════════════════ - protected void applyToAbility() { - // General - ability.setName(name); - ability.setEnabled(enabled); - ability.setWeight(weight); - ability.setLockMovement(lockMovement); - ability.setInterruptible(interruptible); - - // Timing - ability.setCooldownTicks(cooldownTicks); - ability.setWindUpTicks(windUpTicks); - ability.setActiveTicks(activeTicks); - ability.setRecoveryTicks(recoveryTicks); - - // Target - ability.setMinRange(minRange); - ability.setMaxRange(maxRange); - ability.setTargetingMode(targetingMode); - - // Conditions ability.getConditions().clear(); - for (Condition c : conditions) { - ability.addCondition(c); - } - - // Effects - ability.setWindUpSound(windUpSound); - ability.setActiveSound(activeSound); - ability.setWindUpAnimationId(windUpAnimationId); - ability.setActiveAnimationId(activeAnimationId); - - // Telegraph - ability.setShowTelegraph(showTelegraph); - ability.setWindUpColor(windUpColor); - ability.setActiveColor(activeColor); - - // Type-specific values are applied directly in handleTypeTextField and handleTypeButton + for (Condition c : conditions) ability.addCondition(c); } - /** - * Called when an ability is loaded from saved presets. - */ public void loadAbility(Ability loadedAbility) { if (loadedAbility == null) return; - - // Copy all values from loaded ability to current ability NBTTagCompound nbt = loadedAbility.writeNBT(); - // Keep the current typeId since we can't change types mid-edit nbt.setString("typeId", ability.getTypeId()); ability.readNBT(nbt); - // Update cached values - this.name = ability.getName() != null ? ability.getName() : ""; - this.enabled = ability.isEnabled(); - this.weight = ability.getWeight(); - this.lockMovement = ability.isLockMovement(); - this.interruptible = ability.isInterruptible(); - - this.cooldownTicks = ability.getCooldownTicks(); - this.windUpTicks = ability.getWindUpTicks(); - this.activeTicks = ability.getActiveTicks(); - this.recoveryTicks = ability.getRecoveryTicks(); - - this.minRange = ability.getMinRange(); - this.maxRange = ability.getMaxRange(); - this.targetingMode = ability.getTargetingMode(); this.conditions = new ArrayList<>(ability.getConditions()); - - this.showTelegraph = ability.isShowTelegraph(); - this.windUpColor = ability.getWindUpColor(); - this.activeColor = ability.getActiveColor(); - + this.fieldDefs = ability.getAllFieldDefinitions(); + discoverCustomTabs(); initGui(); } - // ═══════════════════════════════════════════════════════════════════════════ - // HELPER METHODS FOR SUBCLASSES - // ═══════════════════════════════════════════════════════════════════════════ - - /** - * Creates a float text field for type-specific settings. - */ - protected GuiNpcTextField createFloatField(int id, int x, int y, int width, float value) { - GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, String.format("%.1f", value)); - return field; - } - - /** - * Creates an integer text field for type-specific settings. - */ - protected GuiNpcTextField createIntField(int id, int x, int y, int width, int value) { - GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, String.valueOf(value)); - field.setIntegersOnly(); - return field; - } - - /** - * Parses a float from a text field with a fallback default value. - */ - protected float parseFloat(GuiNpcTextField field, float defaultValue) { - try { - return Float.parseFloat(field.getText()); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - /** - * Gets the ability being edited. - */ protected Ability getAbility() { return ability; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java index ee2561b4e..c0517257b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.advanced; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -8,15 +9,20 @@ import noppes.npcs.client.gui.util.SubGuiInterface; /** * Confirmation dialog for deleting an ability preset. + * Uses packet-based communication for proper client-server architecture. */ public class SubGuiAbilityDeleteConfirm extends SubGuiInterface { private final String abilityName; - private final SubGuiAbilityLoad loadGui; + private final Runnable onDeleted; - public SubGuiAbilityDeleteConfirm(String abilityName, SubGuiAbilityLoad loadGui) { + public SubGuiAbilityDeleteConfirm(String abilityName) { + this(abilityName, null); + } + + public SubGuiAbilityDeleteConfirm(String abilityName, Runnable onDeleted) { this.abilityName = abilityName; - this.loadGui = loadGui; + this.onDeleted = onDeleted; setBackground("menubg.png"); xSize = 200; @@ -44,9 +50,11 @@ public class SubGuiAbilityDeleteConfirm extends SubGuiInterface { int id = guibutton.id; if (id == 0) { - // Delete the ability - AbilityController.Instance.deleteAbility(abilityName); - loadGui.onAbilityDeleted(abilityName); + // Delete the ability via packet + PacketClient.sendClient(new CustomAbilityRemovePacket(abilityName)); + if (onDeleted != null) { + onDeleted.run(); + } close(); } else if (id == 1) { close(); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java new file mode 100644 index 000000000..4d21f12ca --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java @@ -0,0 +1,56 @@ +package noppes.npcs.client.gui.advanced; + +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; + +/** + * Dialog asking the user whether to clone-and-modify (convert to inline) or modify the parent ability. + */ +public class SubGuiAbilityEditMode extends SubGuiInterface { + + public static final int MODE_CLONE_MODIFY = 0; + public static final int MODE_MODIFY_PARENT = 1; + + private int result = -1; + + public SubGuiAbilityEditMode() { + setBackground("menubg.png"); + xSize = 220; + ySize = 90; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + addLabel(new GuiNpcLabel(0, "ability.edit.mode", guiLeft + 10, y)); + + y += 25; + addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "ability.cloneModify")); + addButton(new GuiNpcButton(1, guiLeft + 115, y, 95, 20, "ability.modifyParent")); + + y += 25; + addButton(new GuiNpcButton(2, guiLeft + 65, y, 90, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + result = MODE_CLONE_MODIFY; + close(); + } else if (guibutton.id == 1) { + result = MODE_MODIFY_PARENT; + close(); + } else if (guibutton.id == 2) { + result = -1; + close(); + } + } + + public int getResult() { + return result; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java index 069c9f1b4..68a74cb8b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java @@ -2,24 +2,45 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; import net.minecraft.client.gui.GuiButton; -import noppes.npcs.client.gui.util.*; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.IScrollData; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.constants.EnumScrollData; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.UUID; +import java.util.Vector; /** * Dialog for loading a saved ability preset. - * Can be opened from SubGuiAbilityConfig (to load into editing ability) - * or from GuiNPCAbilities (to add ability directly to NPC). + * Shows custom abilities from the server, then asks Clone/Reference via SubGuiAbilityLoadMode. */ -public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener { +public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener, IScrollData, IGuiData, ISubGuiListener { private final SubGuiAbilityConfig parentConfig; private final GuiNPCAbilities parentAbilities; - private String selectedName = null; - private List abilityNames = new ArrayList<>(); + private String selectedDisplayName = null; + private String selectedUuid = null; + + /** Maps display name -> UUID for custom abilities. */ + private final HashMap displayToUuid = new HashMap<>(); + private HashMap rawData = new HashMap<>(); + private GuiCustomScroll scroll; + + private boolean waitingForLoad = false; + private int pendingLoadMode = -1; public SubGuiAbilityLoad(SubGuiAbilityConfig parentConfig) { this.parentConfig = parentConfig; @@ -28,6 +49,8 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL setBackground("menubg.png"); xSize = 220; ySize = 180; + + PacketClient.sendClient(new CustomAbilitiesGetPacket()); } public SubGuiAbilityLoad(GuiNPCAbilities parentAbilities) { @@ -37,71 +60,90 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL setBackground("menubg.png"); xSize = 220; ySize = 180; + + PacketClient.sendClient(new CustomAbilitiesGetPacket()); } @Override public void initGui() { super.initGui(); - // Load ability names - Set names = AbilityController.Instance.getSavedAbilityNames(); - abilityNames = new ArrayList<>(names); - int y = guiTop + 5; addLabel(new GuiNpcLabel(0, "ability.load.select", guiLeft + 10, y)); y += 14; - // Scroll list of saved abilities - GuiCustomScroll scroll = new GuiCustomScroll(this, 0); - scroll.setSize(200, 110); + if (scroll == null) { + scroll = new GuiCustomScroll(this, 0); + scroll.setSize(200, 110); + } scroll.guiLeft = guiLeft + 10; scroll.guiTop = y; - scroll.setUnsortedList(abilityNames); - if (selectedName != null && abilityNames.contains(selectedName)) { - scroll.setSelected(selectedName); + scroll.setList(getFilteredList()); + if (selectedDisplayName != null) { + scroll.setSelected(selectedDisplayName); } addScroll(scroll); y += 115; - // Buttons - addButton(new GuiNpcButton(0, guiLeft + 10, y, 60, 20, "gui.load")); - addButton(new GuiNpcButton(1, guiLeft + 80, y, 60, 20, "gui.delete")); - addButton(new GuiNpcButton(2, guiLeft + 150, y, 60, 20, "gui.cancel")); + addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.load")); + addButton(new GuiNpcButton(2, guiLeft + 115, y, 95, 20, "gui.cancel")); - // Disable load/delete if nothing selected - getButton(0).setEnabled(selectedName != null && !selectedName.isEmpty()); - getButton(1).setEnabled(selectedName != null && !selectedName.isEmpty()); + getButton(0).setEnabled(selectedUuid != null); + } + + private List getFilteredList() { + return new ArrayList<>(displayToUuid.keySet()); } @Override public void buttonEvent(GuiButton guibutton) { int id = guibutton.id; - if (id == 0 && selectedName != null) { - // Load the selected ability - Ability loaded = AbilityController.Instance.getSavedAbility(selectedName); - if (loaded != null) { - if (parentConfig != null) { - parentConfig.loadAbility(loaded); - } else if (parentAbilities != null) { - parentAbilities.loadAbility(loaded); - } + if (id == 0 && selectedUuid != null) { + if (parentAbilities != null) { + // From NPC abilities GUI - ask clone/reference + setSubGui(new SubGuiAbilityLoadMode()); + } else { + // From config GUI - always clone + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); } - close(); - } else if (id == 1 && selectedName != null) { - // Delete confirmation - setSubGui(new SubGuiAbilityDeleteConfirm(selectedName, this)); } else if (id == 2) { close(); } } + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiAbilityLoadMode) { + int mode = ((SubGuiAbilityLoadMode) subgui).getResult(); + if (mode < 0) return; // cancelled + + if (mode == SubGuiAbilityLoadMode.MODE_REFERENCE) { + // Reference mode - just pass the reference ID + if (parentAbilities != null && selectedUuid != null) { + parentAbilities.loadAbilityReference(selectedUuid); + } + close(); + } else { + // Clone mode - request full data + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } + } + } + @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { if (scroll.id == 0) { - selectedName = scroll.getSelected(); + selectedDisplayName = scroll.getSelected(); + if (selectedDisplayName != null) { + selectedUuid = displayToUuid.get(selectedDisplayName); + } initGui(); } } @@ -109,9 +151,57 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { if (scroll.id == 0 && selection != null && !selection.isEmpty()) { - // Double-click to load immediately - Ability loaded = AbilityController.Instance.getSavedAbility(selection); + selectedDisplayName = selection; + selectedUuid = displayToUuid.get(selectedDisplayName); + if (selectedUuid != null) { + if (parentAbilities != null) { + setSubGui(new SubGuiAbilityLoadMode()); + } else { + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } + } + } + } + + @Override + public void setData(Vector list, HashMap data, EnumScrollData type) { + if (type == EnumScrollData.CUSTOM_ABILITIES) { + displayToUuid.clear(); + rawData = data; + for (String key : data.keySet()) { + // key format: "displayName\tUUID" + int tabIndex = key.indexOf('\t'); + if (tabIndex > 0) { + String displayName = key.substring(0, tabIndex); + String uuid = key.substring(tabIndex + 1); + displayToUuid.put(displayName, uuid); + } else { + displayToUuid.put(key, key); + } + } + if (scroll != null) { + scroll.setList(getFilteredList()); + if (selectedDisplayName != null && displayToUuid.containsKey(selectedDisplayName)) { + scroll.setSelected(selectedDisplayName); + } + } + initGui(); + } + } + + @Override + public void setGuiData(NBTTagCompound compound) { + if (waitingForLoad) { + waitingForLoad = false; + Ability loaded = AbilityController.Instance.fromNBT(compound); if (loaded != null) { + if (pendingLoadMode == SubGuiAbilityLoadMode.MODE_CLONE) { + // Give it a new UUID so it's a unique inline copy + loaded.setId(UUID.randomUUID().toString()); + } + if (parentConfig != null) { parentConfig.loadAbility(loaded); } else if (parentAbilities != null) { @@ -122,13 +212,11 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL } } - /** - * Called after an ability is deleted to refresh the list. - */ - public void onAbilityDeleted(String name) { - if (name.equals(selectedName)) { - selectedName = null; + @Override + public void setSelected(String selected) { + this.selectedDisplayName = selected; + if (scroll != null) { + scroll.setSelected(selected); } - initGui(); } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java new file mode 100644 index 000000000..28a7bd0bb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java @@ -0,0 +1,56 @@ +package noppes.npcs.client.gui.advanced; + +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; + +/** + * Dialog asking the user whether to Clone (inline copy) or Reference the loaded ability. + */ +public class SubGuiAbilityLoadMode extends SubGuiInterface { + + public static final int MODE_CLONE = 0; + public static final int MODE_REFERENCE = 1; + + private int result = -1; + + public SubGuiAbilityLoadMode() { + setBackground("menubg.png"); + xSize = 200; + ySize = 90; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + addLabel(new GuiNpcLabel(0, "ability.load.mode", guiLeft + 10, y)); + + y += 25; + addButton(new GuiNpcButton(0, guiLeft + 15, y, 80, 20, "ability.clone")); + addButton(new GuiNpcButton(1, guiLeft + 105, y, 80, 20, "ability.reference")); + + y += 25; + addButton(new GuiNpcButton(2, guiLeft + 55, y, 90, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + result = MODE_CLONE; + close(); + } else if (guibutton.id == 1) { + result = MODE_REFERENCE; + close(); + } else if (guibutton.id == 2) { + result = -1; + close(); + } + } + + public int getResult() { + return result; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java index 3be13f0be..9407d20c5 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java @@ -1,23 +1,30 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; 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.IAbilityConfigCallback; import noppes.npcs.client.gui.util.SubGuiInterface; /** * Confirmation dialog for saving an ability preset. + * Uses packet-based communication for proper client-server architecture. */ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { private final Ability ability; - private final boolean exists; + private final IAbilityConfigCallback callback; public SubGuiAbilitySaveConfirm(Ability ability) { + this(ability, null); + } + + public SubGuiAbilitySaveConfirm(Ability ability, IAbilityConfigCallback callback) { this.ability = ability; - this.exists = AbilityController.Instance.hasSavedAbility(ability.getName()); + this.callback = callback; setBackground("menubg.png"); xSize = 200; @@ -30,15 +37,9 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { int y = guiTop + 10; - if (exists) { - addLabel(new GuiNpcLabel(0, "ability.save.overwrite", guiLeft + 10, y)); - y += 12; - addLabel(new GuiNpcLabel(1, "'" + ability.getName() + "'?", guiLeft + 10, y)); - } else { - addLabel(new GuiNpcLabel(0, "ability.save.confirm", guiLeft + 10, y)); - y += 12; - addLabel(new GuiNpcLabel(1, "'" + ability.getName() + "'?", guiLeft + 10, y)); - } + addLabel(new GuiNpcLabel(0, "ability.save.confirm", guiLeft + 10, y)); + y += 12; + addLabel(new GuiNpcLabel(1, "'" + ability.getName() + "'?", guiLeft + 10, y)); y += 30; @@ -51,8 +52,11 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { int id = guibutton.id; if (id == 0) { - // Save the ability - AbilityController.Instance.saveAbility(ability); + // Save the ability via packet + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); + if (callback != null) { + callback.onAbilitySaved(ability); + } close(); } else if (id == 1) { close(); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java index f0d4f2734..d941f19ec 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java @@ -2,7 +2,11 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Condition; import net.minecraft.client.gui.GuiButton; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; /** * SubGui for editing a single ability condition. @@ -84,7 +88,9 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi // Condition Type selector addLabel(new GuiNpcLabel(1, "gui.type", labelX, y + 5)); - addButton(new GuiNpcButton(1, fieldX, y, 115, 20, CONDITION_TYPES, selectedTypeIndex)); + GuiNpcButton typeBtn = new GuiNpcButton(1, fieldX, y, 115, 20, CONDITION_TYPES, selectedTypeIndex); + typeBtn.setHoverText("condition.hover.type"); + addButton(typeBtn); y += 26; // Type-specific fields @@ -92,24 +98,27 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi if (typeId.equals("hit_count")) { // Hit count fields addLabel(new GuiNpcLabel(2, "condition.hits", labelX, y + 5)); - GuiNpcTextField hitsField = new GuiNpcTextField(2, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(requiredHits)); + GuiNpcTextField hitsField = new GuiNpcTextField(2, this, fontRendererObj, fieldX, y, 40, 20, "" + requiredHits); hitsField.setIntegersOnly(); hitsField.setMinMaxDefault(1, 100, 3); + hitsField.setHoverText("condition.hover.hits"); addTextField(hitsField); y += 24; addLabel(new GuiNpcLabel(3, "condition.within", labelX, y + 5)); - GuiNpcTextField ticksField = new GuiNpcTextField(3, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(withinTicks)); + GuiNpcTextField ticksField = new GuiNpcTextField(3, this, fontRendererObj, fieldX, y, 40, 20, "" + withinTicks); ticksField.setIntegersOnly(); ticksField.setMinMaxDefault(1, 1200, 60); + ticksField.setHoverText("condition.hover.within"); addTextField(ticksField); } else { // HP threshold field (as percentage) addLabel(new GuiNpcLabel(2, "condition.percent", labelX, y + 5)); - int percent = (int)(threshold * 100); - GuiNpcTextField thresholdField = new GuiNpcTextField(4, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(percent)); + int percent = (int) (threshold * 100); + GuiNpcTextField thresholdField = new GuiNpcTextField(4, this, fontRendererObj, fieldX, y, 40, 20, "" + percent); thresholdField.setIntegersOnly(); thresholdField.setMinMaxDefault(1, 99, 50); + thresholdField.setHoverText("condition.hover.threshold"); addTextField(thresholdField); addLabel(new GuiNpcLabel(4, "%", fieldX + 45, y + 5)); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java deleted file mode 100644 index fb3e6b074..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java +++ /dev/null @@ -1,92 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityBeam; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Beam ability type-specific settings. - */ -public class SubGuiAbilityBeam extends SubGuiAbilityConfig { - - private final AbilityBeam beam; - - public SubGuiAbilityBeam(AbilityBeam ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.beam = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Damage Interval - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, beam.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.dmgInterval", col2LabelX, y + 5)); - addTextField(createIntField(101, col2FieldX, y, 50, beam.getDamageInterval())); - - y += 24; - - // Row 2: Beam Length + Beam Width - addLabel(new GuiNpcLabel(102, "ability.beamLength", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, beam.getBeamLength())); - - addLabel(new GuiNpcLabel(103, "ability.beamWidth", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, beam.getBeamWidth())); - - y += 24; - - // Row 3: Sweep Angle + Sweep Speed - addLabel(new GuiNpcLabel(104, "ability.sweepAngle", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, beam.getSweepAngle())); - - addLabel(new GuiNpcLabel(105, "ability.sweepSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, beam.getSweepSpeed())); - - y += 24; - - // Row 4: Piercing + Lock On Target - addLabel(new GuiNpcLabel(106, "ability.piercing", labelX, y + 5)); - addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isPiercing() ? 1 : 0)); - - addLabel(new GuiNpcLabel(107, "ability.lockTarget", col2LabelX, y + 5)); - addButton(new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isLockOnTarget() ? 1 : 0)); - - y += 24; - - // Row 5: Sweep Back & Forth - addLabel(new GuiNpcLabel(108, "ability.sweepBackForth", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isSweepBackAndForth() ? 1 : 0)); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 106: beam.setPiercing(value == 1); break; - case 107: beam.setLockOnTarget(value == 1); break; - case 108: beam.setSweepBackAndForth(value == 1); break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: beam.setDamage(parseFloat(field, beam.getDamage())); break; - case 101: beam.setDamageInterval(field.getInteger()); break; - case 102: beam.setBeamLength(parseFloat(field, beam.getBeamLength())); break; - case 103: beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); break; - case 104: beam.setSweepAngle(parseFloat(field, beam.getSweepAngle())); break; - case 105: beam.setSweepSpeed(parseFloat(field, beam.getSweepSpeed())); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java deleted file mode 100644 index d3ff5fd81..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java +++ /dev/null @@ -1,59 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Charge ability type-specific settings. - */ -public class SubGuiAbilityCharge extends SubGuiAbilityConfig { - - private final AbilityCharge charge; - - public SubGuiAbilityCharge(AbilityCharge ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.charge = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, charge.getDamage())); - - addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, charge.getChargeSpeed())); - - y += 24; - - // Row 2: Knockback - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, charge.getKnockback())); - - y += 24; - - // Row 3: Hit Width - addLabel(new GuiNpcLabel(104, "ability.hitWidth", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, charge.getHitWidth())); - - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: charge.setDamage(parseFloat(field, charge.getDamage())); break; - case 101: charge.setChargeSpeed(parseFloat(field, charge.getChargeSpeed())); break; - case 102: charge.setKnockback(parseFloat(field, charge.getKnockback())); break; - case 104: charge.setHitWidth(parseFloat(field, charge.getHitWidth())); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java deleted file mode 100644 index d917282f2..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ /dev/null @@ -1,104 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Cutter ability type-specific settings. - */ -public class SubGuiAbilityCutter extends SubGuiAbilityConfig { - - private final AbilityCutter cutter; - - public SubGuiAbilityCutter(AbilityCutter ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.cutter = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Range - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, cutter.getDamage())); - - addLabel(new GuiNpcLabel(101, "gui.range", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, cutter.getRange())); - - y += 24; - - // Row 2: Arc Angle + Inner Radius - addLabel(new GuiNpcLabel(102, "ability.arcAngle", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, cutter.getArcAngle())); - - addLabel(new GuiNpcLabel(103, "ability.innerRadius", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, cutter.getInnerRadius())); - - y += 24; - - // Row 3: Knockback + Sweep Speed - addLabel(new GuiNpcLabel(104, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, cutter.getKnockback())); - - addLabel(new GuiNpcLabel(105, "ability.sweepSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, cutter.getSweepSpeed())); - - y += 24; - - // Row 4: Sweep Mode + Piercing - addLabel(new GuiNpcLabel(106, "ability.sweepMode", labelX, y + 5)); - String[] sweepModes = {"Swipe", "Spin"}; - addButton(new GuiNpcButton(106, fieldX, y, 70, 20, sweepModes, cutter.getSweepMode().ordinal())); - - addLabel(new GuiNpcLabel(107, "ability.piercing", col2LabelX, y + 5)); - addButton(new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, cutter.isPiercing() ? 1 : 0)); - - y += 24; - - // Row 5: Stun Duration + Poison Time - addLabel(new GuiNpcLabel(108, "ability.stunDuration", labelX, y + 5)); - addTextField(createIntField(108, fieldX, y, 50, cutter.getStunDuration())); - - addLabel(new GuiNpcLabel(109, "ability.poisonTime", col2LabelX, y + 5)); - addTextField(createIntField(109, col2FieldX, y, 50, cutter.getPoisonDurationSeconds())); - - y += 24; - - // Row 6: Poison Level - addLabel(new GuiNpcLabel(110, "ability.poisonLvl", labelX, y + 5)); - addTextField(createIntField(110, fieldX, y, 50, cutter.getPoisonLevel())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 106: cutter.setSweepMode(AbilityCutter.SweepMode.values()[value]); break; - case 107: cutter.setPiercing(value == 1); break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: cutter.setDamage(parseFloat(field, cutter.getDamage())); break; - case 101: cutter.setRange(parseFloat(field, cutter.getRange())); break; - case 102: cutter.setArcAngle(parseFloat(field, cutter.getArcAngle())); break; - case 103: cutter.setInnerRadius(parseFloat(field, cutter.getInnerRadius())); break; - case 104: cutter.setKnockback(parseFloat(field, cutter.getKnockback())); break; - case 105: cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed())); break; - case 108: cutter.setStunDuration(field.getInteger()); break; - case 109: cutter.setPoisonDurationSeconds(field.getInteger()); break; - case 110: cutter.setPoisonLevel(field.getInteger()); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java deleted file mode 100644 index 7ae0ea1f5..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java +++ /dev/null @@ -1,59 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityDash; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Dash ability type-specific settings. - */ -public class SubGuiAbilityDash extends SubGuiAbilityConfig { - - private final AbilityDash dash; - - public SubGuiAbilityDash(AbilityDash ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.dash = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Dash Mode - addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5)); - String[] dashModes = {"Aggressive", "Defensive"}; - addButton(new GuiNpcButton(100, fieldX, y, 80, 20, dashModes, dash.getDashMode().ordinal())); - - y += 24; - - // Row 2: Dash Distance + Dash Speed - addLabel(new GuiNpcLabel(101, "ability.dashDist", labelX, y + 5)); - addTextField(createFloatField(101, fieldX, y, 50, dash.getDashDistance())); - - addLabel(new GuiNpcLabel(102, "ability.dashSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(102, col2FieldX, y, 50, dash.getDashSpeed())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - if (id == 100) { - dash.setDashMode(AbilityDash.DashMode.values()[button.getValue()]); - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 101: dash.setDashDistance(parseFloat(field, dash.getDashDistance())); break; - case 102: dash.setDashSpeed(parseFloat(field, dash.getDashSpeed())); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java deleted file mode 100644 index 4858445fb..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java +++ /dev/null @@ -1,96 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Guard ability type-specific settings. - */ -public class SubGuiAbilityGuard extends SubGuiAbilityConfig { - - private final AbilityGuard guard; - - public SubGuiAbilityGuard(AbilityGuard ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.guard = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 105; - int col2LabelX = guiLeft + 165; - int col2FieldX = guiLeft + 225; - - // Row 1: Damage Reduction - addLabel(new GuiNpcLabel(100, "ability.dmgReduce", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, guard.getDamageReduction())); - - y += 24; - - // Row 2: Can Counter + Counter Type - addLabel(new GuiNpcLabel(101, "ability.canCounter", labelX, y + 5)); - addButton(new GuiNpcButton(101, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, guard.isCanCounter() ? 1 : 0)); - - addLabel(new GuiNpcLabel(102, "ability.counterType", col2LabelX, y + 5)); - String[] types = {"Flat", "Percent"}; - GuiNpcButton counterTypeButton = new GuiNpcButton(102, col2FieldX, y, 60, 20, types, guard.getCounterType().ordinal()); - counterTypeButton.setEnabled(guard.isCanCounter()); - addButton(counterTypeButton); - - y += 24; - - // Row 3: Counter Value + Counter Chance - addLabel(new GuiNpcLabel(103, "ability.counterValue", labelX, y + 5)); - GuiNpcTextField counterValueField = createFloatField(103, fieldX, y, 50, guard.getCounterValue()); - counterValueField.setEnabled(guard.isCanCounter()); - addTextField(counterValueField); - - addLabel(new GuiNpcLabel(104, "ability.counterChance", col2LabelX, y + 5)); - GuiNpcTextField counterChanceField = createFloatField(104, col2FieldX, y, 50, guard.getCounterChance()); - counterChanceField.setEnabled(guard.isCanCounter()); - addTextField(counterChanceField); - - y += 24; - - // Row 4: Counter Sound + Counter Animation - addLabel(new GuiNpcLabel(105, "ability.counterSound", labelX, y + 5)); - GuiNpcTextField counterSoundField = new GuiNpcTextField(105, this, fontRendererObj, fieldX, y, 100, 20, guard.getCounterSound()); - counterSoundField.setEnabled(guard.isCanCounter()); - addTextField(counterSoundField); - - addLabel(new GuiNpcLabel(106, "ability.counterAnim", col2LabelX, y + 5)); - GuiNpcTextField counterAnimField = createIntField(106, col2FieldX, y, 50, guard.getCounterAnimationId()); - counterAnimField.setEnabled(guard.isCanCounter()); - addTextField(counterAnimField); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - switch (id) { - case 101: - guard.setCanCounter(button.getValue() == 1); - initGui(); - break; - case 102: - guard.setCounterType(AbilityGuard.CounterType.values()[button.getValue()]); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: guard.setDamageReduction(parseFloat(field, guard.getDamageReduction())); break; - case 103: guard.setCounterValue(parseFloat(field, guard.getCounterValue())); break; - case 104: guard.setCounterChance(parseFloat(field, guard.getCounterChance())); break; - case 105: guard.setCounterSound(field.getText()); break; - case 106: guard.setCounterAnimationId(field.getInteger()); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java deleted file mode 100644 index def67e71d..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ /dev/null @@ -1,98 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Hazard ability type-specific settings. - */ -public class SubGuiAbilityHazard extends SubGuiAbilityConfig { - - private final AbilityHazard hazard; - - public SubGuiAbilityHazard(AbilityHazard ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.hazard = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Shape + Placement - addLabel(new GuiNpcLabel(100, "ability.shape", labelX, y + 5)); - String[] shapes = {"Circle", "Ring", "Cone"}; - addButton(new GuiNpcButton(100, fieldX, y, 55, 20, shapes, hazard.getShape().ordinal())); - - addLabel(new GuiNpcLabel(101, "ability.placement", col2LabelX, y + 5)); - String[] placements = {"Caster", "Target", "FollowCast", "FollowTgt"}; - addButton(new GuiNpcButton(101, col2FieldX, y, 60, 20, placements, hazard.getPlacement().ordinal())); - - y += 24; - - // Row 2: Radius + Inner Radius - addLabel(new GuiNpcLabel(102, "ability.radius", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, hazard.getRadius())); - - addLabel(new GuiNpcLabel(103, "ability.innerRadius", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, hazard.getInnerRadius())); - - y += 24; - - // Row 3: Damage Per Tick + Damage Interval - addLabel(new GuiNpcLabel(104, "ability.dmgPerTick", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, hazard.getDamagePerTick())); - - addLabel(new GuiNpcLabel(105, "ability.dmgInterval", col2LabelX, y + 5)); - addTextField(createIntField(105, col2FieldX, y, 50, hazard.getDamageInterval())); - - y += 24; - - // Row 4: Slowness Level + Debuff Duration - addLabel(new GuiNpcLabel(106, "ability.slownessLvl", labelX, y + 5)); - addTextField(createIntField(106, fieldX, y, 50, hazard.getSlownessLevel())); - - addLabel(new GuiNpcLabel(107, "ability.debuffDur", col2LabelX, y + 5)); - addTextField(createIntField(107, col2FieldX, y, 50, hazard.getDebuffDuration())); - - y += 24; - - // Row 5: Poison Level + Affects Caster - addLabel(new GuiNpcLabel(108, "ability.poisonLvl", labelX, y + 5)); - addTextField(createIntField(108, fieldX, y, 50, hazard.getPoisonLevel())); - - addLabel(new GuiNpcLabel(109, "ability.affectsCaster", col2LabelX, y + 5)); - addButton(new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, hazard.isAffectsCaster() ? 1 : 0)); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 100: hazard.setShape(AbilityHazard.HazardShape.values()[value]); break; - case 101: hazard.setPlacement(AbilityHazard.PlacementMode.values()[value]); break; - case 109: hazard.setAffectsCaster(value == 1); break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 102: hazard.setRadius(parseFloat(field, hazard.getRadius())); break; - case 103: hazard.setInnerRadius(parseFloat(field, hazard.getInnerRadius())); break; - case 104: hazard.setDamagePerTick(parseFloat(field, hazard.getDamagePerTick())); break; - case 105: hazard.setDamageInterval(field.getInteger()); break; - case 106: hazard.setSlownessLevel(field.getInteger()); break; - case 107: hazard.setDebuffDuration(field.getInteger()); break; - case 108: hazard.setPoisonLevel(field.getInteger()); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java deleted file mode 100644 index 30ccb739e..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java +++ /dev/null @@ -1,74 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityHeal; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Heal ability type-specific settings. - */ -public class SubGuiAbilityHeal extends SubGuiAbilityConfig { - - private final AbilityHeal heal; - - public SubGuiAbilityHeal(AbilityHeal ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.heal = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Heal Amount + Heal Percent - addLabel(new GuiNpcLabel(100, "ability.healAmount", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, heal.getHealAmount())); - - addLabel(new GuiNpcLabel(101, "ability.healPercent", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, heal.getHealPercent())); - - y += 24; - - // Row 2: Heal Radius + Instant Heal - addLabel(new GuiNpcLabel(102, "ability.healRadius", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, heal.getHealRadius())); - - addLabel(new GuiNpcLabel(103, "dialog.instant", col2LabelX, y + 5)); - addButton(new GuiNpcButton(103, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0)); - - y += 24; - - // Row 3: Heal Self + Heal Allies - addLabel(new GuiNpcLabel(104, "ability.healSelf", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealSelf() ? 1 : 0)); - - addLabel(new GuiNpcLabel(105, "ability.healAllies", col2LabelX, y + 5)); - addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealAllies() ? 1 : 0)); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 103: heal.setInstantHeal(value == 1); break; - case 104: heal.setHealSelf(value == 1); break; - case 105: heal.setHealAllies(value == 1); break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: heal.setHealAmount(parseFloat(field, heal.getHealAmount())); break; - case 101: heal.setHealPercent(parseFloat(field, heal.getHealPercent())); break; - case 102: heal.setHealRadius(parseFloat(field, heal.getHealRadius())); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java deleted file mode 100644 index 5c0d99c48..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java +++ /dev/null @@ -1,62 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Heavy Hit ability type-specific settings. - */ -public class SubGuiAbilityHeavyHit extends SubGuiAbilityConfig { - - private final AbilityHeavyHit heavyHit; - - public SubGuiAbilityHeavyHit(AbilityHeavyHit ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.heavyHit = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Potion Time - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, heavyHit.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.potionTime", col2LabelX, y + 5)); - addTextField(createIntField(101, col2FieldX, y, 50, heavyHit.getPotionDurationSeconds())); - - y += 24; - - // Row 2: Knockback + Slowness Level - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, heavyHit.getKnockback())); - - addLabel(new GuiNpcLabel(103, "ability.slownessLvl", col2LabelX, y + 5)); - addTextField(createIntField(103, col2FieldX, y, 50, heavyHit.getSlownessLevel())); - - y += 24; - - // Row 3: Weakness Level - addLabel(new GuiNpcLabel(104, "ability.weaknessLvl", labelX, y + 5)); - addTextField(createIntField(104, fieldX, y, 50, heavyHit.getWeaknessLevel())); - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: heavyHit.setDamage(parseFloat(field, heavyHit.getDamage())); break; - case 101: heavyHit.setPotionDurationSeconds(field.getInteger()); break; - case 102: heavyHit.setKnockback(parseFloat(field, heavyHit.getKnockback())); break; - case 103: heavyHit.setSlownessLevel(field.getInteger()); break; - case 104: heavyHit.setWeaknessLevel(field.getInteger()); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java deleted file mode 100644 index fd75700c0..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ /dev/null @@ -1,96 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Orb ability type-specific settings. - */ -public class SubGuiAbilityOrb extends SubGuiAbilityConfig { - - private final AbilityOrb orb; - - public SubGuiAbilityOrb(AbilityOrb ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.orb = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, orb.getDamage())); - - addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, orb.getOrbSpeed())); - - y += 24; - - // Row 2: Orb Size + Max Distance - addLabel(new GuiNpcLabel(102, "stats.size", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, orb.getOrbSize())); - - addLabel(new GuiNpcLabel(103, "ability.maxDist", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, orb.getMaxDistance())); - - y += 24; - - // Row 3: Homing + Homing Strength - addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isHoming() ? 1 : 0)); - - addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, orb.getHomingStrength())); - - y += 24; - - // Row 4: Explosive + Explosion Radius - addLabel(new GuiNpcLabel(106, "ability.explosive", labelX, y + 5)); - addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isExplosive() ? 1 : 0)); - - addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, orb.getExplosionRadius())); - - y += 24; - - // Row 5: Knockback + Max Lifetime - addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(108, fieldX, y, 50, orb.getKnockback())); - - addLabel(new GuiNpcLabel(109, "ability.lifetime", col2LabelX, y + 5)); - addTextField(createIntField(109, col2FieldX, y, 50, orb.getMaxLifetime())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 104: orb.setHoming(value == 1); break; - case 106: orb.setExplosive(value == 1); break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: orb.setDamage(parseFloat(field, orb.getDamage())); break; - case 101: orb.setOrbSpeed(parseFloat(field, orb.getOrbSpeed())); break; - case 102: orb.setOrbSize(parseFloat(field, orb.getOrbSize())); break; - case 103: orb.setMaxDistance(parseFloat(field, orb.getMaxDistance())); break; - case 105: orb.setHomingStrength(parseFloat(field, orb.getHomingStrength())); break; - case 107: orb.setExplosionRadius(parseFloat(field, orb.getExplosionRadius())); break; - case 108: orb.setKnockback(parseFloat(field, orb.getKnockback())); break; - case 109: orb.setMaxLifetime(field.getInteger()); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java deleted file mode 100644 index 88d7bbf00..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java +++ /dev/null @@ -1,97 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Projectile ability type-specific settings. - */ -public class SubGuiAbilityProjectile extends SubGuiAbilityConfig { - - private final AbilityProjectile projectile; - - public SubGuiAbilityProjectile(AbilityProjectile ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.projectile = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, projectile.getDamage())); - - addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, projectile.getSpeed())); - - y += 24; - - // Row 2: Knockback + Projectile Type - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, projectile.getKnockback())); - - addLabel(new GuiNpcLabel(103, "ability.projType", col2LabelX, y + 5)); - String[] projTypes = {"fireball", "arrow", "magic"}; - int typeIdx = 0; - String currType = projectile.getProjectileType(); - for (int i = 0; i < projTypes.length; i++) { - if (projTypes[i].equals(currType)) { - typeIdx = i; - break; - } - } - addButton(new GuiNpcButton(103, col2FieldX, y, 60, 20, projTypes, typeIdx)); - - y += 24; - - // Row 3: Explosive + Explosion Radius - addLabel(new GuiNpcLabel(104, "ability.explosive", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isExplosive() ? 1 : 0)); - - addLabel(new GuiNpcLabel(105, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, projectile.getExplosionRadius())); - - y += 24; - - // Row 4: Homing + Homing Strength - addLabel(new GuiNpcLabel(106, "ability.homing", labelX, y + 5)); - addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isHoming() ? 1 : 0)); - - addLabel(new GuiNpcLabel(107, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, projectile.getHomingStrength())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 103: - String[] projTypes = {"fireball", "arrow", "magic"}; - projectile.setProjectileType(projTypes[value]); - break; - case 104: projectile.setExplosive(value == 1); break; - case 106: projectile.setHoming(value == 1); break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: projectile.setDamage(parseFloat(field, projectile.getDamage())); break; - case 101: projectile.setSpeed(parseFloat(field, projectile.getSpeed())); break; - case 102: projectile.setKnockback(parseFloat(field, projectile.getKnockback())); break; - case 105: projectile.setExplosionRadius(parseFloat(field, projectile.getExplosionRadius())); break; - case 107: projectile.setHomingStrength(parseFloat(field, projectile.getHomingStrength())); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java deleted file mode 100644 index 38ab22158..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java +++ /dev/null @@ -1,65 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Shockwave ability type-specific settings. - */ -public class SubGuiAbilityShockwave extends SubGuiAbilityConfig { - - private final AbilityShockwave shockwave; - - public SubGuiAbilityShockwave(AbilityShockwave ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.shockwave = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Push Radius - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, shockwave.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.pushRadius", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, shockwave.getPushRadius())); - - y += 24; - - // Row 2: Push Strength - addLabel(new GuiNpcLabel(102, "ability.pushStrength", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, shockwave.getPushStrength())); - - y += 24; - - // Row 3: Stun Duration + Max Targets - addLabel(new GuiNpcLabel(103, "ability.stunDuration", labelX, y + 5)); - addTextField(createIntField(103, fieldX, y, 50, shockwave.getStunDuration())); - - addLabel(new GuiNpcLabel(104, "ability.maxTargets", col2LabelX, y + 5)); - addTextField(createIntField(104, col2FieldX, y, 50, shockwave.getMaxTargets())); - - y += 24; - addLabel(new GuiNpcLabel(105, "ability.shockwaveFalloff", labelX, y + 5)); - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: shockwave.setDamage(parseFloat(field, shockwave.getDamage())); break; - case 101: shockwave.setPushRadius(parseFloat(field, shockwave.getPushRadius())); break; - case 102: shockwave.setPushStrength(parseFloat(field, shockwave.getPushStrength())); break; - case 103: shockwave.setStunDuration(field.getInteger()); break; - case 104: shockwave.setMaxTargets(field.getInteger()); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java deleted file mode 100644 index 7f8c7194e..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java +++ /dev/null @@ -1,62 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Slam ability type-specific settings. - */ -public class SubGuiAbilitySlam extends SubGuiAbilityConfig { - - private final AbilitySlam slam; - - public SubGuiAbilitySlam(AbilitySlam ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.slam = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Radius - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, slam.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.radius", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, slam.getRadius())); - - y += 24; - - // Row 2: Knockback + Leap Speed - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, slam.getKnockbackStrength())); - - addLabel(new GuiNpcLabel(103, "ability.leapSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, slam.getLeapSpeed())); - - y += 24; - - // Row 3: Leap Height - addLabel(new GuiNpcLabel(104, "ability.leapHeight", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, slam.getLeapHeight())); - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: slam.setDamage(parseFloat(field, slam.getDamage())); break; - case 101: slam.setRadius(parseFloat(field, slam.getRadius())); break; - case 102: slam.setKnockbackStrength(parseFloat(field, slam.getKnockbackStrength())); break; - case 103: slam.setLeapSpeed(parseFloat(field, slam.getLeapSpeed())); break; - case 104: slam.setLeapHeight(parseFloat(field, slam.getLeapHeight())); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java deleted file mode 100644 index 5570d944f..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java +++ /dev/null @@ -1,115 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Teleport ability type-specific settings. - */ -public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { - - private final AbilityTeleport teleport; - - public SubGuiAbilityTeleport(AbilityTeleport ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.teleport = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Mode - addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5)); - String[] modes = {"Blink", "Behind", "Single"}; - addButton(new GuiNpcButton(100, fieldX, y, 80, 20, modes, teleport.getMode().ordinal())); - - y += 24; - - switch (teleport.getMode()) { - case BLINK: - // Row 2: Blink Radius + Blink Count - addLabel(new GuiNpcLabel(101, "ability.blinkRadius", labelX, y + 5)); - addTextField(createFloatField(101, fieldX, y, 50, teleport.getBlinkRadius())); - - addLabel(new GuiNpcLabel(102, "ability.blinkCount", col2LabelX, y + 5)); - addTextField(createIntField(102, col2FieldX, y, 50, teleport.getBlinkCount())); - - y += 24; - - // Row 3: Blink Delay + Line of Sight - addLabel(new GuiNpcLabel(103, "ability.blinkDelay", labelX, y + 5)); - addTextField(createIntField(103, fieldX, y, 50, teleport.getBlinkDelayTicks())); - - addLabel(new GuiNpcLabel(105, "ability.lineOfSight", col2LabelX, y + 5)); - addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0)); - y += 24; - break; - case SINGLE: - // Row 2: Blink Radius + Line of Sight - addLabel(new GuiNpcLabel(101, "ability.blinkRadius", labelX, y + 5)); - addTextField(createFloatField(101, fieldX, y, 50, teleport.getBlinkRadius())); - - addLabel(new GuiNpcLabel(105, "ability.lineOfSight", col2LabelX, y + 5)); - addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0)); - y += 24; - break; - case BEHIND: - // Row 2: Behind Distance - addLabel(new GuiNpcLabel(104, "ability.behindDistance", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, teleport.getBehindDistance())); - y += 24; - break; - } - - // Damage + Damage Radius - addLabel(new GuiNpcLabel(106, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(106, fieldX, y, 50, teleport.getDamage())); - - addLabel(new GuiNpcLabel(107, "ability.dmgRadius", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, teleport.getDamageRadius())); - - y += 24; - - // Damage at Start/End - addLabel(new GuiNpcLabel(108, "ability.dmgAtStart", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtStart() ? 1 : 0)); - - addLabel(new GuiNpcLabel(109, "ability.dmgAtEnd", col2LabelX, y + 5)); - addButton(new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtEnd() ? 1 : 0)); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 100: - teleport.setMode(AbilityTeleport.TeleportMode.values()[value]); - initGui(); - break; - case 105: teleport.setRequireLineOfSight(value == 1); break; - case 108: teleport.setDamageAtStart(value == 1); break; - case 109: teleport.setDamageAtEnd(value == 1); break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 101: teleport.setBlinkRadius(parseFloat(field, teleport.getBlinkRadius())); break; - case 102: teleport.setBlinkCount(field.getInteger()); break; - case 103: teleport.setBlinkDelayTicks(field.getInteger()); break; - case 104: teleport.setBehindDistance(parseFloat(field, teleport.getBehindDistance())); break; - case 106: teleport.setDamage(parseFloat(field, teleport.getDamage())); break; - case 107: teleport.setDamageRadius(parseFloat(field, teleport.getDamageRadius())); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java deleted file mode 100644 index 88529fe68..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ /dev/null @@ -1,92 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Trap ability type-specific settings. - */ -public class SubGuiAbilityTrap extends SubGuiAbilityConfig { - - private final AbilityTrap trap; - - public SubGuiAbilityTrap(AbilityTrap ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.trap = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Placement + Trigger Radius - addLabel(new GuiNpcLabel(100, "ability.placement", labelX, y + 5)); - String[] placements = {"Caster", "Target", "Ahead"}; - addButton(new GuiNpcButton(100, fieldX, y, 55, 20, placements, trap.getPlacement().ordinal())); - - addLabel(new GuiNpcLabel(101, "ability.triggerRad", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, trap.getTriggerRadius())); - - y += 24; - - // Row 2: Damage + Damage Radius - addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, trap.getDamage())); - - addLabel(new GuiNpcLabel(103, "ability.dmgRadius", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, trap.getDamageRadius())); - - y += 24; - - // Row 3: Arm Time + Max Triggers - addLabel(new GuiNpcLabel(104, "ability.armTime", labelX, y + 5)); - addTextField(createIntField(104, fieldX, y, 50, trap.getArmTime())); - - addLabel(new GuiNpcLabel(105, "ability.maxTriggers", col2LabelX, y + 5)); - addTextField(createIntField(105, col2FieldX, y, 50, trap.getMaxTriggers())); - - y += 24; - - // Row 4: Root Duration + Stun Duration - addLabel(new GuiNpcLabel(106, "ability.rootDuration", labelX, y + 5)); - addTextField(createIntField(106, fieldX, y, 50, trap.getRootDuration())); - - addLabel(new GuiNpcLabel(107, "ability.stunDuration", col2LabelX, y + 5)); - addTextField(createIntField(107, col2FieldX, y, 50, trap.getStunDuration())); - - y += 24; - - // Row 5: Knockback - addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(108, fieldX, y, 50, trap.getKnockback())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - if (id == 100) { - trap.setPlacement(AbilityTrap.TrapPlacement.values()[button.getValue()]); - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 101: trap.setTriggerRadius(parseFloat(field, trap.getTriggerRadius())); break; - case 102: trap.setDamage(parseFloat(field, trap.getDamage())); break; - case 103: trap.setDamageRadius(parseFloat(field, trap.getDamageRadius())); break; - case 104: trap.setArmTime(field.getInteger()); break; - case 105: trap.setMaxTriggers(field.getInteger()); break; - case 106: trap.setRootDuration(field.getInteger()); break; - case 107: trap.setStunDuration(field.getInteger()); break; - case 108: trap.setKnockback(parseFloat(field, trap.getKnockback())); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java deleted file mode 100644 index a1abbd661..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java +++ /dev/null @@ -1,96 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; - -/** - * GUI for configuring Vortex ability type-specific settings. - */ -public class SubGuiAbilityVortex extends SubGuiAbilityConfig { - - private final AbilityVortex vortex; - - public SubGuiAbilityVortex(AbilityVortex ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.vortex = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Pull Radius + Pull Strength - addLabel(new GuiNpcLabel(100, "ability.pullRadius", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, vortex.getPullRadius())); - - addLabel(new GuiNpcLabel(101, "ability.pullStrength", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, vortex.getPullStrength())); - - y += 24; - - // Row 2: Damage + Knockback - addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, vortex.getDamage())); - - addLabel(new GuiNpcLabel(103, "ability.knockback", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, vortex.getKnockback())); - - y += 24; - - // Row 3: AOE + Max Targets - addLabel(new GuiNpcLabel(104, "ability.aoe", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isAoe() ? 1 : 0)); - - addLabel(new GuiNpcLabel(105, "ability.maxTargets", col2LabelX, y + 5)); - addTextField(createIntField(105, col2FieldX, y, 50, vortex.getMaxTargets())); - - y += 24; - - // Row 4: Stun Duration + Root Duration - addLabel(new GuiNpcLabel(106, "ability.stunDuration", labelX, y + 5)); - addTextField(createIntField(106, fieldX, y, 50, vortex.getStunDuration())); - - addLabel(new GuiNpcLabel(107, "ability.rootDuration", col2LabelX, y + 5)); - addTextField(createIntField(107, col2FieldX, y, 50, vortex.getRootDuration())); - - y += 24; - - // Row 5: Damage on Pull + Pull Damage - addLabel(new GuiNpcLabel(108, "ability.dmgOnPull", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isDamageOnPull() ? 1 : 0)); - - addLabel(new GuiNpcLabel(109, "ability.pullDamage", col2LabelX, y + 5)); - addTextField(createFloatField(109, col2FieldX, y, 50, vortex.getPullDamage())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 104: vortex.setAoe(value == 1); break; - case 108: vortex.setDamageOnPull(value == 1); break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: vortex.setPullRadius(parseFloat(field, vortex.getPullRadius())); break; - case 101: vortex.setPullStrength(parseFloat(field, vortex.getPullStrength())); break; - case 102: vortex.setDamage(parseFloat(field, vortex.getDamage())); break; - case 103: vortex.setKnockback(parseFloat(field, vortex.getKnockback())); break; - case 105: vortex.setMaxTargets(field.getInteger()); break; - case 106: vortex.setStunDuration(field.getInteger()); break; - case 107: vortex.setRootDuration(field.getInteger()); break; - case 109: vortex.setPullDamage(parseFloat(field, vortex.getPullDamage())); break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java index 7d1fa84be..0141e931a 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java @@ -5,6 +5,7 @@ import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket; +import kamkeel.npcs.network.packets.request.animation.BuiltInAnimationGetPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; @@ -37,13 +38,19 @@ import java.util.Vector; public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrollData, ICustomScrollListener, ITextfieldListener, IGuiData, ISubGuiListener, GuiYesNoCallback { private GuiCustomScroll scrollAnimations; - private HashMap data = new HashMap(); + private HashMap customData = new HashMap(); + private HashMap builtInData = new HashMap(); private Animation animation = new Animation(); public boolean playingAnimation = false; private long prevTick; private String selected = null; private String search = ""; + // Toggle between Custom (false) and Built-in (true) animations + private boolean showingBuiltIn = false; + // Track if current animation is built-in + private boolean currentIsBuiltIn = false; + public GuiNPCManageAnimations(EntityNPCInterface npc, boolean save) { super(npc); this.setSave(save); @@ -58,8 +65,15 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void initGui() { super.initGui(); - this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); - this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove")); + // Add/Remove buttons - only show for custom animations + if (!showingBuiltIn) { + this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); + this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove")); + } + + // Toggle button for Custom/Built-in - under Remove button + String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom"; + this.addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 56, 45, 20, toggleLabel)); if (scrollAnimations == null) { scrollAnimations = new GuiCustomScroll(this, 0, 0); @@ -68,13 +82,23 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol scrollAnimations.guiLeft = guiLeft + 220; scrollAnimations.guiTop = guiTop + 4; addScroll(scrollAnimations); + + // Search field addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search)); - if (animation.id == -1) + // Update scroll list based on current view + scrollAnimations.setList(getSearchList()); + + if (animation.id == -1 && !currentIsBuiltIn) return; - addLabel(new GuiNpcLabel(10, "ID", guiLeft + 368, guiTop + 192)); - addLabel(new GuiNpcLabel(11, animation.id + "", guiLeft + 368, guiTop + 192 + 10)); + // ID label - only for custom animations + if (!currentIsBuiltIn) { + addLabel(new GuiNpcLabel(10, "ID", guiLeft + 368, guiTop + 192)); + addLabel(new GuiNpcLabel(11, animation.id + "", guiLeft + 368, guiTop + 192 + 10)); + } else { + addLabel(new GuiNpcLabel(10, "gui.builtin", guiLeft + 368, guiTop + 192)); + } AnimationData data = npc.display.animationData; if (!playingAnimation) { @@ -90,12 +114,13 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol } } - if (animation != null) { + // Edit button - only for custom animations + if (animation != null && !currentIsBuiltIn) { this.addButton(new GuiNpcButton(100, guiLeft + 10, guiTop + 192, 45, 20, "gui.edit")); } String animTexture = "customnpcs:textures/gui/animation.png"; - int playButtonOffsetX = 140; + int playButtonOffsetX = currentIsBuiltIn ? 80 : 140; if (animation != null && !animation.frames.isEmpty()) { if (!this.playingAnimation || data.animation.paused) {//Play this.addLabel(new GuiNpcLabel(90, data.animation.paused ? "animation.paused" : "animation.stopped", guiLeft + playButtonOffsetX - 15, guiTop + 198)); @@ -151,12 +176,17 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol } } + private HashMap getCurrentData() { + return showingBuiltIn ? builtInData : customData; + } + private List getSearchList() { + HashMap data = getCurrentData(); if (search.isEmpty()) { - return new ArrayList(this.data.keySet()); + return new ArrayList(data.keySet()); } List list = new ArrayList(); - for (String name : this.data.keySet()) { + for (String name : data.keySet()) { if (name.toLowerCase().contains(search)) list.add(name); } @@ -167,16 +197,31 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol protected void actionPerformed(GuiButton guibutton) { GuiNpcButton button = (GuiNpcButton) guibutton; - if (button.id == 0) { + // Toggle button + if (button.id == 10) { + showingBuiltIn = !showingBuiltIn; + // Clear selection when switching views + selected = null; + animation = new Animation(); + currentIsBuiltIn = false; + scrollAnimations.clear(); + initGui(); + return; + } + + // Add button - only for custom + if (button.id == 0 && !showingBuiltIn) { save(); String name = "New"; - while (data.containsKey(name)) + while (customData.containsKey(name)) name += "_"; Animation animation = new Animation(-1, name); PacketClient.sendClient(new AnimationSavePacket(animation.writeToNBT())); } - if (button.id == 1) { - if (data.containsKey(scrollAnimations.getSelected())) { + + // Remove button - only for custom + if (button.id == 1 && !showingBuiltIn) { + if (customData.containsKey(scrollAnimations.getSelected())) { GuiYesNo guiyesno = new GuiYesNo(this, scrollAnimations.getSelected(), StatCollector.translateToLocal("gui.delete"), 1); displayGuiScreen(guiyesno); } @@ -204,7 +249,8 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol data.animation.paused = false; } - if (guibutton.id == 100) { + // Edit button - only for custom + if (guibutton.id == 100 && !currentIsBuiltIn) { NoppesUtil.openGUI(player, new GuiNPCEditAnimation(this, this.animation, npc)); } @@ -215,6 +261,10 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void setGuiData(NBTTagCompound compound) { this.animation = new Animation(); animation.readFromNBT(compound); + + // Check if this is a built-in animation + currentIsBuiltIn = compound.hasKey("BuiltIn") && compound.getBoolean("BuiltIn"); + setSelected(animation.name); npc.display.animationData.setAnimation(this.animation); this.playingAnimation = false; @@ -225,7 +275,13 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol @Override public void setData(Vector list, HashMap data, EnumScrollData type) { String name = scrollAnimations.getSelected(); - this.data = data; + + if (type == EnumScrollData.ANIMATIONS) { + this.customData = data; + } else if (type == EnumScrollData.BUILTIN_ANIMATIONS) { + this.builtInData = data; + } + scrollAnimations.setList(getSearchList()); if (name != null) @@ -242,28 +298,40 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { if (guiCustomScroll.id == 0) { selected = scrollAnimations.getSelected(); - PacketClient.sendClient(new AnimationGetPacket(data.get(selected))); + HashMap data = getCurrentData(); + + if (showingBuiltIn) { + // Fetch built-in animation by name + PacketClient.sendClient(new BuiltInAnimationGetPacket(selected)); + } else { + // Fetch custom animation by ID + if (data.containsKey(selected)) { + PacketClient.sendClient(new AnimationGetPacket(data.get(selected))); + } + } } } public void save() { - if (selected != null && data.containsKey(selected) && animation != null) { + // Only save custom animations + if (selected != null && customData.containsKey(selected) && animation != null && !currentIsBuiltIn) { PacketClient.sendClient(new AnimationSavePacket(animation.writeToNBT())); } } @Override public void unFocused(GuiNpcTextField guiNpcTextField) { - if (animation.id == -1) + // Don't allow renaming built-in animations + if (animation.id == -1 || currentIsBuiltIn) return; if (guiNpcTextField.id == 0) { String name = guiNpcTextField.getText(); - if (!name.isEmpty() && !data.containsKey(name)) { + if (!name.isEmpty() && !customData.containsKey(name)) { String old = animation.name; - data.remove(animation.name); + customData.remove(animation.name); animation.name = name; - data.put(animation.name, animation.id); + customData.put(animation.name, animation.id); selected = name; scrollAnimations.replace(old, animation.name); } @@ -280,10 +348,12 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol if (!result) return; if (id == 1) { - if (data.containsKey(scrollAnimations.getSelected())) { - PacketClient.sendClient(new AnimationRemovePacket(data.get(selected))); + // Only delete custom animations + if (customData.containsKey(scrollAnimations.getSelected()) && !showingBuiltIn) { + PacketClient.sendClient(new AnimationRemovePacket(customData.get(selected))); scrollAnimations.clear(); animation = new Animation(); + currentIsBuiltIn = false; initGui(); } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java index e07d2583c..52e78f7d6 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java @@ -338,21 +338,21 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Health label = StatCollector.translateToLocal("stats.health") + ": "; - value = String.valueOf(npc.stats.maxHealth); + value = "" + npc.stats.maxHealth; fontRendererObj.drawString(label, xLabel, y, 0x29d6b9, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // Damage (using getAttackStrength) label = StatCollector.translateToLocal("stats.meleestrength") + ": "; - value = String.valueOf(npc.stats.getAttackStrength()); + value = "" + npc.stats.getAttackStrength(); fontRendererObj.drawString(label, xLabel, y, 0xff5714, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // Attack Speed label = StatCollector.translateToLocal("stats.meleespeed") + ": "; - value = String.valueOf(npc.stats.attackSpeed); + value = "" + npc.stats.attackSpeed; fontRendererObj.drawString(label, xLabel, y, 0xf7ca28, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -381,7 +381,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Walk Speed label = StatCollector.translateToLocal("stats.speed") + ": "; - value = String.valueOf(npc.ais.getWalkingSpeed()); + value = "" + npc.ais.getWalkingSpeed(); fontRendererObj.drawString(label, xLabel, y, 0xffae0d, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -415,7 +415,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Version label = StatCollector.translateToLocal("display.version") + ": "; - value = String.valueOf(linkedItem.version); + value = "" + linkedItem.version; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -423,7 +423,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Max Stack Size labelColor = 0xff5714; label = StatCollector.translateToLocal("display.maxStack") + ": "; - value = String.valueOf(linkedItem.stackSize); + value = "" + linkedItem.stackSize; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -431,7 +431,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Dig Speed labelColor = 0xf7ca28; label = StatCollector.translateToLocal("display.digSpeed") + ": "; - value = String.valueOf(linkedItem.digSpeed); + value = "" + linkedItem.digSpeed; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -498,14 +498,14 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, labelColor = 0x7cff54; // isTool label = StatCollector.translateToLocal("display.isTool") + ": "; - value = String.valueOf(linkedItem.isTool).toUpperCase(); + value = ("" + linkedItem.isTool).toUpperCase(); fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // isNormalItem label = StatCollector.translateToLocal("display.isNormalItem") + ": "; - value = String.valueOf(linkedItem.isNormalItem).toUpperCase(); + value = ("" + linkedItem.isNormalItem).toUpperCase(); fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index e5ae8c436..8fa2e4e41 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -1,114 +1,269 @@ package noppes.npcs.client.gui.global; +import kamkeel.npcs.client.renderer.TelegraphRenderer; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; import net.minecraft.client.gui.GuiButton; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.SubGuiAbilityDeleteConfirm; -import noppes.npcs.client.gui.advanced.SubGuiAbilityLoad; -import noppes.npcs.client.gui.util.*; +import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.GuiYesNoCallback; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.AbilityPreviewExecutor; +import noppes.npcs.client.gui.util.GuiAbilityInterface; +import noppes.npcs.client.gui.util.GuiCustomScroll; +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.GuiTexturedButton; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.IScrollData; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.constants.EnumScrollData; +import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.entity.EntityNPCInterface; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Vector; /** - * Global GUI for managing saved ability presets. - * Allows viewing, editing, and deleting saved abilities. + * Global GUI for managing saved ability presets with 3D preview. + * + * Features: + * - Left side: 3D preview of NPC with ability effects + * - Right side: Ability list with search + * - Play/Pause/Stop buttons to preview abilities */ -public class GuiNpcManageAbilities extends GuiNPCInterface2 implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback { +public class GuiNpcManageAbilities extends GuiAbilityInterface + implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback, + ITextfieldListener, GuiYesNoCallback, IScrollData, IGuiData { + // ==================== DATA ==================== private GuiCustomScroll scroll; - private String selectedName = null; - private List abilityNames = new ArrayList<>(); + private HashMap abilityData = new HashMap<>(); + /** Maps display name -> UUID for custom abilities */ + private final HashMap displayToUuid = new HashMap<>(); + private String selected = null; + private String selectedUuid = null; private String search = ""; + private Ability selectedAbility = null; + + // ==================== PREVIEW ==================== + private AbilityPreviewExecutor previewExecutor; + private long prevTick = 0; public GuiNpcManageAbilities(EntityNPCInterface npc) { super(npc); - loadAbilityNames(); - } - private void loadAbilityNames() { - Set names = AbilityController.Instance.getSavedAbilityNames(); - abilityNames = new ArrayList<>(names); + // Layout offset + this.xOffset = -148 + 110; + this.yOffset = -170 + 167; + + // Initialize preview executor + previewExecutor = new AbilityPreviewExecutor(); + previewExecutor.setParentGui(this); + + // Request ability data from server + PacketClient.sendClient(new CustomAbilitiesGetPacket()); + + // Enable animation data + AnimationData data = npc.display.animationData; + data.setEnabled(true); } @Override public void initGui() { super.initGui(); - int y = guiTop + 10; + // Edit button (above Remove) + addButton(new GuiNpcButton(100, guiLeft + 368, guiTop + 8, 45, 20, "gui.edit")); + getButton(100).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null); - // Title label - addLabel(new GuiNpcLabel(0, "ability.manager.title", guiLeft + 10, y)); - y += 20; + // Remove button + addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove")); + getButton(1).setEnabled(selected != null && !selected.isEmpty() && displayToUuid.containsKey(selected)); - // Scroll list of saved abilities + // Scroll list of saved abilities (right side) if (scroll == null) { scroll = new GuiCustomScroll(this, 0); + scroll.setSize(143, 185); } - scroll.setSize(xSize - 90, 160); - scroll.guiLeft = guiLeft + 10; - scroll.guiTop = y; - scroll.setUnsortedList(getFilteredList()); - if (selectedName != null && abilityNames.contains(selectedName)) { - scroll.setSelected(selectedName); - } + scroll.guiLeft = guiLeft + 220; + scroll.guiTop = guiTop + 4; addScroll(scroll); + // Update scroll list + scroll.setList(getSearchList()); + if (selected != null && displayToUuid.containsKey(selected)) { + scroll.setSelected(selected); + } + // Search bar - addTextField(new GuiNpcTextField(1, this, fontRendererObj, guiLeft + 10, y + 163, xSize - 90, 18, search)); + addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search)); - // Right side buttons - int btnX = guiLeft + xSize - 70; - addButton(new GuiNpcButton(1, btnX, y, 60, 20, "gui.edit")); - addButton(new GuiNpcButton(2, btnX, y + 25, 60, 20, "gui.delete")); - addButton(new GuiNpcButton(3, btnX, y + 60, 60, 20, "selectServer.refresh")); + // Show ability info only if one is selected + if (selectedAbility == null || selected == null) { + return; + } - // Enable/disable buttons based on selection - getButton(1).setEnabled(selectedName != null && !selectedName.isEmpty()); - getButton(2).setEnabled(selectedName != null && !selectedName.isEmpty()); + // ID/Name label (below search bar) + addLabel(new GuiNpcLabel(10, "ID: " + selectedAbility.getId(), guiLeft + 220, guiTop + 215)); + + // Play/Pause/Stop buttons (below model preview area) + String animTexture = "customnpcs:textures/gui/animation.png"; + int playButtonOffsetX = 60; + + boolean isPlaying = previewExecutor.isPlaying() && !previewExecutor.isPaused(); + boolean isPaused = previewExecutor.isPaused(); + boolean isActive = previewExecutor.isActive(); + + if (!isPlaying || isPaused) { + // Show Play button + String statusKey = isPaused ? "animation.paused" : "animation.stopped"; + addLabel(new GuiNpcLabel(90, statusKey, guiLeft + playButtonOffsetX, guiTop + 198)); + addButton(new GuiTexturedButton(91, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 11, 20, animTexture, 18, 71)); + } else { + // Show Pause button + addLabel(new GuiNpcLabel(90, "animation.playing", guiLeft + playButtonOffsetX, guiTop + 198)); + addButton(new GuiTexturedButton(92, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 14, 20, animTexture, 0, 71)); + } + if (isActive) { + // Show Stop button + addButton(new GuiTexturedButton(93, "", guiLeft + playButtonOffsetX + 90, guiTop + 192, 14, 20, animTexture, 33, 71)); + } } - private List getFilteredList() { + private List getSearchList() { if (search.isEmpty()) { - return new ArrayList<>(abilityNames); + return new ArrayList<>(displayToUuid.keySet()); } - List filtered = new ArrayList<>(); - for (String name : abilityNames) { + List list = new ArrayList<>(); + for (String name : displayToUuid.keySet()) { if (name.toLowerCase().contains(search.toLowerCase())) { - filtered.add(name); + list.add(name); } } - return filtered; + return list; } @Override - public void buttonEvent(GuiButton guibutton) { + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + // IMPORTANT: Set NPC rotation BEFORE ticking entities + // This ensures anchor point calculations use the correct facing direction + npc.prevRenderYawOffset = npc.renderYawOffset = NPC_FACING_YAW; + npc.prevRotationYaw = npc.rotationYaw = NPC_FACING_YAW; + npc.prevRotationYawHead = npc.rotationYawHead = NPC_FACING_YAW; + + // Tick preview executor (entities will use the rotation set above) + tickPreview(); + + // Draw base (includes 3D preview via parent class) + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw status text + if (previewExecutor.isActive()) { + String status = previewExecutor.getStatusString(); + fontRendererObj.drawString(status, guiLeft + 12, guiTop + 175, 0xFFFFFF); + } + } + + /** + * Tick the preview executor and animations. + */ + private void tickPreview() { + if (previewExecutor.isPlaying() && !previewExecutor.isPaused()) { + long time = mc.theWorld != null ? mc.theWorld.getTotalWorldTime() : System.currentTimeMillis() / 50; + if (time != prevTick) { + // Advance animation + npc.display.animationData.increaseTime(); + + // Tick preview + previewExecutor.tick(); + + // Sync telegraph to GUI for rendering + setPreviewTelegraph(previewExecutor.getTelegraph()); + + prevTick = time; + + // Check if preview finished + if (!previewExecutor.isActive()) { + initGui(); + } + } + } + } + + @Override + protected void actionPerformed(GuiButton guibutton) { + super.actionPerformed(guibutton); + int id = guibutton.id; - if (id == 1 && selectedName != null) { + if (id == 1 && selected != null && selectedUuid != null) { + GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), 1); + displayGuiScreen(guiyesno); + } else if (id == 100 && selectedAbility != null) { // Edit - Ability ability = AbilityController.Instance.getSavedAbility(selectedName); - if (ability != null) { - setSubGui(ability.createConfigGui(this)); + previewExecutor.stop(); + setSubGui(selectedAbility.createConfigGui(this)); + } else if (id == 91) { + // Play button + if (selectedAbility != null) { + if (previewExecutor.isPaused()) { + previewExecutor.play(); + } else { + previewExecutor.startPreview(selectedAbility, npc); + } + initGui(); } - } else if (id == 2 && selectedName != null) { - // Delete - setSubGui(new SubGuiAbilityDeleteConfirm(selectedName, null)); - } else if (id == 3) { - // Refresh - AbilityController.Instance.load(); - loadAbilityNames(); - selectedName = null; + } else if (id == 92) { + // Pause button + previewExecutor.pause(); initGui(); + } else if (id == 93) { + // Stop button + previewExecutor.stop(); + initGui(); + } + } + + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + if (getTextField(55) != null && getTextField(55).isFocused()) { + if (!search.equals(getTextField(55).getText())) { + search = getTextField(55).getText(); + scroll.resetScroll(); + scroll.setList(getSearchList()); + } } } @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { if (scroll.id == 0) { - selectedName = scroll.getSelected(); - initGui(); + String newSelection = scroll.getSelected(); + if (newSelection != null && !newSelection.equals(selected)) { + previewExecutor.stop(); + + selected = newSelection; + selectedUuid = displayToUuid.get(selected); + if (selectedUuid != null) { + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } + } } } @@ -116,48 +271,101 @@ public class GuiNpcManageAbilities extends GuiNPCInterface2 implements ICustomSc public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { if (scroll.id == 0 && selection != null && !selection.isEmpty()) { // Double-click to edit - Ability ability = AbilityController.Instance.getSavedAbility(selection); - if (ability != null) { - setSubGui(ability.createConfigGui(this)); + if (selectedAbility != null) { + previewExecutor.stop(); + setSubGui(selectedAbility.createConfigGui(this)); } } } @Override - public void keyTyped(char c, int i) { - super.keyTyped(c, i); - if (getTextField(1) != null && getTextField(1).isFocused()) { - if (!search.equals(getTextField(1).getText())) { - search = getTextField(1).getText(); - scroll.setUnsortedList(getFilteredList()); - scroll.resetScroll(); + public void setData(Vector list, HashMap data, EnumScrollData type) { + if (type == EnumScrollData.CUSTOM_ABILITIES) { + String prevSelected = scroll != null ? scroll.getSelected() : null; + this.abilityData = data; + displayToUuid.clear(); + for (String key : data.keySet()) { + // key format: "displayName\tUUID" + int tabIndex = key.indexOf('\t'); + if (tabIndex > 0) { + String displayName = key.substring(0, tabIndex); + String uuid = key.substring(tabIndex + 1); + displayToUuid.put(displayName, uuid); + } else { + displayToUuid.put(key, key); + } } + if (scroll != null) { + scroll.setList(getSearchList()); + if (prevSelected != null && displayToUuid.containsKey(prevSelected)) { + scroll.setSelected(prevSelected); + } + } + initGui(); + } + } + + @Override + public void setGuiData(NBTTagCompound compound) { + // Received ability data from server + selectedAbility = AbilityController.Instance.fromNBT(compound); + if (selectedAbility != null) { + selected = selectedAbility.getName(); + if (scroll != null) { + scroll.setSelected(selected); + } + } + previewExecutor.stop(); + initGui(); + } + + @Override + public void setSelected(String selected) { + this.selected = selected; + if (scroll != null) { + scroll.setSelected(selected); } } @Override public void subGuiClosed(SubGuiInterface subgui) { - // Refresh list after any sub gui closes - loadAbilityNames(); - if (subgui instanceof SubGuiAbilityDeleteConfirm) { - // After delete, clear selection if it was deleted - if (selectedName != null && !abilityNames.contains(selectedName)) { - selectedName = null; - } - } initGui(); } + @Override + public void confirmClicked(boolean result, int id) { + NoppesUtil.openGUI(player, this); + if (!result) return; + + if (id == 1 && selected != null && selectedUuid != null) { + PacketClient.sendClient(new CustomAbilityRemovePacket(selectedUuid)); + scroll.clear(); + selected = null; + selectedUuid = null; + selectedAbility = null; + initGui(); + } + } + @Override public void onAbilitySaved(Ability ability) { - // Save the edited ability - AbilityController.Instance.saveAbility(ability); - loadAbilityNames(); - initGui(); + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + // Search field handling is done in keyTyped } @Override public void save() { - // Nothing to save - abilities are saved individually + // Nothing to save - abilities are saved individually via packets + } + + @Override + public void onGuiClosed() { + super.onGuiClosed(); + // Stop preview when GUI closes + previewExecutor.stop(); } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java index 1b83b0639..40df2d463 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java @@ -12,7 +12,7 @@ import net.minecraft.util.StatCollector; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.SubGuiNpcAvailability; -import noppes.npcs.client.gui.script.GuiScriptRecipe; +import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.GuiButtonBiDirectional; import noppes.npcs.client.gui.util.GuiContainerNPCInterface2; import noppes.npcs.client.gui.util.GuiCustomScroll; @@ -31,6 +31,7 @@ import noppes.npcs.constants.EnumScrollData; import noppes.npcs.containers.ContainerManageRecipes; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; +import noppes.npcs.controllers.data.RecipeScript; import noppes.npcs.entity.EntityNPCInterface; import org.lwjgl.opengl.GL11; @@ -222,15 +223,9 @@ public class GuiNpcManageRecipes extends GuiContainerNPCInterface2 implements IS if (button.id == 16) { save(); if (container.width == 1) { - GuiScriptRecipe gui = new GuiScriptRecipe(this, container.recipeAnvil); - gui.setWorldAndResolution(mc, width, height); - gui.initGui(); - mc.currentScreen = gui; + GuiScriptInterface.open(this, new RecipeScript(container.recipeAnvil.id, true)); } else if (!container.recipe.isGlobal) { - GuiScriptRecipe gui = new GuiScriptRecipe(this, container.recipe); - gui.setWorldAndResolution(mc, width, height); - gui.initGui(); - mc.currentScreen = gui; + GuiScriptInterface.open(this, new RecipeScript(container.recipe.id, false)); } } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java index 41e105a38..ecdec2990 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java @@ -90,7 +90,7 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I ArrayList list = new ArrayList<>(); Set keySet = this.spawn.spawnCompounds.keySet(); for (int i : keySet) { - list.add(String.valueOf(i)); + list.add("" + i); } this.spawnEntryScroll.setList(list); addScroll(this.spawnEntryScroll); @@ -99,7 +99,7 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I if (this.spawnEntryScroll.hasSelected()) { int selected = Integer.parseInt(this.spawnEntryScroll.getSelected()); addButton(new GuiNpcButton(22, guiLeft + 32, y, 20, 20, "-")); - GuiNpcTextField num = new GuiNpcTextField(25, this, guiLeft + 60, y += 10, 30, 20, String.valueOf(selected)); + GuiNpcTextField num = new GuiNpcTextField(25, this, guiLeft + 60, y += 10, 30, 20, "" + selected); num.integersOnly = true; addTextField(num); addButton(new GuiNpcButton(26, guiLeft + 92, y, 100, 20, this.getTitle(this.spawn.spawnCompounds.get(selected)))); @@ -215,8 +215,8 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I } if (textfield.id == 25 && this.spawnEntryScroll.hasSelected()) { int selected = Integer.parseInt(spawnEntryScroll.getSelected()); - if (spawnEntryScroll.getList().contains(String.valueOf(textfield.getInteger()))) { - textfield.setText(String.valueOf(selected)); + if (spawnEntryScroll.getList().contains("" + textfield.getInteger())) { + textfield.setText("" + selected); return; } NBTTagCompound compound = spawn.spawnCompounds.get(selected); diff --git a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java index 227f7cecc..b71f6642a 100644 --- a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java @@ -12,7 +12,7 @@ import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.global.GuiNPCManageLinked; -import noppes.npcs.client.gui.script.GuiScriptLinkedItem; +import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.GuiButtonBiDirectional; import noppes.npcs.client.gui.util.GuiMenuTopButton; import noppes.npcs.client.gui.util.GuiNpcButton; @@ -25,6 +25,7 @@ import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.client.renderer.ImageData; import noppes.npcs.controllers.data.LinkedItem; +import noppes.npcs.controllers.data.LinkedItemScript; import org.lwjgl.opengl.GL11; public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListener, GuiYesNoCallback, ISubGuiListener { @@ -119,8 +120,8 @@ public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListe GuiNpcTextField digSpeedField = new GuiNpcTextField(12, this, x + 110, y, 50, 20, "" + linkedItem.digSpeed); digSpeedField.setIntegersOnly().setMinMaxDefault(0, 20, 1); addTextField(digSpeedField); - GuiNpcTextField attackSpeedField = new GuiNpcTextField(14, this, x +165, y, 50, 20, "" +linkedItem.attackSpeed); - attackSpeedField.setIntegersOnly().setMinMaxDefault(0,999999999,20); + GuiNpcTextField attackSpeedField = new GuiNpcTextField(14, this, x + 165, y, 50, 20, "" + linkedItem.attackSpeed); + attackSpeedField.setIntegersOnly().setMinMaxDefault(0, 999999999, 20); addTextField(attackSpeedField); y += spacing; @@ -307,10 +308,7 @@ public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListe return; } else if (id == -3) { PacketClient.sendClient(new LinkedItemSavePacket(linkedItem.writeToNBT(false), originalName)); - GuiScriptLinkedItem scriptGUI = new GuiScriptLinkedItem((GuiNPCManageLinked) this.parent, linkedItem); - scriptGUI.setWorldAndResolution(mc, width, height); - scriptGUI.initGui(); - mc.currentScreen = scriptGUI; + GuiScriptInterface.open(this.parent, new LinkedItemScript(linkedItem.id)); return; } else if (id == 20) { // Open confirmation for version bump. diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java index bca447b1f..b055cbf43 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java @@ -39,6 +39,7 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 { private GuiNpcSquareButton effectsButton; private GuiNpcSquareButton magicsButton; private GuiNpcSquareButton abilitiesButton; + private GuiNpcSquareButton auctionsButton; public GuiNPCGlobalMainMenu(EntityNPCInterface npc) { super(npc, 5); @@ -96,7 +97,10 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 { magicsButton.setIconPos(24, 24, 48, 56).setIconTexture(specialIcons); registerButton(abilitiesButton = new GuiNpcSquareButton(21, 0, 0, 20, "global.abilities", 0xFF333333)); - abilitiesButton.setIconPos(24, 24, 72, 56).setIconTexture(specialIcons); // Placeholder icon position + abilitiesButton.setIconPos(24, 24, 72, 56).setIconTexture(specialIcons); + + registerButton(auctionsButton = new GuiNpcSquareButton(22, 0, 0, 20, "global.auction", 0xFF333333)); + auctionsButton.setIconPos(24, 24, 96, 56).setIconTexture(specialIcons); // Layout composite buttons optimally. layoutButtons(); @@ -240,7 +244,10 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 { NoppesUtil.requestOpenGUI(EnumGuiType.ManageEffects); } if (id == 21) { - NoppesUtil.openGUI(player, new noppes.npcs.client.gui.global.GuiNpcManageAbilities(npc)); + NoppesUtil.requestOpenGUI(EnumGuiType.ManageAbilities); + } + if (id == 22) { + // NoppesUtil.requestOpenGUI(EnumGuiType.ManageAuction); } } diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java index 885c61d94..ff46f9174 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java @@ -9,11 +9,11 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.advanced.GuiNPCAbilities; import noppes.npcs.client.gui.advanced.GuiNPCAdvancedLinkedNpc; import noppes.npcs.client.gui.advanced.GuiNPCDialogNpcOptions; import noppes.npcs.client.gui.advanced.GuiNPCFactionSetup; import noppes.npcs.client.gui.advanced.GuiNPCLinesMenu; -import noppes.npcs.client.gui.advanced.GuiNPCAbilities; import noppes.npcs.client.gui.advanced.GuiNPCMagic; import noppes.npcs.client.gui.advanced.GuiNPCMarks; import noppes.npcs.client.gui.advanced.GuiNPCNightSetup; @@ -25,8 +25,8 @@ import noppes.npcs.client.gui.roles.GuiNpcConversation; import noppes.npcs.client.gui.roles.GuiNpcFollowerJob; import noppes.npcs.client.gui.roles.GuiNpcGuard; import noppes.npcs.client.gui.roles.GuiNpcHealer; -import noppes.npcs.client.gui.roles.GuiNpcSpawner; import noppes.npcs.client.gui.roles.GuiNpcMount; +import noppes.npcs.client.gui.roles.GuiNpcSpawner; import noppes.npcs.client.gui.roles.GuiNpcTransporter; import noppes.npcs.client.gui.util.GuiNPCInterface2; import noppes.npcs.client.gui.util.GuiNpcButton; @@ -50,7 +50,7 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { int y = guiTop + 10; this.addButton(new GuiNpcButton(3, guiLeft + 85 + 160, y, 52, 20, "selectServer.edit")); this.addButton(new GuiNpcButton(8, guiLeft + 85, y, 155, 20, new String[]{"role.none", "role.trader", "role.follower", "role.bank", "role.transporter", "role.mailman", NoppesStringUtils.translate("role.companion", "(WIP)"), "role.mount", "role.auctioneer"}, npc.advanced.role.ordinal())); - getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman); + getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman && npc.advanced.role != EnumRoleType.Auctioneer); this.addButton(new GuiNpcButton(4, guiLeft + 85 + 160, y += 22, 52, 20, "selectServer.edit")); this.addButton(new GuiNpcButton(5, guiLeft + 85, y, 155, 20, new String[]{"job.none", "job.bard", "job.healer", "job.guard", "job.itemgiver", "role.follower", "job.spawner", "job.conversation", "job.chunkloader"}, npc.advanced.job.ordinal())); @@ -84,7 +84,7 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { hasChanges = true; npc.advanced.setRole(button.getValue()); - getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman); + getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman && npc.advanced.role != EnumRoleType.Auctioneer); } if (button.id == 4) { save(); diff --git a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java new file mode 100644 index 000000000..a50622529 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java @@ -0,0 +1,281 @@ +package noppes.npcs.client.gui.player; + +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.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.StatCollector; +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.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionListing; + +import java.util.List; + +/** + * Handles adding auction information to item tooltips when viewing auction GUIs. + * Called from MixinItem when Item.addInformation is invoked. + */ +@SideOnly(Side.CLIENT) +public class AuctionTooltipHandler { + + /** + * Add auction information to an item's tooltip if we're in an auction GUI. + */ + public static void addAuctionInfo(ItemStack stack, List tooltip) { + if (stack == null || tooltip == 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); + } + } + + /** + * Add auction info when viewing the main listings page. + */ + private static void addListingTooltip(GuiAuctionListing gui, ItemStack stack, List tooltip) { + ContainerAuctionListing container = (ContainerAuctionListing) gui.inventorySlots; + if (container == null) return; + + // Find the listing that matches this ItemStack + AuctionListing listing = container.getListingForItem(stack); + if (listing == null) return; + + tooltip.add(""); + + // Seller + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.seller") + .replace("%s", EnumChatFormatting.WHITE + listing.sellerName)); + + // Current bid or starting price + 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.YELLOW + StatCollector.translateToLocal("auction.startingPrice") + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.startingPrice))); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids")); + } + + // Buyout price + if (listing.hasBuyout()) { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.buyout") + .replace("%s", EnumChatFormatting.GREEN + formatCurrency(listing.buyoutPrice))); + } + + // Time remaining + long timeRemaining = listing.getTimeRemaining(); + String timeText = StatCollector.translateToLocal("auction.timeLeft") + .replace("%s", formatTimeRemaining(timeRemaining)); + tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); + } + + /** + * Add auction info when viewing the trades page. + */ + private static void addTradesTooltip(GuiAuctionTrades gui, ItemStack stack, List tooltip) { + ContainerAuctionTrades container = (ContainerAuctionTrades) gui.inventorySlots; + 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")); + + 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")); + } + + 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); + 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")); + } + } + } + + /** + * Add sell page tooltip info. + */ + private static void addSellTooltip(GuiAuctionSell gui, ItemStack stack, List tooltip) { + 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 + tooltip.add(""); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.leftClear")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.rightRemove")); + return; + } + + // 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. + */ + private static String formatCurrency(long amount) { + if (amount < 1000) { + return amount + " " + AuctionClientConfig.getCurrencyName(); + } + StringBuilder sb = new StringBuilder(); + String str = "" + amount; + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) { + sb.insert(0, ','); + } + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString() + " " + AuctionClientConfig.getCurrencyName(); + } + + /** + * Format time remaining. + */ + private static String formatTimeRemaining(long milliseconds) { + if (milliseconds <= 0) { + return StatCollector.translateToLocal("auction.ended"); + } + + long seconds = (milliseconds / 1000) % 60; + long minutes = (milliseconds / 60000) % 60; + long hours = (milliseconds / 3600000) % 24; + long days = milliseconds / 86400000; + + StringBuilder sb = new StringBuilder(); + if (days > 0) { + sb.append(days).append("d "); + } + if (hours > 0 || days > 0) { + sb.append(hours).append("h "); + } + if (minutes > 0 || hours > 0 || days > 0) { + sb.append(minutes).append("m"); + } else { + sb.append(seconds).append("s"); + } + + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java b/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java index 683e21798..00551911a 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java @@ -237,7 +237,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { fontRendererObj.drawString(percentText, percentTextX, percentTextY, CustomNpcResourceListener.DefaultTextColor); // Add text overlay for percentage with the full unrounded value. if (func_146978_c(percentTextX - guiLeft, percentTextY - guiTop, percentWidth, fontRendererObj.FONT_HEIGHT, mouseX, mouseY)) { - textOverlays.add(new TextOverlayData(percentTextX, percentTextY, percentWidth, fontRendererObj.FONT_HEIGHT, String.valueOf(recipe.getRepairPercentage()))); + textOverlays.add(new TextOverlayData(percentTextX, percentTextY, percentWidth, fontRendererObj.FONT_HEIGHT, "" + recipe.getRepairPercentage())); } if (!recipe.availability.isDefault()) { @@ -303,7 +303,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { int xpBoxWidth = scaledTextWidth; int xpBoxHeight = fontRendererObj.FONT_HEIGHT; if (func_146978_c(xpTextX - guiLeft, xpTextY - guiTop, xpBoxWidth, xpBoxHeight, mouseX, mouseY)) { - textOverlays.add(new TextOverlayData(xpTextX, xpTextY, xpBoxWidth, xpBoxHeight, String.valueOf(recipe.getXpCost()))); + textOverlays.add(new TextOverlayData(xpTextX, xpTextY, xpBoxWidth, xpBoxHeight, "" + recipe.getXpCost())); } } @@ -326,7 +326,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { private String formatXpCost(int xp) { if (xp < 1000) { - return String.valueOf(xp); + return "" + xp; } double xpK = xp / 1000.0; if (xpK == (int) xpK) { diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java new file mode 100644 index 000000000..afe47fb44 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java @@ -0,0 +1,309 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.inventory.Slot; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.client.gui.util.GuiAuctionNavButton; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.containers.ContainerAuctionBidding; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * GUI for viewing and bidding on a specific auction listing. + * Shows item, auction info, and bid/buyout buttons. + */ +public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiListener, IGuiData { + // Icons + private static final ResourceLocation ICON_BID = new ResourceLocation("customnpcs", "textures/items/npcCoinDiamond.png"); + private static final ResourceLocation ICON_BUYOUT = new ResourceLocation("customnpcs", "textures/items/npcCoinEmerald.png"); + + // Layout + private int itemSlotX = 70; + private int itemSlotY = 75; + private int btnBidX = 58; + private int btnBidY = 110; + private int btnBuyoutX = 82; + private int btnBuyoutY = 110; + private int infoX = 110; + private int infoY = 50; + + // Button IDs + private int btnBidId = 200; + private int btnBuyoutId = 201; + + private final ContainerAuctionBidding biddingContainer; + private AuctionListing listing; + private long playerBalance = 0; + private boolean dataLoaded = false; + private boolean isOwnListing = false; + + // Buttons + private GuiAuctionNavButton btnBid; + private GuiAuctionNavButton btnBuyout; + + public GuiAuctionBidding(EntityNPCInterface npc, ContainerAuctionBidding container) { + super(npc, container); + this.biddingContainer = container; + } + + @Override + public void initGui() { + super.initGui(); + + // Bid button (diamond coin) + btnBid = new GuiAuctionNavButton(btnBidId, guiLeft + btnBidX, guiTop + btnBidY, + "auction.bid.place", ICON_BID); + addButton(btnBid); + + // Buy Now button (emerald coin) - only visible if listing has buyout + btnBuyout = new GuiAuctionNavButton(btnBuyoutId, guiLeft + btnBuyoutX, guiTop + btnBuyoutY, + "auction.bid.buyout", ICON_BUYOUT); + addButton(btnBuyout); + + updateButtonState(); + } + + @Override + protected int getCurrentPage() { + // Not a standard page - return -1 so nav buttons aren't selected + return -1; + } + + /** Update button visibility based on listing state */ + private void updateButtonState() { + if (listing == null) { + if (btnBid != null) btnBid.setVisible(false); + if (btnBuyout != null) btnBuyout.setVisible(false); + return; + } + + // Hide buttons for own listings - can view but not bid/buy + if (isOwnListing) { + if (btnBid != null) btnBid.setVisible(false); + if (btnBuyout != null) btnBuyout.setVisible(false); + return; + } + + // Bid button visible for other players' listings + if (btnBid != null) btnBid.setVisible(true); + + // Buyout button only if listing has buyout + if (btnBuyout != null) { + btnBuyout.setVisible(listing.hasBuyout()); + } + } + + @Override + public void actionPerformed(GuiButton button) { + super.actionPerformed(button); + + if (listing == null) return; + + if (button.id == btnBidId) { + openBidSubGui(); + } else if (button.id == btnBuyoutId) { + if (listing.hasBuyout()) { + openBuyNowSubGui(); + } + } + } + + private void openBidSubGui() { + if (listing == null) return; + long minBid = listing.getMinimumBid(AuctionClientConfig.getMinBidIncrement()); + setSubGui(new SubGuiAuctionBid(listing.id, minBid, playerBalance)); + } + + private void openBuyNowSubGui() { + if (listing == null || !listing.hasBuyout()) return; + setSubGui(new SubGuiAuctionBuyNow(listing.id, listing.buyoutPrice, playerBalance)); + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiAuctionBid) { + SubGuiAuctionBid bidGui = (SubGuiAuctionBid) subgui; + if (bidGui.wasSuccessful()) { + // Bid placed - go to listings + AuctionActionPacket.openPage(PAGE_LISTINGS); + } + } else if (subgui instanceof SubGuiAuctionBuyNow) { + SubGuiAuctionBuyNow buyGui = (SubGuiAuctionBuyNow) subgui; + if (buyGui.wasSuccessful()) { + // Bought out - go to my trades + AuctionActionPacket.openPage(PAGE_CLAIMS); + } + } + } + + // ========== Data Handling ========== + + @Override + public void setGuiData(NBTTagCompound compound) { + if (compound.hasKey("BiddingData")) { + playerBalance = compound.getLong("Balance"); + isOwnListing = compound.getBoolean("IsOwnListing"); + + if (compound.hasKey("Listing")) { + listing = AuctionListing.fromNBT(compound.getCompoundTag("Listing")); + biddingContainer.setListing(listing); + } + + dataLoaded = true; + updateButtonState(); + } + } + + // ========== Drawing ========== + + @Override + protected void drawAuctionContent(float partialTicks, int mouseX, int mouseY) { + // Draw item slot background + drawAuctionSlot(guiLeft + itemSlotX, guiTop + itemSlotY); + + if (!dataLoaded || listing == null) { + // Loading text + String loading = StatCollector.translateToLocal("gui.loading"); + fontRendererObj.drawString(loading, guiLeft + infoX, guiTop + infoY, 0xFFFFFF); + return; + } + + // Draw auction info on the right side + drawAuctionInfo(); + } + + /** Draw auction listing information on the right side */ + private void drawAuctionInfo() { + int x = guiLeft + 105; + int y = guiTop + infoY; + int xRight = guiLeft + 215; + + int lineHeight = 9; + + // Seller + String sellerLabel = StatCollector.translateToLocal("auction.seller").replace("%s", ""); + fontRendererObj.drawString(EnumChatFormatting.GRAY + sellerLabel, x, y, 0xFFFFFF); + + String label = listing.sellerName; + int length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.WHITE + listing.sellerName, xRight - length, y, 0xFFFFFF); + y += lineHeight + 4; + + // Current Bid or Starting Price + if (listing.hasBids()) { + String bidLabel = StatCollector.translateToLocal("auction.info.currentBid") + ":"; + fontRendererObj.drawString(EnumChatFormatting.YELLOW + bidLabel, x, y, 0xFFFFFF); + + label = formatCurrency(listing.currentBid); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.GOLD + label, xRight - length, y, 0xFFFFFF); + y += lineHeight + 4; + + // Current Bidder + String bidderLabel = StatCollector.translateToLocal("auction.info.highBidder"); + fontRendererObj.drawString(EnumChatFormatting.GRAY + bidderLabel, x, y, 0xFFFFFF); + + label = listing.highBidderName != null ? listing.highBidderName : "???"; + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.WHITE + label, xRight - length, y, 0xFFFFFF); + y += lineHeight + 4; + } else { + String startLabel = StatCollector.translateToLocal("auction.info.startingPrice"); + fontRendererObj.drawString(EnumChatFormatting.YELLOW + startLabel, x, y, 0xFFFFFF); + + label = formatCurrency(listing.startingPrice); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.GOLD + label, xRight - length, y, 0xFFFFFF); + y += lineHeight + 4; + } + + // Buyout Price (if available) + if (listing.hasBuyout()) { + String buyoutLabel = StatCollector.translateToLocal("auction.info.buyoutPrice"); + fontRendererObj.drawString(EnumChatFormatting.GREEN + buyoutLabel, x, y, 0xFFFFFF); + + label = formatCurrency(listing.buyoutPrice); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.GREEN + label, xRight - length, y, 0xFFFFFF); + y += lineHeight + 4; + } + + // Time Remaining + String timeLabel = StatCollector.translateToLocal("auction.info.timeLeft"); + fontRendererObj.drawString(EnumChatFormatting.GRAY + timeLabel, x, y, 0xFFFFFF); + long timeRemaining = listing.getTimeRemaining(); + EnumChatFormatting timeColor = timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE; + + label = timeColor + formatTimeRemaining(timeRemaining); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(label, xRight - length, y, 0xFFFFFF); + y += lineHeight + 4; + + // Bid Count + String bidCountLabel = StatCollector.translateToLocal("auction.info.bidCount"); + fontRendererObj.drawString(EnumChatFormatting.GRAY + bidCountLabel, x, y, 0xFFFFFF); + + label = "" + listing.bidCount; + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.WHITE + label, xRight - length, y, 0xFFFFFF); + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + super.drawGuiContainerForegroundLayer(mouseX, mouseY); + if (hasSubGui()) return; + + // Draw button tooltips + drawBiddingPageTooltips(mouseX, mouseY); + } + + private void drawBiddingPageTooltips(int mouseX, int mouseY) { + List tooltip = null; + + if (btnBid != null && btnBid.visible && btnBid.isHovered()) { + tooltip = new ArrayList<>(); + tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("auction.bid.place")); + if (listing != null) { + long minBid = listing.getMinimumBid(AuctionClientConfig.getMinBidIncrement()); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.bid.minimum") + .replace("%s", EnumChatFormatting.WHITE + formatCurrency(minBid) + " " + AuctionClientConfig.getCurrencyName())); + } + } else if (btnBuyout != null && btnBuyout.visible && btnBuyout.isHovered()) { + tooltip = new ArrayList<>(); + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.bid.buyout")); + if (listing != null && listing.hasBuyout()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.info.buyoutPrice") + ":"); + tooltip.add(EnumChatFormatting.GREEN + formatCurrency(listing.buyoutPrice) + " " + AuctionClientConfig.getCurrencyName()); + } + } + + if (tooltip != null && !tooltip.isEmpty()) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + drawHoveringText(tooltip, mouseX - guiLeft, mouseY - guiTop, fontRendererObj); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + } + + // ========== Slot Handling ========== + + @Override + protected void handleMouseClick(Slot slot, int slotIndex, int mouseButton, int clickType) { + // Block all slot interactions - view only + } + + public AuctionListing getListing() { return listing; } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java new file mode 100644 index 000000000..7efdbe9c9 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java @@ -0,0 +1,232 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.gui.util.GuiAuctionNavButton; +import noppes.npcs.client.gui.util.GuiContainerNPCInterface; +import noppes.npcs.containers.ContainerAuction; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.util.List; + +/** + * Base GUI for all auction interfaces. + * 256x256 texture with player inventory at bottom. + */ +public abstract class GuiAuctionInterface extends GuiContainerNPCInterface { + // Textures + protected static final ResourceLocation AUCTION_BACKGROUND = new ResourceLocation("customnpcs", "textures/gui/auction/auction.png"); + protected static final ResourceLocation AUCTION_SLOT = new ResourceLocation("customnpcs", "textures/gui/auction/auction_slot.png"); + + // Navigation icons + protected static final ResourceLocation ICON_LISTINGS = new ResourceLocation("customnpcs", "textures/items/npcAncientScroll.png"); + protected static final ResourceLocation ICON_SELL = new ResourceLocation("customnpcs", "textures/gui/auction/sell.png"); + protected static final ResourceLocation ICON_CLAIMS = new ResourceLocation("customnpcs", "textures/gui/auction/my_auctions.png"); + protected static final ResourceLocation ICON_FILTER = new ResourceLocation("customnpcs", "textures/gui/auction/search.png"); + + // Page constants + public static final int PAGE_LISTINGS = 0; + public static final int PAGE_SELL = 1; + public static final int PAGE_CLAIMS = 2; + + // Navigation button IDs + protected static final int BTN_NAV_LISTINGS = 100; + protected static final int BTN_NAV_SELL = 101; + protected static final int BTN_NAV_CLAIMS = 102; + protected static final int BTN_NAV_FILTER = 103; + + // Navigation positions - non-final for testing/positioning + protected int navX = 35; + protected int navYListings = 46; + protected int navYSell = 67; + protected int navYClaims = 88; + + protected final ContainerAuction auctionContainer; + protected GuiAuctionNavButton btnListings; + protected GuiAuctionNavButton btnSell; + protected GuiAuctionNavButton btnClaims; + + public GuiAuctionInterface(EntityNPCInterface npc, ContainerAuction container) { + super(npc, container); + this.auctionContainer = container; + this.xSize = 256; + this.ySize = 256; + this.closeOnEsc = true; + this.title = ""; + } + + @Override + public void initGui() { + super.initGui(); + initNavigationButtons(); + } + + /** Initialize navigation buttons */ + protected void initNavigationButtons() { + int page = getCurrentPage(); + + btnListings = new GuiAuctionNavButton(BTN_NAV_LISTINGS, guiLeft + navX, guiTop + navYListings, + "auction.nav.listings", ICON_LISTINGS); + btnListings.setSelected(page == PAGE_LISTINGS); + addButton(btnListings); + + btnSell = new GuiAuctionNavButton(BTN_NAV_SELL, guiLeft + navX, guiTop + navYSell, + "auction.nav.sell", ICON_SELL); + btnSell.setSelected(page == PAGE_SELL); + addButton(btnSell); + + btnClaims = new GuiAuctionNavButton(BTN_NAV_CLAIMS, guiLeft + navX, guiTop + navYClaims, + "auction.nav.trades", ICON_CLAIMS); + btnClaims.setSelected(page == PAGE_CLAIMS); + addButton(btnClaims); + } + + /** Returns current page constant - implement in subclass */ + protected abstract int getCurrentPage(); + + @Override + public void actionPerformed(GuiButton button) { + super.actionPerformed(button); + int page = getCurrentPage(); + if (button.id == BTN_NAV_LISTINGS && page != PAGE_LISTINGS) { + AuctionActionPacket.openPage(PAGE_LISTINGS); + } else if (button.id == BTN_NAV_SELL && page != PAGE_SELL) { + AuctionActionPacket.openPage(PAGE_SELL); + } else if (button.id == BTN_NAV_CLAIMS && page != PAGE_CLAIMS) { + AuctionActionPacket.openPage(PAGE_CLAIMS); + } + } + + // ========== Background Drawing ========== + + @Override + protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { + drawWorldBackground(0); + + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + mc.renderEngine.bindTexture(AUCTION_BACKGROUND); + drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); + + 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); + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + if (!hasSubGui()) { + drawNavButtonTooltips(mouseX, mouseY); + } + } + + /** Draw navigation button tooltips */ + protected void drawNavButtonTooltips(int mouseX, int mouseY) { + List tooltip = null; + if (btnListings != null && btnListings.isHovered()) tooltip = btnListings.getTooltipLines(); + else if (btnSell != null && btnSell.isHovered()) tooltip = btnSell.getTooltipLines(); + else if (btnClaims != null && btnClaims.isHovered()) tooltip = btnClaims.getTooltipLines(); + + if (tooltip != null) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + drawHoveringText(tooltip, mouseX - guiLeft, mouseY - guiTop, fontRendererObj); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + } + + // ========== Slot Drawing Utilities ========== + + /** Draw auction slot background (18x18) */ + protected void drawAuctionSlot(int x, int y) { + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + mc.renderEngine.bindTexture(AUCTION_SLOT); + Gui.func_146110_a(x, y, 0, 0, 18, 18, 18, 18); + } + + /** Draw colored overlay on slot (ARGB format) */ + protected void drawColoredOverlay(int x, int y, int color) { + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + 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); + } + + /** Draw dark overlay for unavailable slots */ + protected void drawDarkenedOverlay(int x, int y) { + drawColoredOverlay(x, y, 0x80000000); + } + + /** Draw icon texture on slot (16x16) */ + protected void drawIconOverlay(int x, int y, ResourceLocation icon) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + mc.renderEngine.bindTexture(icon); + Gui.func_146110_a(x + 1, y + 1, 0, 0, 16, 16, 16, 16); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + + /** Subclasses implement this to draw specific content */ + protected abstract void drawAuctionContent(float partialTicks, int mouseX, int mouseY); + + // ========== Formatting Utilities ========== + + /** Format currency with commas (e.g., 1000 -> "1,000") */ + protected String formatCurrency(long amount) { + if (amount < 1000) return "" + amount; + StringBuilder sb = new StringBuilder(); + String str = "" + amount; + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) sb.insert(0, ','); + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString(); + } + + /** Format time remaining (e.g., "2d 5h 30m") */ + protected String formatTimeRemaining(long ms) { + if (ms <= 0) return "Ended"; + + long seconds = (ms / 1000) % 60; + long minutes = (ms / 60000) % 60; + long hours = (ms / 3600000) % 24; + long days = ms / 86400000; + + StringBuilder sb = new StringBuilder(); + if (days > 0) sb.append(days).append("d "); + if (hours > 0 || days > 0) sb.append(hours).append("h "); + if (minutes > 0 || hours > 0 || days > 0) sb.append(minutes).append("m"); + else sb.append(seconds).append("s"); + return sb.toString(); + } + + @Override + public void save() { + // Override in subclasses if needed + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java new file mode 100644 index 000000000..381747c6a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java @@ -0,0 +1,302 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.inventory.Slot; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.GuiAuctionNavButton; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.constants.EnumAuctionSort; +import noppes.npcs.containers.ContainerAuctionListing; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * GUI for browsing auction listings. + * Displays 9x5 grid with pagination and filter controls. + */ +public class GuiAuctionListing extends GuiAuctionInterface implements ISubGuiListener, IGuiData { + // Small button textures (10x10) + private static final ResourceLocation SMALL_BUTTON = new ResourceLocation("customnpcs", "textures/gui/auction/small_button.png"); + private static final ResourceLocation SMALL_BUTTON_PRESS = new ResourceLocation("customnpcs", "textures/gui/auction/small_button_press.png"); + + // Arrow icons (8x8) + private static final ResourceLocation ICON_UP = new ResourceLocation("customnpcs", "textures/gui/auction/up.png"); + private static final ResourceLocation ICON_DOWN = new ResourceLocation("customnpcs", "textures/gui/auction/down.png"); + + // Grid layout - non-final for testing/positioning + protected int gridX = 56; + protected int gridY = 46; + protected int cols = 9; + protected int rows = 5; + + // Pagination positions (side by side horizontally) + protected int pageUpX = 33; + protected int pageDownX = 45; + protected int pageBtnY = 128; + + // Filter button position + protected int navYFilter = 109; + + // Button IDs + private static final int BTN_PAGE_UP = 200; + private static final int BTN_PAGE_DOWN = 201; + + private final ContainerAuctionListing listingContainer; + private AuctionFilter filter; + private int currentPage = 0; + private int totalPages = 1; + private int totalListings = 0; + private boolean dataLoaded = false; + + // Buttons defined on this page + private GuiAuctionNavButton btnFilter; + private GuiAuctionNavButton btnPageUp; + private GuiAuctionNavButton btnPageDown; + + public GuiAuctionListing(EntityNPCInterface npc, ContainerAuctionListing container) { + super(npc, container); + this.listingContainer = container; + this.filter = new AuctionFilter(); + } + + @Override + public void initGui() { + super.initGui(); + + // Filter button (18x18 with 16x16 icon) + btnFilter = new GuiAuctionNavButton(BTN_NAV_FILTER, guiLeft + navX, guiTop + navYFilter, + "auction.nav.filter", ICON_FILTER); + addButton(btnFilter); + + // Page Up button (10x10 with 8x8 icon) + btnPageUp = new GuiAuctionNavButton(BTN_PAGE_UP, guiLeft + pageUpX, guiTop + pageBtnY, + 10, 8, "auction.page.prev", ICON_UP, SMALL_BUTTON, SMALL_BUTTON_PRESS); + addButton(btnPageUp); + + // Page Down button (10x10 with 8x8 icon) + btnPageDown = new GuiAuctionNavButton(BTN_PAGE_DOWN, guiLeft + pageDownX, guiTop + pageBtnY, + 10, 8, "auction.page.next", ICON_DOWN, SMALL_BUTTON, SMALL_BUTTON_PRESS); + addButton(btnPageDown); + + updateFilterTooltip(); + requestListings(); + } + + @Override + protected int getCurrentPage() { + return PAGE_LISTINGS; + } + + /** Request listings from server */ + private void requestListings() { + AuctionActionPacket.requestListings(filter, currentPage); + } + + /** Update pagination button visibility based on current page state */ + private void updatePaginationVisibility() { + if (btnPageUp != null) { + btnPageUp.setVisible(currentPage > 0); + } + if (btnPageDown != null) { + btnPageDown.setVisible(currentPage < totalPages - 1); + } + } + + // ========== Filter Handling ========== + + private void updateFilterTooltip() { + if (btnFilter == null) return; + + List tooltip = new ArrayList<>(); + tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("auction.nav.filter")); + tooltip.add(""); + + // Sort mode + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.filter.sort") + ":"); + tooltip.add(EnumChatFormatting.WHITE + " " + filter.getSortBy().getDisplayName()); + tooltip.add(""); + + // Search term + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.filter.search") + ":"); + if (filter.hasSearchText()) { + tooltip.add(EnumChatFormatting.GREEN + " \"" + filter.getSearchText() + "\""); + } else { + tooltip.add(EnumChatFormatting.DARK_GRAY + " " + StatCollector.translateToLocal("auction.filter.none")); + } + tooltip.add(""); + + // Instructions + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.filter.leftClick")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.filter.rightClick")); + + btnFilter.setCustomTooltip(tooltip); + } + + @Override + public void mouseEvent(int mouseX, int mouseY, int mouseButton) { + if (btnFilter != null && btnFilter.isMouseOver(mouseX, mouseY) && mouseButton == 1) { + cycleSortMode(); + } + } + + @Override + public void actionPerformed(GuiButton button) { + super.actionPerformed(button); + + switch (button.id) { + case BTN_NAV_FILTER: + setSubGui(new SubGuiAuctionSearch(filter.getSearchText())); + break; + case BTN_PAGE_UP: + if (currentPage > 0) { + currentPage--; + requestListings(); + NoppesUtil.clickSound(); + } + break; + case BTN_PAGE_DOWN: + if (currentPage < totalPages - 1) { + currentPage++; + requestListings(); + NoppesUtil.clickSound(); + } + break; + } + } + + private void cycleSortMode() { + EnumAuctionSort[] values = EnumAuctionSort.values(); + int next = (filter.getSortBy().ordinal() + 1) % values.length; + filter.setSortBy(values[next]); + updateFilterTooltip(); + currentPage = 0; + requestListings(); + NoppesUtil.clickSound(); + } + + public void onSearchSubmit(String searchText) { + filter.setSearchText(searchText); + updateFilterTooltip(); + currentPage = 0; + requestListings(); + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) {} + + // ========== Data Handling ========== + + @Override + public void setGuiData(NBTTagCompound compound) { + if (compound.hasKey("ListingsData")) { + currentPage = compound.getInteger("Page"); + totalPages = compound.getInteger("TotalPages"); + totalListings = compound.getInteger("TotalListings"); + + // Load listings + listingContainer.displayInventory.clear(); + NBTTagList list = compound.getTagList("Listings", 10); + for (int i = 0; i < list.tagCount() && i < 45; i++) { + AuctionListing listing = AuctionListing.fromNBT(list.getCompoundTagAt(i)); + listingContainer.displayInventory.setListing(i, listing); + } + dataLoaded = true; + } else if (compound.hasKey("ListingsRefresh")) { + requestListings(); + } + } + + // ========== Drawing ========== + + @Override + protected void drawAuctionContent(float partialTicks, int mouseX, int mouseY) { + // Draw slot backgrounds + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + int x = guiLeft + gridX + col * 18; + int y = guiTop + gridY + row * 18; + drawAuctionSlot(x, y); + } + } + + // Draw page indicator below pagination buttons + drawPageIndicator(); + } + + private void drawPageIndicator() { + if (totalPages > 1) { + String text = (currentPage + 1) + "/" + totalPages; + // Center between the two buttons (horizontally centered), below buttons + int centerX = guiLeft + pageUpX + 11; + int x = centerX - fontRendererObj.getStringWidth(text) / 2; + fontRendererObj.drawString(text, x, guiTop + pageBtnY + 12, 0xFFFFFF); + } + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + super.drawGuiContainerForegroundLayer(mouseX, mouseY); + if (hasSubGui()) return; + + // Draw tooltips for buttons on this page + drawListingPageTooltips(mouseX, mouseY); + } + + private void drawListingPageTooltips(int mouseX, int mouseY) { + List tooltip = null; + + // Filter button tooltip + if (btnFilter != null && btnFilter.isHovered()) { + tooltip = btnFilter.getTooltipLines(); + } + // Page up tooltip + else if (btnPageUp != null && btnPageUp.visible && btnPageUp.isHovered()) { + tooltip = btnPageUp.getTooltipLines(); + } + // Page down tooltip + else if (btnPageDown != null && btnPageDown.visible && btnPageDown.isHovered()) { + tooltip = btnPageDown.getTooltipLines(); + } + + if (tooltip != null && !tooltip.isEmpty()) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + drawHoveringText(tooltip, mouseX - guiLeft, mouseY - guiTop, fontRendererObj); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + } + + // ========== Slot Handling ========== + + @Override + protected void handleMouseClick(Slot slot, int slotIndex, int mouseButton, int clickType) { + // Check if clicking on a display slot with a listing + if (slot != null && listingContainer.isDisplaySlot(slot.slotNumber)) { + int listingIndex = slot.slotNumber - listingContainer.getDisplaySlotStart(); + AuctionListing listing = listingContainer.displayInventory.getListing(listingIndex); + if (listing != null) { + // Open bidding page for this listing + AuctionActionPacket.openBidding(listing.id); + NoppesUtil.clickSound(); + } + } + // Block other slot interactions + } + + public AuctionFilter getFilter() { return filter; } + public ContainerAuctionListing getListingContainer() { return listingContainer; } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java new file mode 100644 index 000000000..a8784b3b0 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java @@ -0,0 +1,332 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.GuiAuctionNavButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.containers.ContainerAuctionSell; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.ArrayList; +import java.util.List; + +/** + * GUI for creating auction listings. + * Sell slot interactions: left-click=clear, right-click=remove 1. + * Inventory interactions: left-click=add stack, right-click=add 1. + */ +public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldListener { + private static final ResourceLocation ICON_COIN = new ResourceLocation("customnpcs", "textures/items/npcCoinBronze.png"); + private static final ResourceLocation ICON_BAG = new ResourceLocation("customnpcs", "textures/items/npcBag.png"); + + // Layout + private int contentX = 58; + private int contentY = 50; + private int confirmBtnX = 200; + private int confirmBtnY = 118; + private int navYBuyout = 109; + + // Component IDs + private int txtStartingPriceId = 1; + private int txtBuyoutPriceId = 2; + private int btnConfirmId = 10; + private int btnAllowBuyoutId = 11; + + private final ContainerAuctionSell sellContainer; + private long startingPrice = 0; + private long buyoutPrice = 0; + private boolean allowBuyout = true; + private String errorMessage = null; + + // Buttons + private GuiAuctionNavButton btnAllowBuyout; + private GuiNpcLabel lblBuyout; + private GuiNpcTextField txtBuyout; + + public GuiAuctionSell(EntityNPCInterface npc, ContainerAuctionSell container) { + super(npc, container); + this.sellContainer = container; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + contentY; + + // Starting Price label and field + addLabel(new GuiNpcLabel(1, "auction.sell.startingPrice", guiLeft + contentX, y + 5, 0xFFFFFF)); + GuiNpcTextField startingField = new GuiNpcTextField(txtStartingPriceId, this, fontRendererObj, + guiLeft + contentX + 80, y, 80, 18, startingPrice > 0 ? "" + startingPrice : ""); + startingField.setIntegersOnly(); + addTextField(startingField); + + y += 25; + + // Buyout Price label and field (only visible when allowBuyout is true) + lblBuyout = new GuiNpcLabel(2, "auction.sell.buyoutPrice", guiLeft + contentX, y + 5, 0xFFFFFF); + addLabel(lblBuyout); + + txtBuyout = new GuiNpcTextField(txtBuyoutPriceId, this, fontRendererObj, + guiLeft + contentX + 80, y, 80, 18, buyoutPrice > 0 ? "" + buyoutPrice : ""); + txtBuyout.setIntegersOnly(); + addTextField(txtBuyout); + + // Allow Buyout toggle button (18x18 with bag icon) + btnAllowBuyout = new GuiAuctionNavButton(btnAllowBuyoutId, guiLeft + navX, guiTop + navYBuyout, + "auction.sell.allowBuyout", ICON_BAG); + btnAllowBuyout.setToggle(true); // This is a toggle, not a nav button + btnAllowBuyout.setSelected(allowBuyout); + addButton(btnAllowBuyout); + + // Confirm button (custom textured button - using GuiAuctionNavButton) + GuiAuctionNavButton btnConfirm = new GuiAuctionNavButton(btnConfirmId, guiLeft + confirmBtnX - 20, guiTop + confirmBtnY - 10, + "auction.sell.confirm", ICON_COIN); + addButton(btnConfirm); + + updateBuyoutVisibility(); + updateBuyoutTooltip(); + } + + @Override + protected int getCurrentPage() { + return PAGE_SELL; + } + + /** Update visibility of buyout price field based on toggle state */ + private void updateBuyoutVisibility() { + if (lblBuyout != null) { + lblBuyout.enabled = allowBuyout; + } + if (txtBuyout != null) { + txtBuyout.setVisible(allowBuyout); + if (!allowBuyout) { + txtBuyout.setText(""); + buyoutPrice = 0; + } + } + } + + /** Update the buyout toggle button tooltip */ + private void updateBuyoutTooltip() { + if (btnAllowBuyout == null) return; + + List tooltip = new ArrayList<>(); + tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("auction.sell.allowBuyout")); + tooltip.add(""); + if (allowBuyout) { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.sell.buyoutEnabled")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.buyoutEnabledDesc")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.buyoutEnabledDesc2")); + } else { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.sell.buyoutDisabled")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.buyoutDisabledDesc")); + } + tooltip.add(""); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.clickToToggle")); + + btnAllowBuyout.setCustomTooltip(tooltip); + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + if (textfield.id == txtStartingPriceId) { + try { + startingPrice = Long.parseLong(textfield.getText()); + } catch (NumberFormatException e) { + startingPrice = 0; + } + } else if (textfield.id == txtBuyoutPriceId) { + try { + String text = textfield.getText().trim(); + buyoutPrice = text.isEmpty() ? 0 : Long.parseLong(text); + } catch (NumberFormatException e) { + buyoutPrice = 0; + } + } + errorMessage = null; + } + + @Override + public void actionPerformed(GuiButton button) { + super.actionPerformed(button); + + if (button.id == btnConfirmId) { + createListing(); + } else if (button.id == btnAllowBuyoutId) { + allowBuyout = !allowBuyout; + btnAllowBuyout.setSelected(allowBuyout); + updateBuyoutVisibility(); + updateBuyoutTooltip(); + NoppesUtil.clickSound(); + } + } + + private void createListing() { + // Get latest values from text fields + GuiNpcTextField startingField = getTextField(txtStartingPriceId); + GuiNpcTextField buyoutField = getTextField(txtBuyoutPriceId); + + if (startingField != null) { + try { + startingPrice = Long.parseLong(startingField.getText()); + } catch (NumberFormatException e) { + startingPrice = 0; + } + } + + if (buyoutField != null && allowBuyout) { + try { + String text = buyoutField.getText().trim(); + buyoutPrice = text.isEmpty() ? 0 : Long.parseLong(text); + } catch (NumberFormatException e) { + buyoutPrice = 0; + } + } else { + buyoutPrice = 0; + } + + // Validate + ItemStack item = sellContainer.getItemToSell(); + if (item == null) { + errorMessage = StatCollector.translateToLocal("auction.sell.noItem"); + return; + } + + if (startingPrice <= 0) { + errorMessage = StatCollector.translateToLocal("auction.sell.invalidPrice"); + return; + } + + if (allowBuyout && buyoutPrice > 0 && buyoutPrice < startingPrice) { + errorMessage = StatCollector.translateToLocal("auction.sell.buyoutTooLow"); + return; + } + + // Validate player has enough items + int inventoryCount = sellContainer.countItemInInventory(item); + if (item.stackSize > inventoryCount) { + errorMessage = StatCollector.translateToLocal("auction.sell.notEnoughItems"); + return; + } + + // Send create listing packet + AuctionActionPacket.createListing(item, startingPrice, allowBuyout ? buyoutPrice : 0); + + // Clear the sell slot (server will also clear it) + sellContainer.clearSellSlot(); + + // Navigate back to listings page + AuctionActionPacket.openPage(PAGE_LISTINGS); + } + + @Override + protected void drawAuctionContent(float partialTicks, int mouseX, int mouseY) { + // Draw sell slot background (centered in content area) + int slotX = guiLeft + contentX; + int slotY = guiTop + contentY + 50; + drawAuctionSlot(slotX, slotY); + + // Draw listing fee info (uses cached config from server) + String feeText = StatCollector.translateToLocal("auction.sell.fee") + .replace("%s", formatCurrency(AuctionClientConfig.getListingFee()) + " " + AuctionClientConfig.getCurrencyName()); + fontRendererObj.drawString(EnumChatFormatting.GRAY + feeText, guiLeft + contentX, guiTop + contentY + 77, 0xFFFFFF); + + // Draw error message if any + if (errorMessage != null) { + fontRendererObj.drawString(EnumChatFormatting.RED + errorMessage, guiLeft + contentX, guiTop + contentY + 95, 0xFFFFFF); + } + + drawSellSlotIndicator(slotX, slotY); + } + + /** Draw indicator showing staged vs available items */ + private void drawSellSlotIndicator(int slotX, int slotY) { + ItemStack sellItem = sellContainer.getItemToSell(); + if (sellItem != null) { + int inventoryCount = sellContainer.countItemInInventory(sellItem); + String countText = sellItem.stackSize + "/" + inventoryCount; + + // Color: green if valid, red if over limit + int color = sellItem.stackSize <= inventoryCount ? 0x55FF55 : 0xFF5555; + fontRendererObj.drawStringWithShadow(countText, slotX + 18 + 4, slotY + 5, color); + } else { + // Draw "Place item here" hint + String hint = StatCollector.translateToLocal("auction.sell.placeItem"); + fontRendererObj.drawString(EnumChatFormatting.GRAY + hint, slotX + 20, slotY + 5, 0xFFFFFF); + } + } + + /** + * Handle sell page click behavior. + * Sell slot: left=clear, right=remove 1. + * Inventory: left=add stack, right=add 1. + */ + @Override + protected void handleMouseClick(Slot slot, int slotIndex, int mouseButton, int clickType) { + if (slot == null) return; + + // Sell slot clicks + if (sellContainer.isSellSlot(slotIndex)) { + ItemStack sellItem = sellContainer.getItemToSell(); + if (sellItem != null) { + boolean removeAll = (mouseButton == 0); // Left=clear, Right=remove 1 + sellContainer.removeFromSellSlot(removeAll); + NoppesUtil.clickSound(); + } + return; + } + + // Inventory clicks + if (sellContainer.isPlayerInventorySlot(slotIndex)) { + ItemStack sourceStack = slot.getStack(); + if (sourceStack != null) { + boolean fullStack = (mouseButton == 0); // Left=stack, Right=1 item + sellContainer.addToSellSlot(slotIndex, fullStack); + NoppesUtil.clickSound(); + } + return; + } + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + super.drawGuiContainerForegroundLayer(mouseX, mouseY); + if (hasSubGui()) return; + + // Draw tooltips for buttons (icons, not items - item tooltips handled by AuctionTooltipHandler) + drawSellPageTooltips(mouseX, mouseY); + } + + /** Draw tooltips for sell page buttons */ + private void drawSellPageTooltips(int mouseX, int mouseY) { + List tooltip = null; + + // Check all buttons + GuiNpcButton confirmBtn = getButton(btnConfirmId); + if (confirmBtn instanceof GuiAuctionNavButton) { + GuiAuctionNavButton btn = (GuiAuctionNavButton) confirmBtn; + if (btn.isHovered()) { + tooltip = btn.getTooltipLines(); + } + } + + if (btnAllowBuyout != null && btnAllowBuyout.isHovered()) { + tooltip = btnAllowBuyout.getTooltipLines(); + } + + if (tooltip != null && !tooltip.isEmpty()) { + drawHoveringText(tooltip, mouseX - guiLeft, mouseY - guiTop, fontRendererObj); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java new file mode 100644 index 000000000..21b0f6f01 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java @@ -0,0 +1,388 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.inventory.Slot; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.containers.ContainerAuctionTrades; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * GUI for viewing player's auction trades. + * Shows active listings, bids, and claims with confirmation system. + */ +public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { + // Icon textures + private static final ResourceLocation ICON_X = new ResourceLocation("customnpcs", "textures/gui/auction/x_icon.png"); + private static final ResourceLocation ICON_CHECK = new ResourceLocation("customnpcs", "textures/gui/auction/check_icon.png"); + private static final ResourceLocation ICON_COIN = new ResourceLocation("customnpcs", "textures/items/npcCoinGold.png"); + + // Grid layout + private static final int GRID_X = 56; + private static final int GRID_Y = 46; + private static final int COLS = 9; + private static final int ROWS = 5; + 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 final ContainerAuctionTrades tradesContainer; + private int maxTradeSlots; + + // Pending operation state + private int pendingSlot = -1; + private PendingOp pendingOp = PendingOp.NONE; + + private enum PendingOp { NONE, CANCEL, CLAIM } + + public GuiAuctionTrades(EntityNPCInterface npc, ContainerAuctionTrades container) { + super(npc, container); + this.tradesContainer = container; + // Initial value from client config, updated when server sends trades data + this.maxTradeSlots = AuctionClientConfig.getMaxActiveListings(); + } + + @Override + protected int getCurrentPage() { + return PAGE_CLAIMS; + } + + // ========== Mouse Handling ========== + + @Override + public void mouseEvent(int mouseX, int mouseY, int mouseButton) { + if (hasSubGui()) return; + + int slot = getSlotAt(mouseX, mouseY); + if (slot < 0 || slot >= TOTAL_SLOTS) { + clearPending(); + return; + } + + // Clicking on pending slot - confirm or cancel + if (slot == pendingSlot && pendingOp != PendingOp.NONE) { + handlePendingClick(mouseButton); + return; + } + + // Clear existing pending, start new operation + clearPending(); + + AuctionListing listing = tradesContainer.getListingAt(slot); + AuctionClaim claim = tradesContainer.getClaimAt(slot); + + if (listing != null && tradesContainer.isSellingAt(slot) && mouseButton == 1) { + // Right-click on own listing = show cancel + setPending(slot, PendingOp.CANCEL); + NoppesUtil.clickSound(); + } else if (listing != null && tradesContainer.isBiddingAt(slot) && mouseButton == 0) { + // Left-click on active bid = open bidding GUI to increase bid + AuctionActionPacket.openBidding(listing.id); + NoppesUtil.clickSound(); + } else if (claim != null) { + // Handle claim clicks + if (claim.type == EnumClaimType.REFUND && claim.item != null) { + // REFUND with item = can rebid + if (mouseButton == 0) { + // Left-click = open bidding to rebid + AuctionActionPacket.openBidding(claim.listingId); + NoppesUtil.clickSound(); + } else if (mouseButton == 1) { + // Right-click = show refund claim confirmation + setPending(slot, PendingOp.CLAIM); + NoppesUtil.clickSound(); + } + } else if (mouseButton == 0) { + // Normal claim - left-click shows claim confirmation + setPending(slot, PendingOp.CLAIM); + NoppesUtil.clickSound(); + } + } + } + + /** Handle click on pending operation slot */ + private void handlePendingClick(int mouseButton) { + if (pendingOp == PendingOp.CANCEL && mouseButton == 1) { + // Right-click confirms cancel + AuctionListing listing = tradesContainer.getListingAt(pendingSlot); + if (listing != null) { + AuctionActionPacket.cancelListing(listing.id); + playConfirmSound(); + } + clearPending(); + } else if (pendingOp == PendingOp.CLAIM) { + AuctionClaim claim = tradesContainer.getClaimAt(pendingSlot); + if (claim != null) { + // For REFUND claims with item (rebid option), right-click confirms refund + // For all others, left-click confirms claim + boolean shouldClaim = (claim.type == EnumClaimType.REFUND && claim.item != null) + ? mouseButton == 1 // Right-click for rebiddable refunds + : mouseButton == 0; // Left-click for normal claims + + if (shouldClaim) { + if (claim.type.isItem()) { + AuctionActionPacket.claimItem(claim.id); + } else { + AuctionActionPacket.claimCurrency(claim.id); + } + playConfirmSound(); + clearPending(); + } else { + // Wrong button - cancel operation + clearPending(); + NoppesUtil.clickSound(); + } + } else { + clearPending(); + } + } else { + // Wrong button - cancel operation + clearPending(); + NoppesUtil.clickSound(); + } + } + + /** Set pending operation and hide slot item */ + private void setPending(int slot, PendingOp op) { + pendingSlot = slot; + pendingOp = op; + tradesContainer.setHiddenSlot(slot); + } + + /** Clear pending operation and restore slot */ + private void clearPending() { + if (pendingSlot >= 0) { + tradesContainer.clearHiddenSlot(); + } + pendingSlot = -1; + pendingOp = PendingOp.NONE; + } + + /** Play confirmation sound */ + private void playConfirmSound() { + mc.getSoundHandler().playSound(PositionedSoundRecord.func_147674_a( + new ResourceLocation("random.orb"), 1.0F)); + } + + /** Get slot index at mouse position */ + private int getSlotAt(int mouseX, int mouseY) { + int relX = mouseX - guiLeft - GRID_X; + int relY = mouseY - guiTop - GRID_Y; + if (relX < 0 || relY < 0) return -1; + + int col = relX / 18; + int row = relY / 18; + if (col >= COLS || row >= ROWS) return -1; + + return col + row * COLS; + } + + // ========== Drawing ========== + + @Override + protected void drawAuctionContent(float partialTicks, int mouseX, int mouseY) { + for (int row = 0; row < ROWS; row++) { + for (int col = 0; col < COLS; col++) { + int slot = col + row * COLS; + int x = guiLeft + GRID_X + col * 18; + int y = guiTop + GRID_Y + row * 18; + + drawAuctionSlot(x, y); + + // Color tint based on slot contents + int tint = getSlotTint(slot); + if (tint != 0 && slot != pendingSlot) { + drawColoredOverlay(x, y, tint); + } + + // Darken unavailable slots + if (slot >= maxTradeSlots) { + drawDarkenedOverlay(x, y); + } + + // Pending operation icon or currency claim coin + if (slot == pendingSlot) { + if (pendingOp == PendingOp.CANCEL) { + drawIconOverlay(x, y, ICON_X); + } else if (pendingOp == PendingOp.CLAIM) { + drawIconOverlay(x, y, ICON_CHECK); + } + } else { + // Draw coin icon for currency claims or refund claims without item + AuctionClaim claim = tradesContainer.getClaimAt(slot); + if (claim != null) { + // Show coin if: CURRENCY claim OR REFUND claim without item + if (claim.type == EnumClaimType.CURRENCY || + (claim.type == EnumClaimType.REFUND && claim.item == null)) { + drawIconOverlay(x, y, ICON_COIN); + } + } + } + } + } + } + + /** Get tint color for slot based on contents */ + private int getSlotTint(int slot) { + AuctionListing listing = tradesContainer.getListingAt(slot); + AuctionClaim claim = tradesContainer.getClaimAt(slot); + + if (listing != null) { + if (tradesContainer.isSellingAt(slot)) { + // Selling - Blue + return TINT_BLUE; + } else { + // Bidding - Yellow + return TINT_YELLOW; + } + } else if (claim != null) { + switch (claim.type) { + case CURRENCY: + // Sold item - Green + return TINT_GREEN; + case REFUND: + // Outbid - Red (whether rebiddable or not) + return TINT_RED; + case ITEM: + // Won item (green) or Returned/Expired (red) + return claim.isReturned ? TINT_RED : TINT_GREEN; + default: + return TINT_GREEN; + } + } + return 0; + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + super.drawGuiContainerForegroundLayer(mouseX, mouseY); + if (hasSubGui()) return; + + int slot = getSlotAt(mouseX, mouseY); + if (slot >= 0 && slot < TOTAL_SLOTS) { + drawTradeTooltip(slot, mouseX - guiLeft, mouseY - guiTop); + } + } + + /** Draw tooltip for a trade slot */ + private void drawTradeTooltip(int slot, int x, int y) { + List tooltip = new ArrayList<>(); + + if (slot == pendingSlot) { + // Pending operation tooltip + if (pendingOp == PendingOp.CANCEL) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.confirmCancel")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.rightClickConfirm")); + tooltip.add(EnumChatFormatting.DARK_GRAY + StatCollector.translateToLocal("auction.trades.otherClickCancel")); + } else if (pendingOp == PendingOp.CLAIM) { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.confirmClaim")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.leftClickConfirm")); + tooltip.add(EnumChatFormatting.DARK_GRAY + StatCollector.translateToLocal("auction.trades.otherClickCancel")); + } + } else { + // Normal slot tooltip + AuctionListing listing = tradesContainer.getListingAt(slot); + AuctionClaim claim = tradesContainer.getClaimAt(slot); + + if (listing != null) { + if (tradesContainer.isSellingAt(slot)) { + tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("auction.trades.activeListing")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickToCancel")); + } else if (tradesContainer.isBiddingAt(slot)) { + tooltip.add(EnumChatFormatting.AQUA + StatCollector.translateToLocal("auction.trades.activeBid")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.winningBid")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.leftClickToIncrease")); + } + } else if (claim != null) { + switch (claim.type) { + case CURRENCY: + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.soldClaim")); + if (!claim.itemName.isEmpty()) { + tooltip.add(EnumChatFormatting.WHITE + claim.itemName); + } + if (!claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.buyer") + ": " + EnumChatFormatting.AQUA + claim.otherPlayerName); + } + tooltip.add(EnumChatFormatting.GOLD + String.format("%,d", claim.currency) + " " + AuctionClientConfig.getCurrencyName()); + break; + case REFUND: + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.outbid")); + if (!claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.outbidBy") + ": " + EnumChatFormatting.AQUA + claim.otherPlayerName); + } + tooltip.add(EnumChatFormatting.GOLD + String.format("%,d", claim.currency) + " " + AuctionClientConfig.getCurrencyName()); + // Show rebid option if item is present (active listing) + if (claim.item != null) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.leftClickRebid")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickRefund")); + return; // Skip the generic claim message + } + break; + case ITEM: + if (claim.isReturned) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.expiredClaim")); + } else { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.wonClaim")); + } + break; + } + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.leftClickToClaim")); + } else if (slot >= maxTradeSlots) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.unavailable")); + } + } + + if (!tooltip.isEmpty()) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + drawHoveringText(tooltip, x, y, fontRendererObj); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + } + + // ========== Slot Handling ========== + + @Override + protected void handleMouseClick(Slot slot, int slotIndex, int mouseButton, int clickType) { + // Block all slot interactions + } + + // ========== Server Updates ========== + + @Override + public void setGuiData(NBTTagCompound compound) { + if (compound.hasKey("TradesData")) { + // Full trades data received from server + tradesContainer.setTradesData(compound); + // Update max trade slots from server (permission-aware) + this.maxTradeSlots = tradesContainer.getMaxTradeSlots(); + clearPending(); + } else if (compound.hasKey("TradesUpdate")) { + // Legacy flag - should not happen now but keep for safety + tradesContainer.refreshData(); + } + } + + public ContainerAuctionTrades getTradesContainer() { + return tradesContainer; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java index f11bbb2e6..b0a1c6c32 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java @@ -172,7 +172,7 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { // Draw stock count to the RIGHT of the trade (after output slot) if (stockEnabled && availableStock[slotIdx] < Integer.MAX_VALUE) { - String stockText = String.valueOf(availableStock[slotIdx]); + String stockText = "" + availableStock[slotIdx]; int stockColor = availableStock[slotIdx] > 0 ? 0x00AA00 : 0xAA0000; fontRendererObj.drawString(stockText, x + OUTPUT_OFFSET + 20, y + 5, stockColor); } @@ -317,10 +317,10 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { */ private String formatCurrency(long amount) { if (amount < 1000) { - return String.valueOf(amount); + return "" + amount; } StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java new file mode 100644 index 000000000..a1793c545 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -0,0 +1,160 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * Sub GUI for placing a bid on an auction. + * Shows minimum bid, player balance, and input field. + */ +public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListener { + private int txtBidId = 1; + private int btnBidId = 10; + private int btnCancelId = 11; + + private final String listingId; + private final long minimumBid; + private final long playerBalance; + private long bidAmount; + private boolean successful = false; + private String errorMessage = null; + + public SubGuiAuctionBid(String listingId, long minimumBid, long playerBalance) { + this.listingId = listingId; + this.minimumBid = minimumBid; + this.playerBalance = playerBalance; + this.bidAmount = minimumBid; + + setBackground("menubg.png"); + xSize = 200; + ySize = 120; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int centerX = guiLeft + xSize / 2; + int y = guiTop + 15; + String currencyName = AuctionClientConfig.getCurrencyName(); + + // Minimum bid info + String minText = StatCollector.translateToLocal("auction.bid.minimum") + .replace("%s", EnumChatFormatting.DARK_RED + formatCurrency(minimumBid) + " " + currencyName); + addLabel(new GuiNpcLabel(1, minText, guiLeft + 15, y)); + y += 14; + + // Your balance + String balanceText = StatCollector.translateToLocal("auction.bid.yourBalance") + .replace("%s", EnumChatFormatting.DARK_GREEN + formatCurrency(playerBalance) + " " + currencyName); + addLabel(new GuiNpcLabel(2, balanceText, guiLeft + 15, y)); + y += 18; + + // Bid amount field + addLabel(new GuiNpcLabel(3, "auction.bid.yourBid", guiLeft + 15, y + 4)); + GuiNpcTextField bidField = new GuiNpcTextField(txtBidId, this, fontRendererObj, + guiLeft + 80, y, 100, 18, "" + minimumBid); + bidField.setIntegersOnly(); + addTextField(bidField); + y += 28; + + // Error message area (empty initially) + addLabel(new GuiNpcLabel(4, "", guiLeft + 15, y, 0xFF5555)); + y += 16; + + // Buttons + int btnWidth = 70; + int btnSpacing = 20; + int totalBtnWidth = btnWidth * 2 + btnSpacing; + int btnX = centerX - totalBtnWidth / 2; + + addButton(new GuiNpcButton(btnBidId, btnX, y, btnWidth, 20, "auction.bid.placeBid")); + addButton(new GuiNpcButton(btnCancelId, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + if (textfield.id == txtBidId) { + try { + bidAmount = Long.parseLong(textfield.getText()); + } catch (NumberFormatException e) { + bidAmount = 0; + } + errorMessage = null; + updateErrorLabel(); + } + } + + @Override + public void buttonEvent(GuiButton button) { + if (button.id == btnBidId) { + placeBid(); + } else if (button.id == btnCancelId) { + close(); + } + } + + private void placeBid() { + // Get latest value from text field + GuiNpcTextField bidField = getTextField(txtBidId); + if (bidField != null) { + try { + bidAmount = Long.parseLong(bidField.getText()); + } catch (NumberFormatException e) { + bidAmount = 0; + } + } + + // Validate + if (bidAmount < minimumBid) { + errorMessage = StatCollector.translateToLocal("auction.error.bidTooLow") + .replace("%s", formatCurrency(minimumBid)); + updateErrorLabel(); + return; + } + + if (bidAmount > playerBalance) { + errorMessage = StatCollector.translateToLocal("auction.error.notEnoughCurrency"); + updateErrorLabel(); + return; + } + + // Send bid packet + AuctionActionPacket.placeBid(listingId, bidAmount); + successful = true; + close(); + } + + private void updateErrorLabel() { + GuiNpcLabel errorLabel = getLabel(4); + if (errorLabel != null) { + errorLabel.label = errorMessage != null ? EnumChatFormatting.RED + errorMessage : ""; + } + } + + public boolean wasSuccessful() { + return successful; + } + + private String formatCurrency(long amount) { + if (amount < 1000) return "" + amount; + StringBuilder sb = new StringBuilder(); + String str = "" + amount; + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) sb.insert(0, ','); + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java new file mode 100644 index 000000000..b63471047 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java @@ -0,0 +1,121 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * Sub GUI for confirming a buyout purchase. + * Shows buyout price, player balance, and confirmation buttons. + */ +public class SubGuiAuctionBuyNow extends SubGuiInterface { + private int btnConfirmId = 10; + private int btnCancelId = 11; + + private final String listingId; + private final long buyoutPrice; + private final long playerBalance; + private boolean successful = false; + + public SubGuiAuctionBuyNow(String listingId, long buyoutPrice, long playerBalance) { + this.listingId = listingId; + this.buyoutPrice = buyoutPrice; + this.playerBalance = playerBalance; + + setBackground("menubg.png"); + xSize = 220; + ySize = 130; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int centerX = guiLeft + xSize / 2; + int y = guiTop + 15; + String currencyName = AuctionClientConfig.getCurrencyName(); + + // Title + String title = StatCollector.translateToLocal("auction.buyout.title"); + int titleWidth = fontRendererObj.getStringWidth(title); + addLabel(new GuiNpcLabel(0, title, centerX - titleWidth / 2, y)); + y += 20; + + // Confirmation message + String confirmMsg = StatCollector.translateToLocal("auction.buyout.confirm"); + int confirmWidth = fontRendererObj.getStringWidth(confirmMsg); + addLabel(new GuiNpcLabel(1, confirmMsg, centerX - confirmWidth / 2, y)); + y += 16; + + // Buyout price + String priceText = EnumChatFormatting.DARK_RED + formatCurrency(buyoutPrice) + " " + currencyName; + int priceWidth = fontRendererObj.getStringWidth(priceText); + addLabel(new GuiNpcLabel(2, priceText, centerX - priceWidth / 2, y)); + y += 20; + + // Your balance + String balanceLabel = StatCollector.translateToLocal("auction.bid.yourBalance"); + String balanceText = balanceLabel.replace("%s", EnumChatFormatting.DARK_GREEN + formatCurrency(playerBalance) + " " + currencyName); + addLabel(new GuiNpcLabel(3, balanceText, guiLeft + 20, y)); + y += 14; + + // Insufficient funds warning + if (playerBalance < buyoutPrice) { + String warning = StatCollector.translateToLocal("auction.error.notEnoughCurrency"); + addLabel(new GuiNpcLabel(4, EnumChatFormatting.RED + warning, guiLeft + 20, y, 0xFFFFFF)); + } + y += 20; + + // Buttons + int btnWidth = 70; + int btnSpacing = 20; + int totalBtnWidth = btnWidth * 2 + btnSpacing; + int btnX = centerX - totalBtnWidth / 2; + + GuiNpcButton confirmBtn = new GuiNpcButton(btnConfirmId, btnX, y, btnWidth, 20, "gui.yes"); + confirmBtn.enabled = playerBalance >= buyoutPrice; + addButton(confirmBtn); + addButton(new GuiNpcButton(btnCancelId, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton button) { + if (button.id == btnConfirmId) { + if (playerBalance >= buyoutPrice) { + confirmPurchase(); + } + } else if (button.id == btnCancelId) { + close(); + } + } + + private void confirmPurchase() { + // Send buyout packet + AuctionActionPacket.buyout(listingId); + successful = true; + close(); + } + + public boolean wasSuccessful() { + return successful; + } + + private String formatCurrency(long amount) { + if (amount < 1000) return "" + amount; + StringBuilder sb = new StringBuilder(); + String str = "" + amount; + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) sb.insert(0, ','); + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionSearch.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionSearch.java new file mode 100644 index 000000000..57d52f95d --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionSearch.java @@ -0,0 +1,85 @@ +package noppes.npcs.client.gui.player; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * Small SubGui for entering auction search text. + */ +public class SubGuiAuctionSearch extends SubGuiInterface implements ITextfieldListener { + private static final int TXT_SEARCH = 0; + private static final int BTN_SUBMIT = 1; + private static final int BTN_CLEAR = 2; + private static final int BTN_CLOSE = 3; + + private String searchText; + + public SubGuiAuctionSearch(String currentSearchText) { + this.searchText = currentSearchText != null ? currentSearchText : ""; + setBackground("menubg.png"); + xSize = 180; + ySize = 80; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + // Close button + addButton(new GuiNpcButton(BTN_CLOSE, guiLeft + xSize - 22, guiTop + 4, 18, 18, "X")); + + // Search label + addLabel(new GuiNpcLabel(0, "auction.filter.search", guiLeft + 10, guiTop + 10, 0xFFFFFF)); + + // Search text field + addTextField(new GuiNpcTextField(TXT_SEARCH, this, fontRendererObj, guiLeft + 10, guiTop + 25, 160, 18, searchText)); + + // Submit and Clear buttons + addButton(new GuiNpcButton(BTN_SUBMIT, guiLeft + 10, guiTop + 50, 70, 20, "gui.apply")); + addButton(new GuiNpcButton(BTN_CLEAR, guiLeft + 85, guiTop + 50, 70, 20, "gui.clear")); + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + if (textfield.id == TXT_SEARCH) { + searchText = textfield.getText(); + } + } + + @Override + protected void actionPerformed(GuiButton guibutton) { + if (guibutton.id == BTN_SUBMIT) { + GuiNpcTextField field = getTextField(TXT_SEARCH); + if (field != null) { + searchText = field.getText(); + } + // Notify parent and close + if (parent instanceof GuiAuctionListing) { + ((GuiAuctionListing) parent).onSearchSubmit(searchText); + } + close(); + } else if (guibutton.id == BTN_CLEAR) { + searchText = ""; + GuiNpcTextField field = getTextField(TXT_SEARCH); + if (field != null) { + field.setText(""); + } + // Notify parent and close + if (parent instanceof GuiAuctionListing) { + ((GuiAuctionListing) parent).onSearchSubmit(""); + } + close(); + } else if (guibutton.id == BTN_CLOSE) { + close(); + } + } + + public String getSearchText() { + return searchText; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java index 7ffb7aff2..f9e627fe7 100644 --- a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java @@ -180,7 +180,7 @@ public class GuiProfiles extends GuiCNPCInventory implements ISubGuiListener, IC if (slot.isTemporary()) { label = StatCollector.translateToLocal("profile.temporary"); - result = (String.valueOf(slot.isTemporary())).toUpperCase(); + result = ("" + slot.isTemporary()).toUpperCase(); labelColor = 0xfc60dd; resultColor = 0x35fc81; fontRendererObj.drawString(label, xLabel, y, labelColor, false); diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java index 6c4745d42..7b54518e3 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java @@ -56,7 +56,7 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex int x = guiLeft + i % 3 * 130 + 15; // First position after "$" label int y = guiTop + i / 3 * 22 + 14; long cost = role.getCurrencyCost(i); - GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? String.valueOf(cost) : ""); + GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? "" + cost : ""); field.setMaxStringLength(8); field.setIntegersOnly(); addTextField(field); diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java index 2e369b7e6..4e88c9b21 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java @@ -95,7 +95,7 @@ public class SubGuiNpcTraderStock extends SubGuiInterface implements ISubGuiList return "-1"; // Mixed values, return unlimited } } - return String.valueOf(first); + return "" + first; } @Override diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java index b4a76ba48..c9d40c5df 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java @@ -47,7 +47,7 @@ public class SubGuiNpcTraderStockSlots extends SubGuiInterface implements ITextf // Max stock textfield int maxStock = role.stock.maxStock[i]; - String value = maxStock < 0 ? "-1" : String.valueOf(maxStock); + String value = maxStock < 0 ? "-1" : "" + maxStock; GuiNpcTextField field = new GuiNpcTextField(i, this, fontRendererObj, x + 42, y, 32, 18, value); field.setIntegersOnly(); field.setMinMaxDefault(-1, Integer.MAX_VALUE, -1); diff --git a/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java b/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java index 530a3a2be..fe6daff23 100644 --- a/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java +++ b/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java @@ -5,7 +5,7 @@ import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.data.IScriptUnit; import java.util.ArrayList; import java.util.Iterator; @@ -14,10 +14,10 @@ import java.util.List; public class EventGuiScriptList extends SubGuiInterface { private GuiCustomScroll scroll1; private GuiCustomScroll scroll2; - private ScriptContainer container; + private IScriptUnit container; private List scripts; - public EventGuiScriptList(List scripts, ScriptContainer container) { + public EventGuiScriptList(List scripts, IScriptUnit container) { this.container = container; this.setBackground("menubg.png"); this.xSize = 346; @@ -50,9 +50,9 @@ public class EventGuiScriptList extends SubGuiInterface { this.addScroll(this.scroll2); this.addLabel(new GuiNpcLabel(2, "script.loadedScripts", this.guiLeft + 200, this.guiTop + 4)); ArrayList temp = new ArrayList(this.scripts); - temp.removeAll(this.container.scripts); + temp.removeAll(getScripts()); this.scroll1.setList(temp); - this.scroll2.setList(this.container.scripts); + this.scroll2.setList(getScripts()); this.addButton(new GuiNpcButton(1, this.guiLeft + 145, this.guiTop + 40, 55, 20, ">")); this.addButton(new GuiNpcButton(2, this.guiLeft + 145, this.guiTop + 62, 55, 20, "<")); this.addButton(new GuiNpcButton(3, this.guiLeft + 145, this.guiTop + 90, 55, 20, ">>")); @@ -63,25 +63,25 @@ public class EventGuiScriptList extends SubGuiInterface { protected void actionPerformed(GuiButton guibutton) { GuiNpcButton button = (GuiNpcButton) guibutton; if (button.id == 1 && this.scroll1.hasSelected()) { - this.container.scripts.add(this.scroll1.getSelected()); + getScripts().add(this.scroll1.getSelected()); this.scroll1.selected = -1; this.scroll2.selected = -1; this.initGui(); } if (button.id == 2 && this.scroll2.hasSelected()) { - this.container.scripts.remove(this.scroll2.getSelected()); + getScripts().remove(this.scroll2.getSelected()); this.scroll2.selected = -1; this.initGui(); } if (button.id == 3) { - this.container.scripts.clear(); + getScripts().clear(); Iterator var3 = this.scripts.iterator(); while (var3.hasNext()) { String script = (String) var3.next(); - this.container.scripts.add(script); + getScripts().add(script); } this.scroll1.selected = -1; @@ -90,7 +90,7 @@ public class EventGuiScriptList extends SubGuiInterface { } if (button.id == 4) { - this.container.scripts.clear(); + getScripts().clear(); this.scroll1.selected = -1; this.scroll2.selected = -1; this.initGui(); @@ -102,6 +102,10 @@ public class EventGuiScriptList extends SubGuiInterface { } + public List getScripts() { + return this.container.getExternalScripts(); + } + public void save() { } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java new file mode 100644 index 000000000..9fb601162 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java @@ -0,0 +1,429 @@ +package noppes.npcs.client.gui.script; + + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiConfirmOpenLink; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.GuiYesNoCallback; +import net.minecraft.util.StatCollector; +import noppes.npcs.NoppesStringUtils; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +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.GuiNpcTextArea; +import noppes.npcs.client.gui.util.GuiScriptTextArea; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextChangeListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.controllers.ScriptController; +import noppes.npcs.janino.JaninoScript; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback, ITextChangeListener, ICustomScrollListener, ISubGuiListener { + + private GuiScriptTextArea textArea; + private int activeTab = 0; + public List allExternalScripts = new ArrayList<>(); + private final int scriptLimit = 1; + public String previousHookClicked = ""; + + // GUI display list + List hooklist = new ArrayList<>(); + + // Map from display name (with parameters if overloaded) to Method + Map hookMap = new HashMap<>(); + + public final GuiScreen parent; + private final JaninoScript container; + + public Runnable saveFunction; + + public GuiJaninoScript(GuiScreen parent, JaninoScript handler) { + this.drawDefaultBackground = true; + this.closeOnEsc = true; + this.xSize = 420; + this.setBackground("menubg.png"); + + this.parent = parent; + this.container = handler; + + createHookList(); + + String lang = handler.getLanguage(); + allExternalScripts.addAll(ScriptController.Instance.getScripts(lang)); + } + + public GuiJaninoScript setSave(Runnable run) { + saveFunction = run; + return this; + } + + public void createHookList() { + // Group all declared methods of the handler's type by their method name + Map> grouped = Arrays.stream(container.type.getDeclaredMethods()) + .collect(Collectors.groupingBy(Method::getName)); + + for (Map.Entry> entry : grouped.entrySet()) { + List methods = entry.getValue(); + + // Sort overloads by number of parameters (ascending) + // This ensures the simplest overload comes first + methods.sort(Comparator.comparingInt(Method::getParameterCount)); + boolean first = true; + + + for (Method m : methods) { + if (Modifier.isFinal(m.getModifiers())) + continue; //skip finals + + String displayName = first ? m.getName() : + m.getName() + "(" + Arrays.stream(m.getParameterTypes()) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ")) + ")"; + + + hooklist.add(displayName); + hookMap.put(displayName, m); + first = false; + } + } + } + + public void initGui() { + this.ySize = (int) ((double) this.xSize * 0.56); + if ((double) this.ySize > (double) this.height * 0.95) { + this.ySize = (int) ((double) this.height * 0.95); + this.xSize = (int) ((double) this.ySize / 0.56); + } + + this.bgScale = (float) this.xSize / 400.0F; + super.initGui(); + this.guiTop += 10; + int yoffset = (int) ((double) this.ySize * 0.02); + GuiMenuTopButton top; + addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize - 2, guiTop - 17, "X")); + this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); + int topXoffset = 0; + int topYoffset = 0; + + + if (this.container != null) { + this.addTopButton(top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); + } else { + this.addTopButton(new GuiMenuTopButton(this.scriptLimit, top.xPosition + top.width, top.yPosition, "+")); + } + + top = this.getTopButton(this.activeTab); + if (top == null) { + this.activeTab = 0; + top = this.getTopButton(0); + } + + top.active = true; + if (this.activeTab == 1) { + GuiCustomScroll hooks = new GuiCustomScroll(this, 1); + hooks.allowUserInput = false; + hooks.setSize(108, 198); + hooks.guiLeft = this.guiLeft - 110; + hooks.guiTop = this.guiTop + 14; + + hooks.setUnsortedList(this.hooklist); + this.addScroll(hooks); + GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, this.guiTop + 5); + hookLabel.color = 11184810; + this.addLabel(hookLabel); + JaninoScript container = this.container; + if (textArea == null) + textArea = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + else + textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + textArea.enableCodeHighlighting(); + textArea.setListener(this); + this.closeOnEsc(textArea::closeOnEsc); + this.addTextField(textArea); + + int left1 = this.guiLeft + this.xSize - 104; + this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); + this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); + this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); + this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); + + this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); + // this.getButton(107).enabled = false; + + GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); + scroll.setSize(100, (int) ((double) this.ySize * 0.54) - yoffset * 2); + scroll.guiLeft = left1; + scroll.guiTop = this.guiTop + 88 + yoffset; + if (container != null) + scroll.setList(container.externalScripts); + + this.addScroll(scroll); + } else { + GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); + var8.enabled = false; + this.addTextField(var8); + int var9 = this.guiLeft + this.xSize - 150; + this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); + this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); + this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); + String containerLang = container.getLanguage(); + this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, new String[]{containerLang.equals("Java") ? "Janino (JVM 8)" : containerLang}, + this.getLanguageIndex())); + // this.getButton(103).enabled = !this.languages.isEmpty(); + this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); + // Note: GuiJaninoScript is deprecated; enabled button disabled + this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, 0)); + + this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); + this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); + this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); + this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); + } + + this.xSize = 420; + this.ySize = 256; + } + + public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { + String hook = scroll.getSelected(); + if (this.previousHookClicked.equals(hook)) { + String addString = ""; + if (!this.getTextField(2) + .getText() + .isEmpty()) { + addString = addString + "\n"; + } + + addString = addString + generateMethodStub(hookMap.get(hook)); + this.getTextField(2) + .setText(this.getTextField(2) + .getText() + addString); + this.previousHookClicked = ""; + } else { + this.previousHookClicked = hook; + } + } + + public void confirmClicked(boolean flag, int i) { + if (flag) { + if (i == 0) + this.openLink("https://www.curseforge.com/minecraft/mc-mods/customnpc-plus"); + else if (i == 1) + this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); + else if (i == 2) + this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); + else if (i == 3) + this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); + + if (i == 10) { //Remove + this.container.setScript(""); + this.activeTab = 0; + } else if (i == 101) { //Paste + this.getTextField(2) + .setText(NoppesStringUtils.getClipboardContents()); + this.setScript(); + } else if (i == 102) { //Clear + if (this.activeTab == 1) + container.script = ""; + else + this.container.clearConsole(); + + this.initGui(); + } + } + + this.displayGuiScreen(this); + } + + protected void actionPerformed(GuiButton guibutton) { + if (guibutton.id == -1) { + close(); + return; + } + + if (guibutton.id == 0) { + this.setScript(); + this.activeTab = 0; + this.initGui(); + container.ensureCompiled(); + } + + if (guibutton.id == this.scriptLimit) { + // if (container.container == null) + //this.container.container = new ScriptContainer(this.container); + // else + // this.setScript(); + this.activeTab = 1; + this.initGui(); + } + + if (guibutton.id == 109) { + this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); + } else if (guibutton.id == 110) { + this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/KAMKEEL/CustomNPC-Plus-API", 1, true)); + } else if (guibutton.id == 111) { + this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/Noppes/CustomNPCsAPI", 2, true)); + } else if (guibutton.id == 112) { + this.displayGuiScreen(new GuiConfirmOpenLink(this, "http://www.minecraftforge.net/forum/index.php/board,122.0.html", 3, true)); + } + + GuiYesNo confirmScreen; + + //Copy + if (guibutton.id == 100) { + NoppesStringUtils.setClipboardContents(this.getTextField(2) + .getText()); + } + //Paste + else if (guibutton.id == 101) { + GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); + this.displayGuiScreen(guiyesno); + } + //Clear + else if (guibutton.id == 102) { + confirmScreen = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); + this.displayGuiScreen(confirmScreen); + } + //Enable + else if (guibutton.id == 104) { + // Deprecated GUI - enabled is now per-handler, not per-unit + } + //Remove + else if (guibutton.id == 105) { + confirmScreen = new GuiYesNo(this, "", guibutton.displayString, 10); + this.displayGuiScreen(confirmScreen); + } + //Load Script + else if (guibutton.id == 107) { + setSubGui(new SubGuiSelectList(allExternalScripts, container.externalScripts, "All Scripts", "Selected Scripts")); + } + } + + public void subGuiClosed(SubGuiInterface sub) { + if (sub instanceof SubGuiSelectList) + container.setExternalScripts(((SubGuiSelectList) sub).selected); + + } + + /** + * Generates a stub string for a given Method. + */ + public static String generateMethodStub(Method method) { + String mods = java.lang.reflect.Modifier.toString(method.getModifiers()); + if (!mods.isEmpty()) + mods += " "; + + String returnTypeStr = method.getReturnType() + .getSimpleName(); + String name = method.getName(); + + Map typeCount = new HashMap<>(); + String params = Arrays.stream(method.getParameters()) + .map(p -> { + String typeName = p.getType() + .getSimpleName(); + String baseName = Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + + int count = typeCount.getOrDefault(baseName, 0) + 1; + typeCount.put(baseName, count); + + // append number if same type of arg exists + String paramName = count == 1 ? baseName : baseName + (count - 1); + + return typeName + " " + paramName; + }) + .collect(Collectors.joining(", ")); + + String body; + Class returnType = method.getReturnType(); + if (returnType == void.class) + body = ""; + else if (returnType.isPrimitive()) { + if (returnType == boolean.class) + body = " return false;"; + else if (returnType == char.class) + body = " return '\\0';"; + else + body = " return 0;"; + } else { + body = " return null;"; + } + + return String.format("%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); + } + + private String getConsoleText() { + Map map = this.container.getConsole(); + StringBuilder builder = new StringBuilder(); + Iterator var3 = map.entrySet() + .iterator(); + + while (var3.hasNext()) { + Map.Entry entry = (Map.Entry) var3.next(); + builder.insert(0, new Date(entry.getKey()) + entry.getValue() + "\n"); + } + + return builder.toString(); + } + + private int getLanguageIndex() { + return 0; + } + + private void setScript() { + if (this.activeTab == 1) { + String text = this.getTextField(2) + .getText(); + text = text.replace("\r\n", "\n"); + text = text.replace("\r", "\n"); + container.setScript(text); + } + } + + public void save() { + if (saveFunction != null) + saveFunction.run(); + else if (parent instanceof GuiNPCInterface) + ((GuiNPCInterface) parent).save(); + } + + public void textUpdate(String text) { +// container.setScript(text); + } + + public static GuiJaninoScript create(GuiScreen parent, JaninoScript script, int width, int height) { + GuiJaninoScript gui = new GuiJaninoScript(parent, script); + gui.setWorldAndResolution(Minecraft.getMinecraft(), width, height); + Minecraft.getMinecraft().currentScreen = gui; + gui.initGui(); + return gui; + } + + + @Override + public void close() { + this.save(); + this.setScript(); + container.ensureCompiled(); + parent.setWorldAndResolution(mc, width, height); + parent.initGui(); + mc.currentScreen = parent; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java index 027f51a60..db1338fff 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java @@ -1,64 +1,13 @@ package noppes.npcs.client.gui.script; -import kamkeel.npcs.network.packets.request.script.EventScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.DataScript; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.entity.EntityNPCInterface; -import java.util.ArrayList; -import java.util.List; - public class GuiNPCEventScripts extends GuiScriptInterface { - private final DataScript script; - public GuiNPCEventScripts(EntityNPCInterface npc) { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_NPC)); - this.script = new DataScript(npc); - this.handler = this.script; - EventScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(script); - container.readFromNBT(compound.getCompoundTag("Script")); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - EventScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - EventScriptPacket.Save(-1, containers.size(), scriptData); - } + this.handler = new DataScript(npc); + if (this.handler instanceof IScriptHandlerPacket) + ((IScriptHandlerPacket) this.handler).requestData(); } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java deleted file mode 100644 index cef9096bd..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java +++ /dev/null @@ -1,62 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.GlobalNPCDataScript; - -import java.util.ArrayList; -import java.util.List; - -public class GuiScriptAllNPCs extends GuiScriptInterface { - private final GlobalNPCDataScript script = new GlobalNPCDataScript(null); - - public GuiScriptAllNPCs() { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_NPC)); - this.handler = this.script; - GlobalNPCScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(script); - container.readFromNBT(compound.getCompoundTag("Script")); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - GlobalNPCScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - GlobalNPCScriptPacket.Save(-1, containers.size(), scriptData); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java deleted file mode 100644 index a97efa90f..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java +++ /dev/null @@ -1,34 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.blocks.tiles.TileScripted; -import noppes.npcs.controllers.ScriptHookController; - -import java.util.ArrayList; - - -public class GuiScriptBlock extends GuiScriptInterface { - private final TileScripted tileScripted; - - public GuiScriptBlock(int x, int y, int z) { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_BLOCK)); - - this.handler = this.tileScripted = (TileScripted) player.worldObj.getTileEntity(x, y, z); - BlockScriptPacket.Get(tileScripted.xCoord, tileScripted.yCoord, tileScripted.zCoord); - } - - public void setGuiData(NBTTagCompound compound) { - this.tileScripted.setNBT(compound); - super.setGuiData(compound); - loaded = true; - } - - public void save() { - if (loaded) { - super.save(); - BlockScriptPacket.Save(tileScripted.xCoord, tileScripted.yCoord, tileScripted.zCoord, this.tileScripted.getNBT(new NBTTagCompound())); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java deleted file mode 100644 index f4fb80846..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ /dev/null @@ -1,415 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiConfirmOpenLink; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.StatCollector; -import noppes.npcs.NBTTags; -import noppes.npcs.NoppesStringUtils; -import noppes.npcs.client.gui.global.GuiNPCManageEffects; -import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiMenuTopButton; -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.GuiNpcTextArea; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.GuiScriptTextArea; -import noppes.npcs.client.gui.util.ICustomScrollListener; -import noppes.npcs.client.gui.util.IGuiData; -import noppes.npcs.client.gui.util.IJTextAreaListener; -import noppes.npcs.client.gui.util.ITextChangeListener; -import noppes.npcs.client.gui.util.ITextfieldListener; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.CustomEffect; -import noppes.npcs.controllers.data.EffectScript; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public class GuiScriptEffect extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener { - - private int activeTab = 0; - public Map> languages = new HashMap(); - private final int scriptLimit = 1; - public List hookList = new ArrayList(); - public String previousHookClicked = ""; - - public final GuiNPCManageEffects parent; - public final CustomEffect effect; - private final EffectScript scriptHandler; - boolean loaded = false; - - public GuiScriptEffect(GuiNPCManageEffects parent, CustomEffect effect) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - - this.parent = parent; - this.effect = effect; - this.scriptHandler = new EffectScript(); - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_EFFECT)); - - EffectScriptPacket.Get(effect.id); - } - - public void initGui() { - this.ySize = (int) ((double) this.xSize * 0.56); - if ((double) this.ySize > (double) this.height * 0.95) { - this.ySize = (int) ((double) this.height * 0.95); - this.xSize = (int) ((double) this.ySize / 0.56); - } - - this.bgScale = (float) this.xSize / 400.0F; - super.initGui(); - this.guiTop += 10; - int yoffset = (int) ((double) this.ySize * 0.02); - GuiMenuTopButton top; - this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); - int topXoffset = 0; - int topYoffset = 0; - - - if (this.scriptHandler.container != null) { - this.addTopButton(top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); - - } else { - this.addTopButton(new GuiMenuTopButton(this.scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - } - - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); - } - - top.active = true; - if (this.activeTab == 1) { - GuiCustomScroll hooks = new GuiCustomScroll(this, 1); - hooks.allowUserInput = false; - hooks.setSize(108, 198); - hooks.guiLeft = this.guiLeft - 110; - hooks.guiTop = this.guiTop + 14; - - hooks.setUnsortedList(this.hookList); - this.addScroll(hooks); - GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, this.guiTop + 5); - hookLabel.color = 11184810; - this.addLabel(hookLabel); - ScriptContainer container = this.scriptHandler.container; - GuiScriptTextArea ta = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - ta.enableCodeHighlighting(); - ta.setListener(this); - this.addTextField(ta); - int left1 = this.guiLeft + this.xSize - 104; - this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); - this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); - this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - - this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); - this.getButton(107).enabled = false; - - GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); - scroll.setSize(100, (int) ((double) this.ySize * 0.54) - yoffset * 2); - scroll.guiLeft = left1; - scroll.guiTop = this.guiTop + 88 + yoffset; - if (container != null) { - scroll.setList(container.scripts); - } - - this.addScroll(scroll); - } else { - GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); - var8.enabled = false; - this.addTextField(var8); - int var9 = this.guiLeft + this.xSize - 150; - this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); - this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); - this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, this.languages.keySet().toArray(new String[this.languages.keySet().size()]), this.getScriptIndex())); - this.getButton(103).enabled = !this.languages.isEmpty(); - this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.scriptHandler.getEnabled() ? 1 : 0)); - - this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); - this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); - this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); - this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); - } - - this.xSize = 420; - this.ySize = 256; - } - - public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { - String hook = scroll.getSelected(); - if (this.previousHookClicked.equals(hook)) { - String addString = ""; - if (!this.getTextField(2).getText().isEmpty()) { - addString = addString + "\n"; - } - - addString = addString + "function " + hook + "(event) {\n \n}\n"; - this.getTextField(2).setText(this.getTextField(2).getText() + addString); - this.previousHookClicked = ""; - } else { - this.previousHookClicked = hook; - } - - } - - public void unFocused(GuiNpcTextField textfield) { - } - - private String getConsoleText() { - Map map = this.scriptHandler.getConsoleText(); - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - private int getScriptIndex() { - int i = 0; - - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.scriptHandler.getLanguage())) { - return i; - } - } - - return 0; - } - - public void confirmClicked(boolean flag, int i) { - if (flag) { - if (i == 0) { - this.openLink("https://www.curseforge.com/minecraft/mc-mods/customnpc-plus"); - } - - if (i == 1) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 2) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 3) { - this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); - } - - if (i == 10) { - this.scriptHandler.container = null; - this.activeTab = 0; - } - - if (i == 101) { - this.getTextField(2).setText(NoppesStringUtils.getClipboardContents()); - this.setScript(); - } - - if (i == 102) { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - container.script = ""; - } else { - this.scriptHandler.clearConsole(); - } - - this.initGui(); - } - } - - this.displayGuiScreen(this); - } - - protected void actionPerformed(GuiButton guibutton) { - if (guibutton.id == 0) { - this.setScript(); - this.activeTab = 0; - this.initGui(); - } - - if (guibutton.id == this.scriptLimit) { - if (scriptHandler.container == null) - this.scriptHandler.container = new ScriptContainer(this.scriptHandler); - else - this.setScript(); - this.activeTab = 1; - this.initGui(); - } - - if (guibutton.id == 109) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); - } - - if (guibutton.id == 110) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/KAMKEEL/CustomNPC-Plus-API", 1, true)); - } - - if (guibutton.id == 111) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/Noppes/CustomNPCsAPI", 2, true)); - } - - if (guibutton.id == 112) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "http://www.minecraftforge.net/forum/index.php/board,122.0.html", 3, true)); - } - - if (guibutton.id == 100) { - NoppesStringUtils.setClipboardContents(this.getTextField(2).getText()); - } - - if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - this.displayGuiScreen(guiyesno); - } - - GuiYesNo container1; - if (guibutton.id == 102) { - container1 = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 103) { - this.scriptHandler.setLanguage(guibutton.displayString); - } - - if (guibutton.id == 104) { - this.scriptHandler.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); - } - - if (guibutton.id == 105) { - container1 = new GuiYesNo(this, "", ((GuiNpcButton) guibutton).displayString, 10); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 107) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - -// this.setSubGui(new EventGuiScriptList((List)this.languages.get(this.script.getLanguage()), container)); - } - - } - - private void setScript() { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - - String text = this.getTextField(2).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.script = text; - } - - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - this.scriptHandler.setLanguage(compound.getString("ScriptLanguage")); - this.scriptHandler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.copiedSetGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(this.scriptHandler); - container.readFromNBT(compound.getCompoundTag("Script")); - this.scriptHandler.container = container; - this.initGui(); - } - } - - private void copiedSetGuiData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); - } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); - } - - public void save() { - if (loaded) { - this.setScript(); - - List containers = this.scriptHandler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - EffectScriptPacket.Save(effect.id, i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.scriptHandler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.scriptHandler.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.scriptHandler.getConsoleText())); - - EffectScriptPacket.Save(effect.id, -1, containers.size(), scriptData); - } - } - - public void textUpdate(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - } - - public void saveText(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - this.initGui(); - } - - @Override - public void close() { - this.save(); - parent.setWorldAndResolution(mc, width, height); - parent.initGui(); - mc.currentScreen = parent; - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java deleted file mode 100644 index 01a179d54..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java +++ /dev/null @@ -1,117 +0,0 @@ -package noppes.npcs.client.gui.script; - -import com.google.common.reflect.ClassPath; -import cpw.mods.fml.common.eventhandler.Event; -import cpw.mods.fml.common.gameevent.TickEvent; -import cpw.mods.fml.common.network.FMLNetworkEvent; -import kamkeel.npcs.network.packets.request.script.ForgeScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.event.entity.EntityEvent; -import net.minecraftforge.event.entity.player.ItemTooltipEvent; -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.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.ForgeDataScript; -import org.apache.commons.lang3.StringUtils; - -import java.io.IOException; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; - -public class GuiScriptForge extends GuiScriptInterface { - private final ForgeDataScript script = new ForgeDataScript(); - - public GuiScriptForge() { - // Add built-in "init" hook from controller - hookList.addAll(ScriptHookController.Instance.getBuiltInHooks(IScriptHookHandler.CONTEXT_FORGE)); - - // Dynamically discover Forge event hooks - ArrayList list = new ArrayList(); - try { - list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("cpw.mods.fml.common.gameevent")); - list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event")); - list.removeAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event.terraingen")); - } catch (IOException e) { - e.printStackTrace(); - } - - for (ClassPath.ClassInfo e1 : list) { - ClassPath.ClassInfo classLoader = e1; - Class infoClass = classLoader.load(); - ArrayList c = new ArrayList(Arrays.asList(infoClass.getDeclaredClasses())); - if (c.isEmpty()) { - c.add(infoClass); - } - Iterator var10 = c.iterator(); - while (var10.hasNext()) { - Class c1 = (Class) var10.next(); - if (!EntityEvent.EntityConstructing.class.isAssignableFrom(c1) && !WorldEvent.PotentialSpawns.class.isAssignableFrom(c1) && !TickEvent.RenderTickEvent.class.isAssignableFrom(c1) && !TickEvent.ClientTickEvent.class.isAssignableFrom(c1) && !FMLNetworkEvent.ClientCustomPacketEvent.class.isAssignableFrom(c1) && !ItemTooltipEvent.class.isAssignableFrom(c1) && Event.class.isAssignableFrom(c1) && !Modifier.isAbstract(c1.getModifiers()) && Modifier.isPublic(c1.getModifiers()) && !ChunkEvent.class.isAssignableFrom(c1) && !ChunkWatchEvent.class.isAssignableFrom(c1) && !ChunkDataEvent.class.isAssignableFrom(c1)) { - String eventName = c1.getName(); - int i = eventName.lastIndexOf("."); - eventName = StringUtils.uncapitalize(eventName.substring(i + 1).replace("$", "")); - - hookList.add(eventName); - } - } - } - hookList.add("onCNPCNaturalSpawn"); - - // Add any addon-registered hooks for the forge context - for (String addonHook : ScriptHookController.Instance.getAddonHooks(IScriptHookHandler.CONTEXT_FORGE)) { - if (!hookList.contains(addonHook)) { - hookList.add(addonHook); - } - } - - this.handler = this.script; - ForgeScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(script); - container.readFromNBT(compound.getCompoundTag("Script")); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - ForgeScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - ForgeScriptPacket.Save(-1, containers.size(), scriptData); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java index 15aec96dd..52ab86b24 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java @@ -8,6 +8,9 @@ import net.minecraft.util.ResourceLocation; import noppes.npcs.client.gui.util.GuiNPCInterface; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.controllers.data.ForgeDataScript; +import noppes.npcs.controllers.data.GlobalNPCDataScript; +import noppes.npcs.controllers.data.PlayerDataScript; import org.lwjgl.opengl.GL11; import java.util.HashMap; @@ -65,13 +68,13 @@ public class GuiScriptGlobal extends GuiNPCInterface implements IGuiData { protected void actionPerformed(GuiButton guibutton) { if (guibutton.id == 0) { - this.displayGuiScreen(new GuiScriptPlayers()); + GuiScriptInterface.open(this, new PlayerDataScript(null)); } if (guibutton.id == 1) { - this.displayGuiScreen(new GuiScriptForge()); + GuiScriptInterface.open(this, new ForgeDataScript()); } if (guibutton.id == 2) { - this.displayGuiScreen(new GuiScriptAllNPCs()); + GuiScriptInterface.open(this, new GlobalNPCDataScript(null)); } if (guibutton.id > 2) { try { diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 185eb90fd..82012429f 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -1,7 +1,9 @@ package noppes.npcs.client.gui.script; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiConfirmOpenLink; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; @@ -17,16 +19,21 @@ import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextArea; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.GuiScriptTextArea; +import noppes.npcs.client.gui.util.GuiUtil; import noppes.npcs.client.gui.util.ICustomScrollListener; import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.client.gui.util.IJTextAreaListener; import noppes.npcs.client.gui.util.ITextChangeListener; import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptHandlerPacket; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.item.ScriptCustomItem; +import org.lwjgl.opengl.Display; import java.util.ArrayList; import java.util.Date; @@ -40,10 +47,59 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected int activeTab = 0; public IScriptHandler handler; public Map> languages = new HashMap(); - private int scriptLimit = 1; + protected int scriptLimit = 1; public List hookList = new ArrayList(); protected boolean loaded = false; + protected Map textAreas = new HashMap<>(); + protected GuiScreen parent; + + + /** + * If true, uses scroll-based tab selection instead of top buttons + */ + protected boolean useScrollTabs = false; + + /** + * If true, shows settings/script toggle instead of numbered tabs + */ + protected boolean useSettingsToggle = false; + + // ==================== FULLSCREEN MODE ==================== + /** + * Whether the editor viewport is currently in fullscreen mode + */ + public static boolean isFullscreen = false; + + /** + * Fullscreen Layout Configuration + * Adjust these values to customize the fullscreen viewport appearance. + * All padding values are in scaled GUI pixels. + */ + public static class FullscreenConfig { + /** + * Padding from the top edge of the screen + */ + public static int paddingTop = 20; + /** + * Padding from the bottom edge of the screen + */ + public static int paddingBottom = 20; + /** + * Padding from the left edge of the screen + */ + public static int paddingLeft = 20; + /** + * Padding from the right edge of the screen + */ + public static int paddingRight = 20; + } + + /** + * Fullscreen toggle button drawn at top-right of viewport + */ + public final FullscreenButton fullscreenButton = new FullscreenButton(); + public GuiScriptInterface() { this.drawDefaultBackground = true; this.closeOnEsc = true; @@ -51,8 +107,37 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.setBackground("menubg.png"); } + /** + * Create a GuiScriptInterface for a handler without displaying it. + * Useful when a caller needs to return a GuiScreen (e.g. ClientProxy). + */ + public static GuiScriptInterface create(GuiScreen parent, IScriptHandler handler) { + GuiScriptInterface gui = new GuiScriptInterface(); + gui.handler = handler; + gui.parent = parent; + + // Initialize hooks from handler + gui.hookList = new ArrayList<>(handler.getHooks()); + + // Request data from server + if (handler instanceof IScriptHandlerPacket) + ((IScriptHandlerPacket) handler).requestData(); + + return gui; + } + + /** + * Open a GuiScriptInterface for a handler. + * Wraps the script in a SingleScriptHandler automatically. + */ + public static GuiScriptInterface open(GuiScreen parent, IScriptHandler handler) { + GuiScriptInterface gui = create(parent, handler); + Minecraft.getMinecraft().displayGuiScreen(gui); + return gui; + } + public void initGui() { - //this.xSize = (int)((double)this.width * 0.88D); + // ==================== BASE LAYOUT CALCULATION ==================== this.ySize = (int) ((double) this.xSize * 0.56D); if ((double) this.ySize > (double) this.height * 0.95D) { this.ySize = (int) ((double) this.height * 0.95D); @@ -63,33 +148,96 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb super.initGui(); this.guiTop += 10; int yoffset = (int) ((double) this.ySize * 0.02D); - GuiMenuTopButton top; - this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); - int topXoffset = 0; - int topYoffset = 0; - for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { - if (ta % 20 == 0 && ta > 0) { - topYoffset -= 20; - topXoffset -= top.width + 20 * 22; + if (hookList.isEmpty()) + hookList = new ArrayList<>(handler.getHooks()); + + // ==================== TAB BUTTONS ==================== + // Skip standard tab creation for GUIs that use custom toggle/scroll tabs + if (!useSettingsToggle) { + GuiMenuTopButton top; + int topButtonsX = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingLeft : this.guiLeft + 4; + int topButtonsY = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingTop - 20 : this.guiTop - 17; + this.addTopButton(top = new GuiMenuTopButton(0, topButtonsX, topButtonsY, "gui.settings")); + + int topXoffset = 0; + int topYoffset = 0; + boolean isSingle = handler.isSingleContainer(); + if (!isSingle) { + for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { + if (ta % 20 == 0 && ta > 0) { + topYoffset -= 20; + topXoffset -= top.width + 20 * 22; + } + this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, + top.yPosition + topYoffset, ta + 1 + "")); + topXoffset = 0; + topYoffset = 0; + scriptLimit = ta + 2; + } } - this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, ta + 1 + "")); - topXoffset = 0; - topYoffset = 0; - scriptLimit = ta + 2; + + if (isSingle && handler.getSingleScript() != null) + this.addTopButton( + top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, + "Script")); + else if (this.handler.getScripts().size() < 100) + this.addTopButton(new GuiMenuTopButton(scriptLimit, top.xPosition + top.width, top.yPosition, "+")); + + + top = this.getTopButton(this.activeTab); + if (top == null) { + this.activeTab = 0; + top = this.getTopButton(0); + } + top.active = true; } - if (this.handler.getScripts().size() < 100) - this.addTopButton(new GuiMenuTopButton(scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); + // ==================== SCRIPT EDITOR TAB (activeTab > 0) ==================== + // Skip for GUIs that handle their own view initialization (useSettingsToggle) + if (!useSettingsToggle) { + if (this.activeTab > 0) { + initScriptEditorTab(yoffset); + } else { + // ==================== SETTINGS TAB (activeTab == 0) ==================== + initSettingsTab(yoffset); + } } - top.active = true; - if (this.activeTab > 0) { + this.xSize = 420; + this.ySize = 256; + } + + /** + * Initialize the script editor tab layout. + * Handles both normal and fullscreen modes. + */ + private void initScriptEditorTab(int yoffset) { + IScriptUnit container = getCurrentContainer(); + + // ==================== CALCULATE VIEWPORT BOUNDS ==================== + int editorX, editorY, editorWidth, editorHeight; + + FullscreenConfig.paddingTop = 30; + FullscreenConfig.paddingBottom = 20; + FullscreenConfig.paddingLeft = 20; + FullscreenConfig.paddingRight = 20; + if (isFullscreen) { + // Fullscreen: viewport fills screen with configured padding + editorX = FullscreenConfig.paddingLeft; + editorY = FullscreenConfig.paddingTop; + editorWidth = this.width - FullscreenConfig.paddingLeft - FullscreenConfig.paddingRight; + editorHeight = this.height - FullscreenConfig.paddingTop - FullscreenConfig.paddingBottom; + } else { + // Normal: viewport within the GUI panel, leaving space for right panel + editorX = guiLeft + 1 + yoffset; + editorY = guiTop + yoffset; + editorWidth = xSize - 108 - yoffset; + editorHeight = (int) (ySize * 0.96) - yoffset * 2; + } + + // ==================== HOOKS SCROLL (hidden in fullscreen) ==================== + if (!isFullscreen) { GuiCustomScroll hooks = new GuiCustomScroll(this, 1); hooks.allowUserInput = false; hooks.setSize(108, 198); @@ -106,54 +254,143 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, guiTop + 5); hookLabel.color = 0xaaaaaa; addLabel(hookLabel); + } - ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); - GuiScriptTextArea ta = new GuiScriptTextArea(this, 2, guiLeft + 1 + yoffset, guiTop + yoffset, xSize - 108 - yoffset, (int) (ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - ta.enableCodeHighlighting(); - ta.setListener(this); + // ==================== SCRIPT TEXT AREA ==================== + int idx = getActiveScriptIndex(); + GuiScriptTextArea activeArea = getActiveScriptArea(); + if (activeArea == null) { + activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.getScript()); + activeArea.setListener(this); + this.closeOnEsc(activeArea::closeOnEsc); + textAreas.put(idx, activeArea); + } else { + activeArea.init(editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.getScript()); + } - this.addTextField(ta); + // Set the scripting language for proper syntax highlighting + // Use the container's language if available, otherwise fall back to handler's language + String language = (container != null) ? container.getLanguage() : this.handler.getLanguage(); + activeArea.setLanguage(language); + // Set the script context for context-aware hook autocomplete + activeArea.setScriptContext(getScriptContext()); + + // Setup fullscreen key binding + GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { + if (e.isPress()) { + toggleFullscreen(); + } + }); + + activeArea.enableCodeHighlighting(); + this.addTextField(activeArea); + + // ==================== FULLSCREEN BUTTON ==================== + int scrollbarOffset = activeArea.hasVerticalScrollbar() ? -8 : -2; + fullscreenButton.initGui(editorX + editorWidth, editorY, scrollbarOffset); + + // ==================== RIGHT PANEL BUTTONS (hidden in fullscreen) ==================== + if (!isFullscreen) { int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); + + // Language toggle button (only if handler supports Janino and there is more than one option) + if (handler.supportsJanino() && getLanguageOptions().size() > 1 && container != null) { + this.addButton(new GuiNpcButton(113, left1, this.guiTop + 42 + yoffset, 121, 20, container.getLanguage())); + } + this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); + GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); scroll.setSize(100, (int) ((double) this.ySize * 0.54D) - yoffset * 2); scroll.guiLeft = left1; scroll.guiTop = this.guiTop + 88 + yoffset; if (container != null) { - scroll.setList(container.scripts); + scroll.setList(container.getExternalScripts()); } - this.addScroll(scroll); - } else { - GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); - var8.canEdit = false; - var8.setFocused(true); - this.addTextField(var8); - int var9 = this.guiLeft + this.xSize - 150; - this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); - this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); - this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, (String[]) this.languages.keySet().toArray(new String[this.languages.keySet().size()]), this.getScriptIndex())); - this.getButton(103).enabled = this.languages.size() > 0; - this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.handler.getEnabled() ? 1 : 0)); - if (this.player.worldObj.isRemote) { - this.addButton(new GuiNpcButton(106, var9, this.guiTop + 55, 150, 20, "script.openfolder")); - } + } + } - this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); - this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); - this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); - this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); + /** + * Initialize the settings tab layout (console view). + */ + private void initSettingsTab(int yoffset) { + GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, + this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); + var8.canEdit = false; + var8.setFocused(true); + this.addTextField(var8); + + int var9 = this.guiLeft + this.xSize - 150; + this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); + this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); + this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); + List languageOptions = getLanguageOptions(); + this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, + languageOptions.toArray(new String[0]), + this.getLanguageIndex(languageOptions))); + this.getButton(103).enabled = languageOptions.size() > 0; + this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); + this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, + new String[]{"gui.no", "gui.yes"}, this.handler.getEnabled() ? 1 : 0)); + + if (this.player.worldObj.isRemote) { + this.addButton(new GuiNpcButton(106, var9, this.guiTop + 55, 150, 20, "script.openfolder")); } - this.xSize = 420; - this.ySize = 256; + this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); + this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); + this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); + this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); + } + + public GuiScriptInterface setDimensions(int x, int y) { + this.xSize = x; + this.ySize = y; + return this; + } + + /** + * Get the script context for this GUI. + * Override in subclasses to return a different context. + * + * @return The script context (default: GLOBAL, or handler's context if available) + */ + protected ScriptContext getScriptContext() { + return handler.getContext(); + } + + // ==================== RENDERING ==================== + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw fullscreen button on top of everything when on script editor tab + // Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves + if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer()) { + fullscreenButton.draw(mouseX, mouseY); + } + } + + // ==================== MOUSE HANDLING ==================== + + @Override + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + // Check fullscreen button first when on script editor tab + // Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves + if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer() && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + return; + } + + super.mouseClicked(mouseX, mouseY, mouseButton); } public String previousHookClicked = ""; @@ -162,10 +399,17 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { String hook = scroll.getSelected(); if (previousHookClicked.equals(hook)) { + IScriptUnit container = getCurrentContainer(); + if (container == null) + return; + String addString = ""; if (!this.getTextField(2).getText().isEmpty()) addString += "\n"; - addString += "function " + hook + "(event) {\n \n}\n"; + + // Generate the appropriate stub based on language + // hook is already the proper method name from EnumScriptType.function + addString += container.generateHookStub(hook, null); this.getTextField(2).setText(this.getTextField(2).getText() + addString); previousHookClicked = ""; @@ -192,19 +436,66 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return builder.toString(); } - private int getScriptIndex() { - int i = 0; - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.handler.getLanguage())) { + /** + * Finds the index of the current handler's language within the available options. + * + * @return The 0-based index, or 0 if not found. + */ + public int getLanguageIndex() { + return getLanguageIndex(getLanguageOptions()); + } + + /** + * Finds the index of the current handler's language within the provided list. + * + * @param languageOptions The list of available languages to search. + * @return The 0-based index, or 0 if not found. + */ + protected int getLanguageIndex(List languageOptions) { + if (languageOptions == null || languageOptions.isEmpty()) + return 0; + + int i = 0; + String current = this.handler != null ? this.handler.getLanguage() : null; + for (String language : languageOptions) { + if (current != null && language.equalsIgnoreCase(current)) return i; - } + i++; } return 0; } + /** + * Retrieves the list of available scripting languages for this handler. + *

+ * This method prioritizes languages provided by the server through the {@link #languages} map. + * If "Java" is not present but the handler supports Janino, it injects "Java" into the options + * and ensures the client-side Janino script list is synchronized for the {@link EventGuiScriptList}. + * + * @return A list of language names available for selection. + */ + protected List getLanguageOptions() { + List options = new ArrayList<>(); + if (this.languages != null && !this.languages.isEmpty()) + options.addAll(this.languages.keySet()); + + if ((options.isEmpty() || !options.contains("Java")) && this.handler != null && this.handler.supportsJanino()) { + List scripts = new ArrayList<>(); + if (ScriptController.Instance != null && ScriptController.Instance.scripts != null) { + scripts.addAll(ScriptController.Instance.scripts.keySet()); + } + if (this.languages == null) + this.languages = new HashMap(); + if (!this.languages.containsKey("Java")) + this.languages.put("Java", scripts); + options.add("Java"); + } + + return options; + } + public void confirmClicked(boolean flag, int i) { if (flag) { if (i == 0) { @@ -223,7 +514,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); } if (i == 10) { - this.handler.getScripts().remove(this.activeTab - 1); + handler.removeScriptUnit(this.activeTab - 1); this.activeTab = 0; } if (i == 101) { @@ -231,10 +522,10 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.setScript(); } if (i == 102) { - ScriptContainer container; + IScriptUnit container; if (this.activeTab > 0) { - container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); - container.script = ""; + container = this.handler.getScripts().get(this.activeTab - 1); + container.setScript(""); } else { this.handler.clearConsole(); if (this.handler instanceof ScriptCustomItem) { @@ -248,17 +539,67 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.displayGuiScreen(this); } + public GuiScriptTextArea getActiveScriptArea() { + int idx = getActiveScriptIndex(); + if (idx >= 0 && textAreas.containsKey(idx)) + return textAreas.get(idx); + return null; + } + + /** + * Get the current script index for text area storage. + * Override in subclasses with different tab logic. + */ + protected int getActiveScriptIndex() { + return this.activeTab - 1; + } + + /** + * Get the current script container. + * Override in subclasses with different container access patterns. + */ + protected IScriptUnit getCurrentContainer() { + int idx = getActiveScriptIndex(); + if (idx >= 0 && idx < this.handler.getScripts().size()) + return this.handler.getScripts().get(idx); + return null; + } + + @Override + public GuiNpcTextField getTextField(int id) { + if (id == 2) { + GuiScriptTextArea area = getActiveScriptArea(); + if (area != null) + return area; + } + return super.getTextField(id); + } + @Override protected void actionPerformed(GuiButton guibutton) { if (guibutton.id >= 0 && guibutton.id < scriptLimit) { this.setScript(); this.activeTab = guibutton.id; this.initGui(); + + // Ensure JaninoScript is compiled after editing + IScriptUnit current = getCurrentContainer(); + if (current != null) + current.ensureCompiled(); } if (guibutton.id == scriptLimit) { - this.handler.getScripts().add(new ScriptContainer(this.handler)); - this.activeTab = this.handler.getScripts().size(); + if (handler.isSingleContainer()) { + if (handler.getSingleScript() == null) { + handler.addScriptUnit(new ScriptContainer(handler)); + } else { + this.setScript(); + } + this.activeTab = 1; + } else { + handler.addScriptUnit(new ScriptContainer(this.handler)); + this.activeTab = this.handler.getScripts().size(); + } this.initGui(); } @@ -287,7 +628,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb displayGuiScreen(guiyesno); } - ScriptContainer container; if (guibutton.id == 102) { GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); displayGuiScreen(guiyesno); @@ -313,37 +653,103 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } if (guibutton.id == 107) { - container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + IScriptUnit container = getCurrentContainer(); if (container == null) { - this.handler.getScripts().add(container = new ScriptContainer(this.handler)); + container = new ScriptContainer(this.handler); + handler.addScriptUnit(container); } - this.setSubGui(new EventGuiScriptList((List) this.languages.get(this.handler.getLanguage()), container)); + String language = container != null ? container.getLanguage() : this.handler.getLanguage(); + this.setSubGui(new EventGuiScriptList((List) this.languages.get(language), container)); + } + + // Language toggle button - switch between ECMAScript and Java + if (guibutton.id == 113) { + int idx = getActiveScriptIndex(); + if (idx >= 0 && idx < handler.getScripts().size()) { + IScriptUnit currentUnit = handler.getScripts().get(idx); + IScriptUnit newUnit; + + if (currentUnit.isJanino()) { + // Switch from Java to ECMAScript + newUnit = new ScriptContainer(handler); + newUnit.setScript(currentUnit.getScript()); + newUnit.setExternalScripts(new ArrayList<>(currentUnit.getExternalScripts())); + } else { + // Switch from ECMAScript to Java + newUnit = handler.createJaninoScriptUnit(); + if (newUnit != null) { + newUnit.setScript(currentUnit.getScript()); + newUnit.setExternalScripts(new ArrayList<>(currentUnit.getExternalScripts())); + } else { + return; // Handler doesn't support Janino + } + } + + handler.replaceScriptUnit(idx, newUnit); + // Clear the cached text area so it recreates with new language + textAreas.remove(idx); + + // Reinitialize the GUI + initGui(); + } } } - private void setScript() { - if (this.activeTab > 0) { - ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + protected void setScript() { + if (this.activeTab > 0 || useScrollTabs) { + IScriptUnit container = getCurrentContainer(); + if (container == null) { - this.handler.getScripts().add(container = new ScriptContainer(this.handler)); + container = new ScriptContainer(this.handler); + handler.addScriptUnit(container); } - String text = (this.getTextField(2)).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.script = text; + if (container != null) { + String text = (this.getTextField(2)).getText(); + text = text.replace("\r\n", "\n"); + text = text.replace("\r", "\n"); + container.setScript(text); + } } } public void setGuiData(NBTTagCompound compound) { + if (handler instanceof IScriptHandlerPacket) { + IScriptHandlerPacket.GuiDataResult result = ((IScriptHandlerPacket) handler).setGuiData(compound); + if (result.kind == IScriptHandlerPacket.GuiDataKind.LOAD_COMPLETE) { + loaded = true; + return; + } + if (result.kind == IScriptHandlerPacket.GuiDataKind.METADATA) { + loadLanguagesData(compound); + return; + } + if (result.kind == IScriptHandlerPacket.GuiDataKind.TAB) { + initGui(); + return; + } + return; + } + + loadLanguagesData(compound); + } + + + // ==================== UNIFIED SCRIPT DATA HANDLING ==================== + + /** + * Load languages data from NBT. + * Separated for potential override in subclasses if needed. + */ + protected void loadLanguagesData(NBTTagCompound compound) { NBTTagList data = compound.getTagList("Languages", 10); HashMap languages = new HashMap(); for (int i = 0; i < data.tagCount(); ++i) { NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); + java.util.ArrayList scripts = new java.util.ArrayList(); NBTTagList list = comp.getTagList("Scripts", 8); for (int j = 0; j < list.tagCount(); ++j) { @@ -353,28 +759,193 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb languages.put(comp.getString("Language"), scripts); } + if (handler != null && handler.supportsJanino()) { + List javaScripts = (List) languages.get("Java"); + if (javaScripts == null || javaScripts.isEmpty()) { + javaScripts = new ArrayList<>(); + if (ScriptController.Instance != null && ScriptController.Instance.scripts != null) { + javaScripts.addAll(ScriptController.Instance.scripts.keySet()); + } + languages.put("Java", javaScripts); + } + } + this.languages = languages; this.initGui(); } public void save() { - if (loaded) - this.setScript(); + if (!loaded) + return; + + this.setScript(); + + if (handler instanceof IScriptHandlerPacket) + ((IScriptHandlerPacket) handler).sync(); + + } public void textUpdate(String text) { - ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + IScriptUnit container = getCurrentContainer(); if (container != null) { - container.script = text; + container.setScript(text); } } + @Deprecated @Override + //NEVER USED public void saveText(String text) { - ScriptContainer container = handler.getScripts().get(activeTab); + IScriptUnit container = getCurrentContainer(); if (container != null) - container.script = text; + container.setScript(text); initGui(); } + + @Override + public void close() { + // Ensure JaninoScript is compiled after editing + IScriptUnit current = getCurrentContainer(); + if (current != null) + current.ensureCompiled(); + + if (parent != null) { + this.save(); + parent.setWorldAndResolution(mc, width, height); + parent.initGui(); + mc.currentScreen = parent; + } else + super.close(); + } + + // ==================== FULLSCREEN METHODS ==================== + + /** + * Toggle fullscreen mode for the editor viewport. + * When entering fullscreen, maximizes the game window if it's not already. + */ + public void toggleFullscreen() { + isFullscreen = !isFullscreen; + + // When entering fullscreen, maximize the game window if not already fullscreen + if (isFullscreen && !Display.isFullscreen()) { + // Note: Display.setFullscreen(true) would make it truly fullscreen, + // but that's usually not desirable for an in-game editor. + // The viewport will expand to fill whatever window size exists. + } + + initGui(); + } + + /** + * Check if editor is currently in fullscreen mode + */ + public boolean isEditorFullscreen() { + return isFullscreen; + } + + // ==================== FULLSCREEN BUTTON ==================== + + /** + * Button to toggle fullscreen mode, displayed at top-right corner of viewport. + * Shows expand icon when minimized, collapse icon when fullscreen. + */ + + public class FullscreenButton { + public int x, y; + public int size = 12; + public boolean hovered = false; + + /** + * Initialize button position relative to viewport bounds + */ + public void initGui(int viewportEndX, int viewportY, int scrollbarOffset) { + // Position at top-right, accounting for scrollbar + this.x = viewportEndX + scrollbarOffset - size - 4; + this.y = viewportY + 4; + } + + /** + * Draw the fullscreen toggle button + */ + public void draw(int mouseX, int mouseY) { + hovered = isMouseOver(mouseX, mouseY); + int y = this.y + getYOffset(); + + // Button background + int bgColor = hovered ? 0x80666666 : 0x60444444; + GuiUtil.drawRectD(x, y, x + size, y + size, bgColor); + + // Draw corner brackets to indicate expand/collapse + int iconColor = hovered ? 0xFFFFFFFF : 0xFFAAAAAA; + int inset = 2; + int cornerLen = 3; + + if (isFullscreen) { + // Draw collapse icon (corners pointing inward) — inward L-shapes + int top = y + inset + 2; + int left = x + inset + 2; + int right = x + size - inset - 3; + int bottom = y + size - inset - 3; + + drawRect(left, top - cornerLen + 1, left + 1, top + 1, iconColor); + drawRect(left - cornerLen + 1, top, left + 1, top + 1, iconColor); + + drawRect(right, top, right + cornerLen, top + 1, iconColor); + drawRect(right, top - cornerLen + 1, right + 1, top + 1, iconColor); + + drawRect(left - cornerLen + 1, bottom, left + 1, bottom + 1, iconColor); + drawRect(left, bottom, left + 1, bottom + cornerLen, iconColor); + + drawRect(right, bottom, right + cornerLen, bottom + 1, iconColor); + drawRect(right, bottom, right + 1, bottom + cornerLen, iconColor); + + } else { + // Draw expand icon (corners pointing outward) ⌐¬⌙⌞ + // Top-left corner pointing outward + drawRect(x + inset, y + inset, x + inset + 1, y + inset + cornerLen, iconColor); + drawRect(x + inset, y + inset, x + inset + cornerLen, y + inset + 1, iconColor); + // Top-right corner pointing outward + drawRect(x + size - inset - 1, y + inset, x + size - inset, y + inset + cornerLen, iconColor); + drawRect(x + size - inset - cornerLen, y + inset, x + size - inset, y + inset + 1, iconColor); + // Bottom-left corner pointing outward + drawRect(x + inset, y + size - inset - cornerLen, x + inset + 1, y + size - inset, iconColor); + drawRect(x + inset, y + size - inset - 1, x + inset + cornerLen, y + size - inset, iconColor); + // Bottom-right corner pointing outward + drawRect(x + size - inset - 1, y + size - inset - cornerLen, x + size - inset, y + size - inset, + iconColor); + drawRect(x + size - inset - cornerLen, y + size - inset - 1, x + size - inset, y + size - inset, + iconColor); + } + } + + public void drawRect(int x1, int y1, int x2, int y2, int color) { + GuiUtil.drawRectD(x1, y1, x2, y2, color); + } + + /** + * Check if mouse is over the button + */ + public boolean isMouseOver(int mouseX, int mouseY) { + int y = this.y + getYOffset(); + return mouseX >= x && mouseX < x + size && mouseY >= y && mouseY < y + size; + } + + /** + * Handle click - returns true if click was consumed + */ + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (button == 0 && isMouseOver(mouseX, mouseY)) { + toggleFullscreen(); + return true; + } + return false; + } + + public int getYOffset() { + return GuiScriptTextArea.searchBar.getTotalHeight(); + } + } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java deleted file mode 100644 index 8b2a423cf..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java +++ /dev/null @@ -1,72 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.item.ItemScriptErrorPacket; -import kamkeel.npcs.network.packets.request.script.item.ItemScriptPacket; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.CustomItems; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.scripted.item.ScriptCustomItem; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -public class GuiScriptItem extends GuiScriptInterface { - private ScriptCustomItem item; - public static Map consoleText = new HashMap<>(); - - public GuiScriptItem() { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_ITEM)); - - this.handler = this.item = new ScriptCustomItem(new ItemStack(CustomItems.scripted_item)); - ItemScriptErrorPacket.Get(); - ItemScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("ItemScriptConsole")) { - consoleText = NBTTags.GetLongStringMap(compound.getTagList("ItemScriptConsole", 10)); - initGui(); - } else { - this.item.setMCNbt(compound); - this.item.loadScriptData(); - super.setGuiData(compound); - loaded = true; - } - } - - public void save() { - if (loaded) { - super.save(); - ItemScriptPacket.Save(this.item.getMCNbt()); - } - } - - protected String getConsoleText() { - Map map = consoleText; - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - public void confirmClicked(boolean flag, int i) { - if (i == 102) { - if (activeTab <= 0) { - consoleText = new HashMap<>(); - ItemScriptErrorPacket.Clear(); - } - } - super.confirmClicked(flag, i); - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java deleted file mode 100644 index 2e168f3fa..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ /dev/null @@ -1,416 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiConfirmOpenLink; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.StatCollector; -import noppes.npcs.NBTTags; -import noppes.npcs.NoppesStringUtils; -import noppes.npcs.client.gui.global.GuiNPCManageLinked; -import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiMenuTopButton; -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.GuiNpcTextArea; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.GuiScriptTextArea; -import noppes.npcs.client.gui.util.ICustomScrollListener; -import noppes.npcs.client.gui.util.IGuiData; -import noppes.npcs.client.gui.util.IJTextAreaListener; -import noppes.npcs.client.gui.util.ITextChangeListener; -import noppes.npcs.client.gui.util.ITextfieldListener; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.LinkedItem; -import noppes.npcs.controllers.data.LinkedItemScript; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public class GuiScriptLinkedItem extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener { - - private int activeTab = 0; - public Map> languages = new HashMap(); - private final int scriptLimit = 1; - public List hookList = new ArrayList(); - public String previousHookClicked = ""; - - public final GuiNPCManageLinked parent; - public final LinkedItem linkedItem; - private final LinkedItemScript scriptHandler; - boolean loaded = false; - - public GuiScriptLinkedItem(GuiNPCManageLinked parent, LinkedItem linkedItem) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - - this.parent = parent; - this.linkedItem = linkedItem; - this.scriptHandler = new LinkedItemScript(); - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_LINKED_ITEM)); - - LinkedItemScriptPacket.Get(linkedItem.id); - } - - public void initGui() { - this.ySize = (int) ((double) this.xSize * 0.56); - if ((double) this.ySize > (double) this.height * 0.95) { - this.ySize = (int) ((double) this.height * 0.95); - this.xSize = (int) ((double) this.ySize / 0.56); - } - - this.bgScale = (float) this.xSize / 400.0F; - super.initGui(); - this.guiTop += 10; - int yoffset = (int) ((double) this.ySize * 0.02); - GuiMenuTopButton top; - this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); - int topXoffset = 0; - int topYoffset = 0; - - - if (this.scriptHandler.container != null) { - this.addTopButton(top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); - - } else { - this.addTopButton(new GuiMenuTopButton(this.scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - } - - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); - } - - top.active = true; - if (this.activeTab == 1) { - GuiCustomScroll hooks = new GuiCustomScroll(this, 1); - hooks.allowUserInput = false; - hooks.setSize(108, 198); - hooks.guiLeft = this.guiLeft - 110; - hooks.guiTop = this.guiTop + 14; - - hooks.setUnsortedList(this.hookList); - this.addScroll(hooks); - GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, this.guiTop + 5); - hookLabel.color = 11184810; - this.addLabel(hookLabel); - ScriptContainer container = this.scriptHandler.container; - GuiScriptTextArea ta = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - ta.enableCodeHighlighting(); - ta.setListener(this); - this.addTextField(ta); - int left1 = this.guiLeft + this.xSize - 104; - this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); - this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); - this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - - this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); - this.getButton(107).enabled = false; - - GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); - scroll.setSize(100, (int) ((double) this.ySize * 0.54) - yoffset * 2); - scroll.guiLeft = left1; - scroll.guiTop = this.guiTop + 88 + yoffset; - if (container != null) { - scroll.setList(container.scripts); - } - - this.addScroll(scroll); - } else { - GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); - var8.enabled = false; - this.addTextField(var8); - int var9 = this.guiLeft + this.xSize - 150; - this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); - this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); - this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, this.languages.keySet().toArray(new String[this.languages.keySet().size()]), this.getScriptIndex())); - this.getButton(103).enabled = !this.languages.isEmpty(); - this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.scriptHandler.getEnabled() ? 1 : 0)); - - this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); - this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); - this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); - this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); - } - - this.xSize = 420; - this.ySize = 256; - } - - public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { - String hook = scroll.getSelected(); - if (this.previousHookClicked.equals(hook)) { - String addString = ""; - if (!this.getTextField(2).getText().isEmpty()) { - addString = addString + "\n"; - } - - addString = addString + "function " + hook + "(event) {\n \n}\n"; - this.getTextField(2).setText(this.getTextField(2).getText() + addString); - this.previousHookClicked = ""; - } else { - this.previousHookClicked = hook; - } - - } - - public void unFocused(GuiNpcTextField textfield) { - } - - private String getConsoleText() { - Map map = this.scriptHandler.getConsoleText(); - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - private int getScriptIndex() { - int i = 0; - - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.scriptHandler.getLanguage())) { - return i; - } - } - - return 0; - } - - public void confirmClicked(boolean flag, int i) { - if (flag) { - if (i == 0) { - this.openLink("https://www.curseforge.com/minecraft/mc-mods/customnpc-plus"); - } - - if (i == 1) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 2) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 3) { - this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); - } - - if (i == 10) { - this.scriptHandler.container = null; - this.activeTab = 0; - } - - if (i == 101) { - this.getTextField(2).setText(NoppesStringUtils.getClipboardContents()); - this.setScript(); - } - - if (i == 102) { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - container.script = ""; - } else { - this.scriptHandler.clearConsole(); - } - - this.initGui(); - } - } - - this.displayGuiScreen(this); - } - - protected void actionPerformed(GuiButton guibutton) { - if (guibutton.id == 0) { - this.setScript(); - this.activeTab = 0; - this.initGui(); - } - - if (guibutton.id == this.scriptLimit) { - if (scriptHandler.container == null) - this.scriptHandler.container = new ScriptContainer(this.scriptHandler); - else - this.setScript(); - this.activeTab = 1; - this.initGui(); - } - - if (guibutton.id == 109) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); - } - - if (guibutton.id == 110) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/KAMKEEL/CustomNPC-Plus-API", 1, true)); - } - - if (guibutton.id == 111) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/Noppes/CustomNPCsAPI", 2, true)); - } - - if (guibutton.id == 112) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "http://www.minecraftforge.net/forum/index.php/board,122.0.html", 3, true)); - } - - if (guibutton.id == 100) { - NoppesStringUtils.setClipboardContents(this.getTextField(2).getText()); - } - - if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - this.displayGuiScreen(guiyesno); - } - - GuiYesNo container1; - if (guibutton.id == 102) { - container1 = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 103) { - this.scriptHandler.setLanguage(guibutton.displayString); - } - - if (guibutton.id == 104) { - this.scriptHandler.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); - } - - if (guibutton.id == 105) { - container1 = new GuiYesNo(this, "", ((GuiNpcButton) guibutton).displayString, 10); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 107) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - -// this.setSubGui(new EventGuiScriptList((List)this.languages.get(this.script.getLanguage()), container)); - } - - } - - private void setScript() { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - - String text = this.getTextField(2).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.script = text; - } - - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - this.scriptHandler.setLanguage(compound.getString("ScriptLanguage")); - this.scriptHandler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.copiedSetGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(this.scriptHandler); - container.readFromNBT(compound.getCompoundTag("Script")); - this.scriptHandler.container = container; - this.initGui(); - } - - } - - private void copiedSetGuiData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); - } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); - } - - public void save() { - if (loaded) { - this.setScript(); - - List containers = this.scriptHandler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - LinkedItemScriptPacket.Save(linkedItem.id, i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.scriptHandler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.scriptHandler.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.scriptHandler.getConsoleText())); - - LinkedItemScriptPacket.Save(linkedItem.id, -1, containers.size(), scriptData); - } - } - - public void textUpdate(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - } - - public void saveText(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - this.initGui(); - } - - @Override - public void close() { - this.save(); - parent.setWorldAndResolution(mc, width, height); - parent.initGui(); - mc.currentScreen = parent; - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java deleted file mode 100644 index e0dfc8366..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java +++ /dev/null @@ -1,64 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.PlayerDataScript; - -import java.util.ArrayList; -import java.util.List; - -public class GuiScriptPlayers extends GuiScriptInterface { - private PlayerDataScript script = new PlayerDataScript((EntityPlayer) null); - - public GuiScriptPlayers() { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_PLAYER)); - - this.handler = this.script; - PlayerScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(script); - container.readFromNBT(compound.getCompoundTag("Script")); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - PlayerScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - PlayerScriptPacket.Save(-1, containers.size(), scriptData); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java deleted file mode 100644 index 4acdb02e4..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ /dev/null @@ -1,435 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiConfirmOpenLink; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.StatCollector; -import noppes.npcs.NBTTags; -import noppes.npcs.NoppesStringUtils; -import noppes.npcs.client.gui.global.GuiNpcManageRecipes; -import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiMenuTopButton; -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.GuiNpcTextArea; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.GuiScriptTextArea; -import noppes.npcs.client.gui.util.ICustomScrollListener; -import noppes.npcs.client.gui.util.IGuiData; -import noppes.npcs.client.gui.util.IJTextAreaListener; -import noppes.npcs.client.gui.util.ITextChangeListener; -import noppes.npcs.client.gui.util.ITextfieldListener; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.RecipeAnvil; -import noppes.npcs.controllers.data.RecipeCarpentry; -import noppes.npcs.controllers.data.RecipeScript; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public class GuiScriptRecipe extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener { - private int activeTab = 0; - public Map> languages = new HashMap(); - private final int scriptLimit = 1; - public List hookList = new ArrayList(); - public String previousHookClicked = ""; - - public final GuiNpcManageRecipes parent; - public final int recipeId; - public final boolean anvil; - - private final RecipeScript scriptHandler; - boolean loaded = false; - - public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeCarpentry recipe) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - - this.parent = parent; - - this.scriptHandler = new RecipeScript(); - this.anvil = false; - this.recipeId = recipe.id; - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_RECIPE)); - - RecipeScriptPacket.Get(false, recipeId); - } - - public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeAnvil recipe) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - - this.parent = parent; - - this.scriptHandler = new RecipeScript(); - this.anvil = true; - this.recipeId = recipe.id; - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_RECIPE)); - - RecipeScriptPacket.Get(true, recipeId); - } - - public void initGui() { - this.ySize = (int) ((double) this.xSize * 0.56); - if ((double) this.ySize > (double) this.height * 0.95) { - this.ySize = (int) ((double) this.height * 0.95); - this.xSize = (int) ((double) this.ySize / 0.56); - } - - this.bgScale = (float) this.xSize / 400.0F; - super.initGui(); - this.guiTop += 10; - int yoffset = (int) ((double) this.ySize * 0.02); - GuiMenuTopButton top; - this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); - int topXoffset = 0; - int topYoffset = 0; - - if (this.scriptHandler.container != null) { - this.addTopButton(top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); - - } else { - this.addTopButton(new GuiMenuTopButton(this.scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - } - - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); - } - - top.active = true; - if (this.activeTab == 1) { - GuiCustomScroll hooks = new GuiCustomScroll(this, 1); - hooks.allowUserInput = false; - hooks.setSize(108, 198); - hooks.guiLeft = this.guiLeft - 110; - hooks.guiTop = this.guiTop + 14; - - hooks.setUnsortedList(this.hookList); - this.addScroll(hooks); - GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, this.guiTop + 5); - hookLabel.color = 11184810; - this.addLabel(hookLabel); - ScriptContainer container = this.scriptHandler.container; - GuiScriptTextArea ta = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - ta.enableCodeHighlighting(); - ta.setListener(this); - this.addTextField(ta); - int left1 = this.guiLeft + this.xSize - 104; - this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); - this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); - this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - - this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); - this.getButton(107).enabled = false; - - GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); - scroll.setSize(100, (int) ((double) this.ySize * 0.54) - yoffset * 2); - scroll.guiLeft = left1; - scroll.guiTop = this.guiTop + 88 + yoffset; - if (container != null) { - scroll.setList(container.scripts); - } - - this.addScroll(scroll); - } else { - GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); - var8.enabled = false; - this.addTextField(var8); - int var9 = this.guiLeft + this.xSize - 150; - this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); - this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); - this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, this.languages.keySet().toArray(new String[this.languages.keySet().size()]), this.getScriptIndex())); - this.getButton(103).enabled = !this.languages.isEmpty(); - this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.scriptHandler.getEnabled() ? 1 : 0)); - - this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); - this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); - this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); - this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); - } - - this.xSize = 420; - this.ySize = 256; - } - - public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { - String hook = scroll.getSelected(); - if (this.previousHookClicked.equals(hook)) { - String addString = ""; - if (!this.getTextField(2).getText().isEmpty()) { - addString = addString + "\n"; - } - - addString = addString + "function " + hook + "(event) {\n \n}\n"; - this.getTextField(2).setText(this.getTextField(2).getText() + addString); - this.previousHookClicked = ""; - } else { - this.previousHookClicked = hook; - } - - } - - public void unFocused(GuiNpcTextField textfield) { - } - - private String getConsoleText() { - Map map = this.scriptHandler.getConsoleText(); - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - private int getScriptIndex() { - int i = 0; - - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.scriptHandler.getLanguage())) { - return i; - } - } - - return 0; - } - - public void confirmClicked(boolean flag, int i) { - if (flag) { - if (i == 0) { - this.openLink("https://www.curseforge.com/minecraft/mc-mods/customnpc-plus"); - } - - if (i == 1) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 2) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 3) { - this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); - } - - if (i == 10) { - this.scriptHandler.container = null; - this.activeTab = 0; - } - - if (i == 101) { - this.getTextField(2).setText(NoppesStringUtils.getClipboardContents()); - this.setScript(); - } - - if (i == 102) { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - container.script = ""; - } else { - this.scriptHandler.clearConsole(); - } - - this.initGui(); - } - } - - this.displayGuiScreen(this); - } - - protected void actionPerformed(GuiButton guibutton) { - if (guibutton.id == 0) { - this.setScript(); - this.activeTab = 0; - this.initGui(); - } - - if (guibutton.id == this.scriptLimit) { - if (scriptHandler.container == null) - this.scriptHandler.container = new ScriptContainer(this.scriptHandler); - else - this.setScript(); - this.activeTab = 1; - this.initGui(); - } - - if (guibutton.id == 109) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); - } - - if (guibutton.id == 110) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/KAMKEEL/CustomNPC-Plus-API", 1, true)); - } - - if (guibutton.id == 111) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/Noppes/CustomNPCsAPI", 2, true)); - } - - if (guibutton.id == 112) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "http://www.minecraftforge.net/forum/index.php/board,122.0.html", 3, true)); - } - - if (guibutton.id == 100) { - NoppesStringUtils.setClipboardContents(this.getTextField(2).getText()); - } - - if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - this.displayGuiScreen(guiyesno); - } - - GuiYesNo container1; - if (guibutton.id == 102) { - container1 = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 103) { - this.scriptHandler.setLanguage(guibutton.displayString); - } - - if (guibutton.id == 104) { - this.scriptHandler.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); - } - - if (guibutton.id == 105) { - container1 = new GuiYesNo(this, "", ((GuiNpcButton) guibutton).displayString, 10); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 107) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - -// this.setSubGui(new EventGuiScriptList((List)this.languages.get(this.script.getLanguage()), container)); - } - - } - - private void setScript() { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - - String text = this.getTextField(2).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.script = text; - } - - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - this.scriptHandler.setLanguage(compound.getString("ScriptLanguage")); - this.scriptHandler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.copiedSetGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(this.scriptHandler); - container.readFromNBT(compound.getCompoundTag("Script")); - this.scriptHandler.container = container; - this.initGui(); - } - } - - private void copiedSetGuiData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); - } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); - } - - public void save() { - if (loaded) { - this.setScript(); - - List containers = this.scriptHandler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - RecipeScriptPacket.Save(anvil, recipeId, i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.scriptHandler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.scriptHandler.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.scriptHandler.getConsoleText())); - - RecipeScriptPacket.Save(anvil, recipeId, -1, containers.size(), scriptData); - } - } - - public void textUpdate(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - } - - public void saveText(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - this.initGui(); - } - - @Override - public void close() { - this.save(); - parent.setWorldAndResolution(mc, width, height); - parent.initGui(); - mc.currentScreen = parent; - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java b/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java new file mode 100644 index 000000000..2e20a6ed5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java @@ -0,0 +1,123 @@ +package noppes.npcs.client.gui.script; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.ArrayList; +import java.util.List; + +public class SubGuiSelectList extends SubGuiInterface { + private GuiCustomScroll scroll1; + private GuiCustomScroll scroll2; + + public String allName, selectedName; + public List all; + public List selected; + + public SubGuiSelectList(List all, List selected, String allName, String selectedName) { + setBackground("menubg.png"); + xSize = 346; + ySize = 216; + this.allName = allName; + this.selectedName = selectedName; + + if (all == null) + all = new ArrayList<>(); + this.all = all; + + if (selected == null) + selected = new ArrayList<>(); + this.selected = selected; + } + + @Override + public void initGui() { + super.initGui(); + if (scroll1 == null) { + scroll1 = new GuiCustomScroll(this, 0); + scroll1.setSize(140, 180); + } + + addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize - 22, guiTop - 17, "X")); + + scroll1.guiLeft = guiLeft + 4; + scroll1.guiTop = guiTop + 14; + this.addScroll(scroll1); + addLabel(new GuiNpcLabel(1, allName, guiLeft + 4, guiTop + 4)); + + if (scroll2 == null) { + scroll2 = new GuiCustomScroll(this, 1); + scroll2.setSize(140, 180); + } + scroll2.guiLeft = guiLeft + 200; + scroll2.guiTop = guiTop + 14; + this.addScroll(scroll2); + addLabel(new GuiNpcLabel(2, selectedName, guiLeft + 200, guiTop + 4)); + + List tempAll = new ArrayList<>(all); + tempAll.removeAll(selected); + scroll1.setList(tempAll); + scroll2.setList(selected); + + addButton(new GuiNpcButton(1, guiLeft + 145, guiTop + 40, 55, 20, ">")); + addButton(new GuiNpcButton(2, guiLeft + 145, guiTop + 62, 55, 20, "<")); + + addButton(new GuiNpcButton(3, guiLeft + 145, guiTop + 90, 55, 20, ">>")); + addButton(new GuiNpcButton(4, guiLeft + 145, guiTop + 112, 55, 20, "<<")); + + addButton(new GuiNpcButton(66, guiLeft + 260, guiTop + 194, 60, 20, "gui.done")); + } + + protected void actionPerformed(GuiButton guibutton) { + GuiNpcButton button = (GuiNpcButton) guibutton; + + if (button.id == -1) { + close(); + return; + } + + if (button.id == 1) { + if (scroll1.hasSelected()) { + selected.add(scroll1.getSelected()); + + scroll1.selected = -1; + scroll2.selected = -1; + initGui(); + } + } + if (button.id == 2) { + if (scroll2.hasSelected()) { + selected.remove(scroll2.getSelected()); + scroll2.selected = -1; + initGui(); + } + } + if (button.id == 3) { + selected.clear(); + for (String type : all) + selected.add(type); + + scroll1.selected = -1; + scroll2.selected = -1; + initGui(); + } + if (button.id == 4) { + selected.clear(); + scroll1.selected = -1; + scroll2.selected = -1; + initGui(); + } + if (button.id == 66) { + close(); + } + } + + @Override + public void save() { + + } +} diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java index 882f5817b..2a1f04344 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java @@ -19,20 +19,42 @@ import java.util.Vector; /** * Animation selection dialog for choosing global animations. - * Requests animation data from the server via packets. + * Toggle between Built-in animations (by name) and Custom animations (by ID). */ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScrollListener, IScrollData { private GuiCustomScroll scrollAnimations; - private HashMap animationData = new HashMap(); + // Built-in animations (name only, no ID) + private HashMap builtInData = new HashMap<>(); + // Custom/user animations (name -> ID) + private HashMap customData = new HashMap<>(); + + // View mode: true = built-in, false = custom + private boolean showingBuiltIn = true; + + // Initial values passed in private int initialAnimationId; - public int selectedAnimationId = -1; - private String selectedName = null; + private String initialAnimationName; + // Selected values to return + public int selectedAnimationId = -1; + public String selectedBuiltInName = ""; + + private String selectedCustomName = null; private String search = ""; + /** + * Constructor for selecting by ID (user animations). + */ public GuiAnimationSelection(int animationId) { + this(animationId, ""); + } + + /** + * Constructor for selecting by name (built-in) or ID (user). + */ + public GuiAnimationSelection(int animationId, String animationName) { drawDefaultBackground = false; title = ""; setBackground("menubg.png"); @@ -40,7 +62,12 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr ySize = 226; this.initialAnimationId = animationId; + this.initialAnimationName = animationName != null ? animationName : ""; this.selectedAnimationId = animationId; + this.selectedBuiltInName = this.initialAnimationName; + + // Start showing built-in if we have a built-in name, otherwise custom + this.showingBuiltIn = !this.initialAnimationName.isEmpty(); // Request animation data from server PacketClient.sendClient(new AnimationsGetPacket()); @@ -50,55 +77,87 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr public void initGui() { super.initGui(); - this.addLabel(new GuiNpcLabel(0, "menu.animations", guiLeft + 8, guiTop + 4)); + int y = guiTop + 4; - this.addButton(new GuiNpcButton(1, guiLeft + 4, guiTop + ySize - 35, 50, 20, "gui.clear")); - this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, guiTop + ySize - 35, 50, 20, "gui.done")); - this.addButton(new GuiNpcButton(3, guiLeft + xSize - 108, guiTop + ySize - 35, 50, 20, "gui.cancel")); + // Toggle button for switching views + String viewLabel = showingBuiltIn ? "gui.builtinAnimations" : "gui.customAnimations"; + this.addButton(new GuiNpcButton(10, guiLeft + 4, y, 212, 20, viewLabel)); + y += 24; + // Animation scroll list if (scrollAnimations == null) { scrollAnimations = new GuiCustomScroll(this, 0, 0); - scrollAnimations.setSize(212, 163); + scrollAnimations.setSize(212, 130); } scrollAnimations.setList(getSearchList()); - if (selectedName != null) { - scrollAnimations.setSelected(selectedName); - } - scrollAnimations.guiLeft = guiLeft + 4; - scrollAnimations.guiTop = guiTop + 14; - this.addScroll(scrollAnimations); - addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, guiTop + 179, 212, 20, search)); + // Set selection based on current mode + if (showingBuiltIn && !selectedBuiltInName.isEmpty()) { + scrollAnimations.setSelected(selectedBuiltInName); + } else if (!showingBuiltIn && selectedCustomName != null) { + scrollAnimations.setSelected(selectedCustomName); + } + + scrollAnimations.guiLeft = guiLeft + 4; + scrollAnimations.guiTop = y; + this.addScroll(scrollAnimations); + y += 134; + + // Search textbox + addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, y, 212, 20, search)); + y += 24; + + // Buttons + this.addButton(new GuiNpcButton(1, guiLeft + 4, y, 50, 20, "gui.clear")); + this.addButton(new GuiNpcButton(3, guiLeft + xSize - 108, y, 50, 20, "gui.cancel")); + this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, y, 50, 20, "gui.done")); } @Override public void setData(Vector list, HashMap data, EnumScrollData type) { - this.animationData = data; + if (type == EnumScrollData.BUILTIN_ANIMATIONS) { + this.builtInData = data; - // Find the name for our initial animation ID - if (initialAnimationId >= 0) { - for (String name : data.keySet()) { - if (data.get(name) == initialAnimationId) { - selectedName = name; - break; + // Check if initial name matches a built-in animation + if (!initialAnimationName.isEmpty()) { + for (String name : builtInData.keySet()) { + if (name.equalsIgnoreCase(initialAnimationName)) { + selectedBuiltInName = name; + selectedAnimationId = -1; + selectedCustomName = null; + showingBuiltIn = true; + break; + } + } + } + } else if (type == EnumScrollData.ANIMATIONS) { + this.customData = data; + + // Find the name for our initial animation ID (if not using built-in) + if (selectedBuiltInName.isEmpty() && initialAnimationId >= 0) { + for (String name : data.keySet()) { + if (data.get(name) == initialAnimationId) { + selectedCustomName = name; + showingBuiltIn = false; + break; + } } } } if (scrollAnimations != null) { scrollAnimations.setList(getSearchList()); - if (selectedName != null) { - scrollAnimations.setSelected(selectedName); + if (showingBuiltIn && !selectedBuiltInName.isEmpty()) { + scrollAnimations.setSelected(selectedBuiltInName); + } else if (!showingBuiltIn && selectedCustomName != null) { + scrollAnimations.setSelected(selectedCustomName); } } } @Override public void setSelected(String selected) { - this.selectedName = selected; - if (scrollAnimations != null) { - scrollAnimations.setSelected(selected); - } + // Not used in this implementation } @Override @@ -109,18 +168,22 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr if (search.equals(getTextField(55).getText())) return; search = getTextField(55).getText().toLowerCase(); - scrollAnimations.resetScroll(); - scrollAnimations.setList(getSearchList()); + if (scrollAnimations != null) { + scrollAnimations.resetScroll(); + scrollAnimations.setList(getSearchList()); + } } } } private List getSearchList() { + HashMap sourceData = showingBuiltIn ? builtInData : customData; + if (search.isEmpty()) { - return new ArrayList(animationData.keySet()); + return new ArrayList<>(sourceData.keySet()); } - List list = new ArrayList(); - for (String name : animationData.keySet()) { + List list = new ArrayList<>(); + for (String name : sourceData.keySet()) { if (name.toLowerCase().contains(search)) list.add(name); } @@ -129,11 +192,20 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { - if (guiCustomScroll.id == 0) { - String selected = scrollAnimations.getSelected(); - if (selected != null && animationData.containsKey(selected)) { - selectedName = selected; - selectedAnimationId = animationData.get(selected); + String selected = scrollAnimations.getSelected(); + if (selected == null) return; + + if (showingBuiltIn) { + if (builtInData.containsKey(selected)) { + selectedBuiltInName = selected; + selectedAnimationId = -1; + selectedCustomName = null; + } + } else { + if (customData.containsKey(selected)) { + selectedCustomName = selected; + selectedAnimationId = customData.get(selected); + selectedBuiltInName = ""; } } initGui(); @@ -141,27 +213,40 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (selectedAnimationId < 0) - return; - close(); + // Double-click to confirm selection + if (!selectedBuiltInName.isEmpty() || selectedAnimationId >= 0) { + close(); + } } @Override protected void actionPerformed(GuiButton guibutton) { int id = guibutton.id; - if (id == 1) { - // Clear selection - selectedName = null; + + // Toggle view button + if (id == 10) { + showingBuiltIn = !showingBuiltIn; + search = ""; + if (scrollAnimations != null) { + scrollAnimations.resetScroll(); + } + initGui(); + } + // Clear + else if (id == 1) { + selectedBuiltInName = ""; selectedAnimationId = -1; + selectedCustomName = null; close(); } - if (id == 2) { - // Done - keep selection + // Done + else if (id == 2) { close(); } - if (id == 3) { - // Cancel - restore initial value + // Cancel + else if (id == 3) { selectedAnimationId = initialAnimationId; + selectedBuiltInName = initialAnimationName; close(); } } @@ -170,6 +255,16 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr * Get the name of the selected animation (for display purposes). */ public String getSelectedName() { - return selectedName; + if (!selectedBuiltInName.isEmpty()) { + return selectedBuiltInName; + } + return selectedCustomName; + } + + /** + * Check if a built-in animation was selected. + */ + public boolean isBuiltInSelected() { + return !selectedBuiltInName.isEmpty(); } } diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java index 4f1bccfaf..3b8fda84d 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java @@ -371,6 +371,7 @@ public class GuiTextureSelection extends SubGuiInterface implements ICustomScrol selectedDomain = domain; this.location = location; } + static class TextureData { String domain; String absoluteName; diff --git a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java new file mode 100644 index 000000000..3dce761d5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java @@ -0,0 +1,368 @@ +package noppes.npcs.client.gui.util; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityProjectile; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.world.World; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.AnimationData; +import noppes.npcs.entity.EntityCustomNpc; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Client-side executor for previewing abilities in the GUI. + * + * Lifecycle: + * 1. WINDUP phase - plays windup animation, entities spawn and charge + * 2. ACTIVE phase - plays active animation, entities fire and move + * 3. EXTENDED phase - continues for +100 ticks after animations to see results + * 4. IDLE - preview complete + * + * Features: + * - Invisible fake target NPC placed in front of the preview NPC for targeting + * - NPC movement tracking for abilities that move the NPC (e.g. slam) + */ +@SideOnly(Side.CLIENT) +public class AbilityPreviewExecutor { + + // ==================== PREVIEW STATE ==================== + private Ability previewAbility; + private EntityNPCInterface previewNpc; + private AbilityPhase phase = AbilityPhase.IDLE; + private int currentTick = 0; + private boolean playing = false; + private boolean paused = false; + + // ==================== TIMING ==================== + private static final int EXTENDED_DURATION = 100; + private int activeDuration = 40; + + // ==================== FAKE TARGET ==================== + /** Invisible fake target NPC for ability targeting (placed 5 blocks in front of preview NPC) */ + private EntityNPCInterface fakeTarget; + private static final double TARGET_DISTANCE = 5.0; + + // ==================== NPC START POSITION ==================== + /** Saved NPC position for restoring after movement abilities */ + private double npcStartX, npcStartY, npcStartZ; + + // ==================== PREVIEW CONTENT ==================== + private TelegraphInstance previewTelegraph; + private List previewEntities = new ArrayList<>(); + + // ==================== CALLBACKS ==================== + private GuiAbilityInterface parentGui; + + public AbilityPreviewExecutor() { + } + + public void setParentGui(GuiAbilityInterface gui) { + this.parentGui = gui; + } + + /** + * Start previewing an ability. + */ + public void startPreview(Ability ability, EntityNPCInterface npc) { + stop(); + + this.previewAbility = ability; + this.previewNpc = npc; + this.phase = AbilityPhase.WINDUP; + this.currentTick = 0; + this.playing = true; + this.paused = false; + this.activeDuration = ability.getPreviewActiveDuration(); + + // Save NPC start position for restoring later + this.npcStartX = npc.posX; + this.npcStartY = npc.posY; + this.npcStartZ = npc.posZ; + + // Create invisible fake target NPC for targeting + createFakeTarget(npc); + + // Set preview target on the ability + ability.setPreviewTarget(fakeTarget); + + // Start tracking NPC movement + if (parentGui != null) { + parentGui.startTrackingMovement(); + } + + // Create telegraph for preview + previewTelegraph = ability.createTelegraph(npc, fakeTarget); + + // Start windup animation + Animation windUpAnim = ability.getWindUpAnimation(); + if (windUpAnim != null) { + AnimationData data = npc.display.animationData; + data.setEnabled(true); + data.setAnimation(windUpAnim); + data.animation.paused = false; + } + + // Spawn preview entity on first tick if ability spawns during windup + if (ability.spawnPreviewDuringWindup()) { + spawnPreviewEntity(); + } + } + + /** + * Create an invisible fake target NPC placed in front of the preview NPC. + * This entity is NOT rendered - it only exists for ability targeting logic. + */ + private void createFakeTarget(EntityNPCInterface npc) { + if (npc.worldObj == null) return; + + try { + fakeTarget = new EntityCustomNpc(npc.worldObj); + // Position target in front of NPC based on its facing direction + float facingYaw = GuiAbilityInterface.NPC_FACING_YAW; + double yawRad = Math.toRadians(facingYaw); + double targetX = npc.posX - Math.sin(yawRad) * TARGET_DISTANCE; + double targetY = npc.posY; + double targetZ = npc.posZ + Math.cos(yawRad) * TARGET_DISTANCE; + fakeTarget.setPosition(targetX, targetY, targetZ); + fakeTarget.prevPosX = targetX; + fakeTarget.prevPosY = targetY; + fakeTarget.prevPosZ = targetZ; + } catch (Exception e) { + fakeTarget = null; + } + } + + /** + * Get the fake target entity for abilities that need a target. + */ + public EntityLivingBase getFakeTarget() { + return fakeTarget; + } + + public void play() { + if (previewAbility == null || phase == AbilityPhase.IDLE) { + return; + } + playing = true; + paused = false; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + if (data.animation != null) { + data.animation.paused = false; + } + } + } + + public void pause() { + paused = true; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + if (data.animation != null) { + data.animation.paused = true; + } + } + } + + public void stop() { + playing = false; + paused = false; + phase = AbilityPhase.IDLE; + currentTick = 0; + + previewTelegraph = null; + + if (parentGui != null) { + for (Entity entity : previewEntities) { + parentGui.removePreviewEntity(entity); + } + parentGui.stopTrackingMovement(); + parentGui.setPreviewTelegraph(null); + } + previewEntities.clear(); + + // Reset NPC position to start position (undo movement from Slam/Charge/Dash) + if (previewNpc != null && npcStartX != 0 && npcStartY != 0 && npcStartZ != 0) { + previewNpc.posX = npcStartX; + previewNpc.posY = npcStartY; + previewNpc.posZ = npcStartZ; + previewNpc.prevPosX = npcStartX; + previewNpc.prevPosY = npcStartY; + previewNpc.prevPosZ = npcStartZ; + } + + // Remove fake target + fakeTarget = null; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + data.setAnimation(new Animation()); + data.animation.paused = false; + } + } + + /** + * Tick the preview forward. + */ + public void tick() { + if (!playing || paused || previewAbility == null || previewNpc == null) { + return; + } + + currentTick++; + + if (previewTelegraph != null) { + previewTelegraph.tick(null); + } + + tickEntities(); + + switch (phase) { + case WINDUP: + tickWindup(); + break; + case ACTIVE: + tickActive(); + break; + case IDLE: + tickExtended(); + break; + default: + break; + } + } + + private void tickWindup() { + previewAbility.onPreviewWindUpTick(previewNpc, currentTick); + + if (currentTick >= previewAbility.getWindUpTicks()) { + transitionToActive(); + } + } + + private void tickActive() { + previewAbility.onPreviewActiveTick(previewNpc, currentTick); + + if (currentTick == 1 && !previewAbility.spawnPreviewDuringWindup()) { + spawnPreviewEntity(); + fireEntities(); + } + + if (currentTick >= activeDuration) { + transitionToExtended(); + } + } + + private void tickExtended() { + if (currentTick >= EXTENDED_DURATION) { + playing = false; + } + } + + private void transitionToActive() { + phase = AbilityPhase.ACTIVE; + currentTick = 0; + + previewTelegraph = null; + + // Notify ability of execution (for movement abilities like Slam/Charge/Dash) + previewAbility.onPreviewExecute(previewNpc); + + fireEntities(); + + Animation activeAnim = previewAbility.getActiveAnimation(); + if (activeAnim != null && previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + data.setAnimation(activeAnim); + data.animation.paused = false; + } + } + + private void transitionToExtended() { + phase = AbilityPhase.IDLE; + currentTick = 0; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + data.setAnimation(new Animation()); + } + } + + private void spawnPreviewEntity() { + Entity entity = previewAbility.createPreviewEntity(previewNpc); + if (entity != null) { + previewEntities.add(entity); + if (parentGui != null) { + parentGui.addPreviewEntity(entity); + } + } + } + + private void fireEntities() { + for (Entity entity : previewEntities) { + if (entity instanceof EntityAbilityOrb) { + ((EntityAbilityOrb) entity).startPreviewFiring(); + } else if (entity instanceof EntityAbilityBeam) { + ((EntityAbilityBeam) entity).startPreviewFiring(); + } else if (entity instanceof EntityAbilityDisc) { + ((EntityAbilityDisc) entity).startPreviewFiring(); + } + } + } + + private void tickEntities() { + Iterator iter = previewEntities.iterator(); + while (iter.hasNext()) { + Entity entity = iter.next(); + if (entity == null || entity.isDead) { + if (parentGui != null) { + parentGui.removePreviewEntity(entity); + } + iter.remove(); + continue; + } + entity.onUpdate(); + } + } + + // ==================== GETTERS ==================== + + public boolean isPlaying() { return playing; } + public boolean isPaused() { return paused; } + public boolean isActive() { return playing || paused; } + public AbilityPhase getPhase() { return phase; } + public int getCurrentTick() { return currentTick; } + public TelegraphInstance getTelegraph() { return previewTelegraph; } + public List getPreviewEntities() { return previewEntities; } + public Ability getPreviewAbility() { return previewAbility; } + public EntityNPCInterface getPreviewNpc() { return previewNpc; } + + public String getStatusString() { + if (!playing && !paused) { + return "Stopped"; + } else if (paused) { + return "Paused"; + } else if (phase == AbilityPhase.WINDUP) { + return "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0); + } else if (phase == AbilityPhase.ACTIVE) { + return "Active: " + currentTick + "/" + activeDuration; + } else if (phase == AbilityPhase.IDLE && playing) { + return "Extended: " + currentTick + "/" + EXTENDED_DURATION; + } + return ""; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java new file mode 100644 index 000000000..ea8dcc0b5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java @@ -0,0 +1,391 @@ +package noppes.npcs.client.gui.util; + +import kamkeel.npcs.client.renderer.TelegraphRenderer; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.client.ClientEventHandler; +import noppes.npcs.client.EntityUtil; +import noppes.npcs.entity.EntityCustomNpc; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.entity.data.ModelData; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base GUI class for ability preview with fixed camera. + * + * Key concepts: + * - NPC positioned on left side of preview area + * - NPC faces a fixed direction (NPC_FACING_YAW) + * - Viewing direction can be rotated (rotates entire 3D space) + * - Fixed zoom level + * - Preview entities rendered in same coordinate space as NPC + * - NPC position changes (e.g. slam jump) tracked and rendered + */ +public class GuiAbilityInterface extends GuiNPCInterface2 { + + // ==================== FIXED CAMERA SETTINGS ==================== + /** Fixed camera zoom level (higher = more zoomed in) */ + protected static float CAMERA_ZOOM = 45.0f; + /** NPC facing direction - affects anchor points and visual facing */ + protected static float NPC_FACING_YAW = 160.0f; + /** Camera pitch (slight downward angle) */ + protected static float CAMERA_PITCH = 10.0f; + + // ==================== VIEWING ROTATION ==================== + /** Viewing direction rotation (rotates entire 3D space) */ + protected static float viewRotation = 0.0f; + + private GuiNpcButton btnLeft, btnRight; + + // ==================== NPC STATE ==================== + public ModelData playerdata; + public EntityNPCInterface npc; + + /** NPC starting position (saved when preview starts, used to track movement) */ + protected double npcStartX, npcStartY, npcStartZ; + /** Whether we're tracking NPC movement */ + protected boolean trackingNpcMovement = false; + + /** Entities to render in preview space */ + protected List previewEntities = new ArrayList<>(); + + /** Current telegraph to render in preview */ + protected TelegraphInstance previewTelegraph; + + // ==================== LAYOUT ==================== + /** Preview area bounds */ + protected int previewX, previewY, previewWidth, previewHeight; + /** NPC render position on screen */ + protected int npcScreenX, npcScreenY; + + public int xOffset = 0; + public int yOffset = 0; + + public GuiAbilityInterface(EntityNPCInterface npc) { + super(npc); + this.npc = npc; + this.playerdata = ((EntityCustomNpc) npc).modelData; + this.drawDefaultBackground = false; + + CAMERA_ZOOM = 30f; + NPC_FACING_YAW = 310f; + CAMERA_PITCH = 5f; + } + + @Override + public void initGui() { + super.initGui(); + + // Calculate preview area (left side of GUI) + previewX = guiLeft + 10; + previewY = guiTop + 10; + previewWidth = 200; + previewHeight = 180; + + // NPC position: left side of preview, slightly below center + npcScreenX = previewX + (int)(previewWidth * 0.33f) + xOffset; + npcScreenY = previewY + (int)(previewHeight * 0.85f) + yOffset; + + // Viewing rotation buttons (below preview area) + int btnY = guiTop + 192; + addButton(btnLeft = new GuiNpcButton(680, guiLeft + 10, btnY, 20, 20, "<")); + addButton(btnRight = new GuiNpcButton(681, guiLeft + 32, btnY, 20, 20, ">")); + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + // Handle held rotation buttons + if (Mouse.isButtonDown(0)) { + if (btnLeft != null && btnLeft.mousePressed(mc, mouseX, mouseY)) { + viewRotation += partialTicks * 2; + } else if (btnRight != null && btnRight.mousePressed(mc, mouseX, mouseY)) { + viewRotation -= partialTicks * 2; + } + } + + // Draw background and UI elements + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw the 3D preview + drawPreview(mouseX, mouseY, partialTicks); + } + + /** + * Draw the 3D preview with NPC and entities. + */ + protected void drawPreview(int mouseX, int mouseY, float partialTicks) { + GL11.glColor4f(1, 1, 1, 1); + + // Enable scissor to clip rendering to preview area + GL11.glEnable(GL11.GL_SCISSOR_TEST); + setScissorClip(previewX, previewY, previewWidth, previewHeight); + + // Get the entity to render (could be transformed player model) + EntityLivingBase renderEntity = playerdata.getEntity(npc); + if (renderEntity == null) { + renderEntity = this.npc; + } + + // Sync NPC properties to render entity + EntityUtil.Copy(npc, renderEntity); + + // Set NPC facing direction (affects anchor points and visual facing) + npc.prevRenderYawOffset = npc.renderYawOffset = NPC_FACING_YAW; + npc.prevRotationYaw = npc.rotationYaw = NPC_FACING_YAW; + npc.prevRotationYawHead = npc.rotationYawHead = NPC_FACING_YAW; + + // Also set on render entity + renderEntity.prevRenderYawOffset = renderEntity.renderYawOffset = NPC_FACING_YAW; + renderEntity.prevRotationYaw = renderEntity.rotationYaw = NPC_FACING_YAW; + renderEntity.prevRotationYawHead = renderEntity.rotationYawHead = NPC_FACING_YAW; + renderEntity.rotationPitch = 0; + + // Calculate NPC position delta for movement rendering (e.g. slam jump) + // Interpolate with partial ticks for smooth movement between frames + double npcDeltaX = 0, npcDeltaY = 0, npcDeltaZ = 0; + if (trackingNpcMovement) { + double interpX = npc.prevPosX + (npc.posX - npc.prevPosX) * partialTicks; + double interpY = npc.prevPosY + (npc.posY - npc.prevPosY) * partialTicks; + double interpZ = npc.prevPosZ + (npc.posZ - npc.prevPosZ) * partialTicks; + npcDeltaX = interpX - npcStartX; + npcDeltaY = interpY - npcStartY; + npcDeltaZ = interpZ - npcStartZ; + } + + // ==================== BEGIN 3D RENDERING ==================== + GL11.glEnable(GL11.GL_COLOR_MATERIAL); + GL11.glPushMatrix(); + + // Move to NPC render position on screen + GL11.glTranslatef(npcScreenX, npcScreenY, 300F); + + // Apply zoom (scale) + GL11.glScalef(-CAMERA_ZOOM, CAMERA_ZOOM, CAMERA_ZOOM); + + // Flip for screen coordinates (Y is inverted) + GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F); + + // Apply slight camera pitch (looking down at scene) + GL11.glRotatef(CAMERA_PITCH, 1.0F, 0.0F, 0.0F); + + // Apply VIEWING rotation (rotates entire space, not just NPC) + GL11.glRotatef(viewRotation, 0.0F, 1.0F, 0.0F); + + // Setup lighting (standard item lighting for consistent colors) + GL11.glRotatef(135F, 0.0F, 1.0F, 0.0F); + RenderHelper.enableStandardItemLighting(); + GL11.glRotatef(-135F, 0.0F, 1.0F, 0.0F); + + // Adjust for entity offset + GL11.glTranslatef(0.0F, renderEntity.yOffset, 0.0F); + + RenderManager.instance.playerViewY = 180F; + ClientEventHandler.renderingEntityInGUI = true; + + // Render the NPC at its position delta (for movement like slam jumps) + try { + RenderManager.instance.renderEntityWithPosYaw(renderEntity, npcDeltaX, npcDeltaY, npcDeltaZ, 0.0F, partialTicks); + } catch (Exception e) { + playerdata.setEntityClass(null); + } + + // Render preview entities at their positions relative to NPC start position + renderPreviewEntities(partialTicks); + + // Render telegraph in preview space + renderPreviewTelegraph(partialTicks); + + ClientEventHandler.renderingEntityInGUI = false; + + GL11.glPopMatrix(); + + // Cleanup GL state + RenderHelper.disableStandardItemLighting(); + GL11.glDisable(GL12.GL_RESCALE_NORMAL); + OpenGlHelper.setActiveTexture(OpenGlHelper.lightmapTexUnit); + GL11.glDisable(GL11.GL_TEXTURE_2D); + OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit); + + // Disable scissor + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + // Clear depth buffer so UI renders on top + GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT); + } + + /** + * Render preview entities in the same coordinate space as the NPC. + * Called WITHIN the 3D matrix transformation. + */ + protected void renderPreviewEntities(float partialTicks) { + // Save current GL state that NPC render may have changed + boolean lightingWasEnabled = GL11.glIsEnabled(GL11.GL_LIGHTING); + + // Use the start position as the reference point for entity offsets + double refX = trackingNpcMovement ? npcStartX : npc.posX; + double refY = trackingNpcMovement ? npcStartY : npc.posY; + double refZ = trackingNpcMovement ? npcStartZ : npc.posZ; + + for (Entity entity : previewEntities) { + if (entity == null || entity.isDead) continue; + + // Calculate entity position relative to reference (NPC start position) + double offsetX = entity.prevPosX + (entity.posX - entity.prevPosX) * partialTicks - refX; + double offsetY = entity.prevPosY + (entity.posY - entity.prevPosY) * partialTicks - refY; + double offsetZ = entity.prevPosZ + (entity.posZ - entity.prevPosZ) * partialTicks - refZ; + + // The NPC model was translated by npc.yOffset, so we need to subtract it + offsetY -= npc.yOffset; + + // Reset GL state before each entity to ensure colors render correctly + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + GL11.glDisable(GL11.GL_LIGHTING); + + try { + RenderManager.instance.renderEntityWithPosYaw(entity, offsetX, offsetY, offsetZ, entity.rotationYaw, partialTicks); + } catch (Exception e) { + // Ignore rendering errors + } + } + + // Restore lighting state for any subsequent rendering + if (lightingWasEnabled) { + GL11.glEnable(GL11.GL_LIGHTING); + } + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + } + + /** + * Render the telegraph in the 3D preview space. + * Called within the GL matrix transformation. + */ + protected void renderPreviewTelegraph(float partialTicks) { + if (previewTelegraph == null) return; + if (TelegraphRenderer.Instance == null) return; + + // Telegraph position is in world coords - convert to preview space + double refX = trackingNpcMovement ? npcStartX : npc.posX; + double refY = trackingNpcMovement ? npcStartY : npc.posY; + double refZ = trackingNpcMovement ? npcStartZ : npc.posZ; + + double offsetX = previewTelegraph.getInterpolatedX(partialTicks) - refX; + double offsetY = previewTelegraph.getInterpolatedY(partialTicks) - refY; + double offsetZ = previewTelegraph.getInterpolatedZ(partialTicks) - refZ; + + // Adjust for NPC yOffset (same as entities) + offsetY -= npc.yOffset; + + TelegraphRenderer.Instance.renderTelegraphInGUI(previewTelegraph, offsetX, offsetY, offsetZ, 1.0f, partialTicks); + } + + public void setPreviewTelegraph(TelegraphInstance telegraph) { + this.previewTelegraph = telegraph; + } + + /** + * Set scissor clip for preview area. + */ + protected void setScissorClip(int x, int y, int width, int height) { + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scale = sr.getScaleFactor(); + int scaledY = mc.displayHeight - (y + height) * scale; + GL11.glScissor(x * scale, scaledY, width * scale, height * scale); + } + + // ==================== MOVEMENT TRACKING ==================== + + /** + * Start tracking NPC movement from current position. + */ + public void startTrackingMovement() { + npcStartX = npc.posX; + npcStartY = npc.posY; + npcStartZ = npc.posZ; + trackingNpcMovement = true; + } + + /** + * Stop tracking NPC movement. + */ + public void stopTrackingMovement() { + trackingNpcMovement = false; + } + + // ==================== PREVIEW ENTITY MANAGEMENT ==================== + + public void addPreviewEntity(Entity entity) { + if (entity != null && !previewEntities.contains(entity)) { + previewEntities.add(entity); + } + } + + public void removePreviewEntity(Entity entity) { + previewEntities.remove(entity); + } + + public void clearPreviewEntities() { + previewEntities.clear(); + } + + public List getPreviewEntities() { + return previewEntities; + } + + public float getNpcFacingYaw() { + return NPC_FACING_YAW; + } + + // ==================== BACKGROUND ==================== + + @Override + protected void drawBackground() { + super.drawBackground(); + + drawGradientRect(previewX, previewY, previewX + previewWidth, previewY + previewHeight, + 0xc0101010, 0xd0101010); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + } + + // ==================== STANDARD OVERRIDES ==================== + + @Override + public boolean doesGuiPauseGame() { + return false; + } + + @Override + public void keyTyped(char par1, int par2) { + super.keyTyped(par1, par2); + if (par2 == 1) { + close(); + } + } + + public void close() { + this.mc.displayGuiScreen((GuiScreen) null); + this.mc.setIngameFocus(); + } + + @Override + public void save() { + } + + @Override + protected void actionPerformed(GuiButton btn) { + if (btn.id == 670) { + close(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiAuctionNavButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiAuctionNavButton.java new file mode 100644 index 000000000..665dd2259 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/GuiAuctionNavButton.java @@ -0,0 +1,197 @@ +package noppes.npcs.client.gui.util; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.entity.RenderItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.util.ArrayList; +import java.util.List; + +/** + * Configurable textured button for auction GUI. + * Supports custom button sizes, icon sizes, and textures. + */ +public class GuiAuctionNavButton extends GuiNpcButton { + // Default auction button textures (18x18) + private static final ResourceLocation DEFAULT_NORMAL = new ResourceLocation("customnpcs", "textures/gui/auction/button.png"); + private static final ResourceLocation DEFAULT_PRESSED = new ResourceLocation("customnpcs", "textures/gui/auction/button_press.png"); + + // Textures + private ResourceLocation textureNormal; + private ResourceLocation texturePressed; + private ResourceLocation iconTexture; + private ItemStack iconItem; + + // Dimensions + private int buttonSize; + private int iconSize; + + // State + private boolean selected; + private boolean isToggle; // Toggle buttons can be clicked when selected, nav buttons cannot + private final String tooltipKey; + private List customTooltip; + + /** + * Create a button with default 18x18 size and 16x16 icon using default textures. + */ + public GuiAuctionNavButton(int id, int x, int y, String tooltipKey, ResourceLocation iconTexture) { + this(id, x, y, 18, 16, tooltipKey, iconTexture, DEFAULT_NORMAL, DEFAULT_PRESSED); + } + + /** + * Create a button with default 18x18 size and 16x16 icon with an item icon. + */ + public GuiAuctionNavButton(int id, int x, int y, String tooltipKey, ItemStack iconItem) { + super(id, x, y, 18, 18, ""); + this.tooltipKey = tooltipKey; + this.iconTexture = null; + this.iconItem = iconItem; + this.buttonSize = 18; + this.iconSize = 16; + this.textureNormal = DEFAULT_NORMAL; + this.texturePressed = DEFAULT_PRESSED; + this.selected = false; + this.customTooltip = null; + } + + /** + * Create a fully customizable button. + * + * @param id Button ID + * @param x X position + * @param y Y position + * @param buttonSize Size of the button background texture (e.g., 18 for 18x18, 10 for 10x10) + * @param iconSize Size of the icon texture (e.g., 16 for 16x16, 8 for 8x8) + * @param tooltipKey Lang key for tooltip + * @param iconTexture Icon texture to draw inside the button + * @param textureNormal Background texture for normal state + * @param texturePressed Background texture for pressed/hover state + */ + public GuiAuctionNavButton(int id, int x, int y, int buttonSize, int iconSize, String tooltipKey, + ResourceLocation iconTexture, ResourceLocation textureNormal, ResourceLocation texturePressed) { + super(id, x, y, buttonSize, buttonSize, ""); + this.tooltipKey = tooltipKey; + this.iconTexture = iconTexture; + this.iconItem = null; + this.buttonSize = buttonSize; + this.iconSize = iconSize; + this.textureNormal = textureNormal; + this.texturePressed = texturePressed; + this.selected = false; + this.customTooltip = null; + } + + public void setSelected(boolean selected) { + this.selected = selected; + } + + public boolean isSelected() { + return selected; + } + + /** + * Set whether this is a toggle button. + * Toggle buttons can be clicked when selected (to toggle off). + * Nav buttons cannot be clicked when selected. + */ + public GuiAuctionNavButton setToggle(boolean toggle) { + this.isToggle = toggle; + return this; + } + + public boolean isHovered() { + return field_146123_n; + } + + /** + * Set a custom multi-line tooltip. If null, uses the default tooltipKey translation. + */ + public void setCustomTooltip(List tooltip) { + this.customTooltip = tooltip; + } + + /** + * Get the tooltip lines for display. + */ + public List getTooltipLines() { + if (customTooltip != null && !customTooltip.isEmpty()) { + return customTooltip; + } + List lines = new ArrayList<>(); + if (tooltipKey != null && !tooltipKey.isEmpty()) { + lines.add(StatCollector.translateToLocal(tooltipKey)); + } + return lines; + } + + public String getTooltipText() { + return tooltipKey != null ? StatCollector.translateToLocal(tooltipKey) : ""; + } + + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY) { + if (!this.visible) { + return; + } + + // Check if mouse is hovering + boolean hovered = mouseX >= xPosition && mouseX < xPosition + width + && mouseY >= yPosition && mouseY < yPosition + height; + field_146123_n = hovered; + + // Draw button background + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + if (selected || hovered) { + mc.getTextureManager().bindTexture(texturePressed); + } else { + mc.getTextureManager().bindTexture(textureNormal); + } + Gui.func_146110_a(xPosition, yPosition, 0, 0, buttonSize, buttonSize, buttonSize, buttonSize); + + // Calculate icon offset to center it + int iconOffset = (buttonSize - iconSize) / 2; + int iconX = xPosition + iconOffset; + int iconY = yPosition + iconOffset; + + if (iconItem != null) { + // Render item + RenderHelper.enableGUIStandardItemLighting(); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + RenderItem itemRenderer = RenderItem.getInstance(); + itemRenderer.renderItemAndEffectIntoGUI(mc.fontRenderer, mc.renderEngine, iconItem, iconX, iconY); + GL11.glDisable(GL11.GL_LIGHTING); + RenderHelper.disableStandardItemLighting(); + } else if (iconTexture != null) { + // Render texture icon + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + mc.getTextureManager().bindTexture(iconTexture); + Gui.func_146110_a(iconX, iconY, 0, 0, iconSize, iconSize, iconSize, iconSize); + } + } + + @Override + public boolean mousePressed(Minecraft minecraft, int mouseX, int mouseY) { + // Don't allow clicking if selected (for nav buttons, not toggle buttons) + if (selected && !isToggle) { + return false; + } + return enabled && visible && mouseX >= xPosition && mouseX < xPosition + width + && mouseY >= yPosition && mouseY < yPosition + height; + } + + /** + * Check if this button contains the given mouse coordinates. + */ + public boolean isMouseOver(int mouseX, int mouseY) { + return mouseX >= xPosition && mouseX < xPosition + width + && mouseY >= yPosition && mouseY < yPosition + height; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java b/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java index 07822888c..3624de03c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java @@ -9,7 +9,7 @@ import noppes.npcs.constants.EnumDiagramLayout; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java b/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java index 8a1d015d0..cef818647 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java @@ -18,11 +18,11 @@ import org.lwjgl.opengl.GL12; public class GuiModelInterface2 extends GuiNPCInterface2 { public ModelData playerdata; - private static float rotation = 0; + protected static float rotation = 0; private GuiNpcButton left, right, zoom, unzoom; - private static float zoomed = 60; + protected static float zoomed = 60; public int xOffset = 0; public int yOffset = 0; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 7e7d2acd8..4ddd820c0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; public abstract class GuiNPCInterface extends GuiScreen { public EntityClientPlayerMP player; @@ -48,6 +49,7 @@ public abstract class GuiNPCInterface extends GuiScreen { public String title; private ResourceLocation background = null; public boolean closeOnEsc = false; + public Supplier closeOnEscSupplier = null; public int guiLeft, guiTop, xSize, ySize; private SubGuiInterface subgui; public int mouseX, mouseY, mouseScroll; @@ -55,6 +57,7 @@ public abstract class GuiNPCInterface extends GuiScreen { public float bgScaleX = 1; public float bgScaleY = 1; public float bgScaleZ = 1; + public int bgTextureHeight = 256; // Actual content height in texture (for bottom border stitching) public GuiNPCInterface(EntityNPCInterface npc) { this.player = Minecraft.getMinecraft().thePlayer; @@ -72,6 +75,11 @@ public abstract class GuiNPCInterface extends GuiScreen { background = new ResourceLocation("customnpcs", "textures/gui/" + texture); } + public void setBackground(String texture, int textureHeight) { + background = new ResourceLocation("customnpcs", "textures/gui/" + texture); + bgTextureHeight = textureHeight; + } + public ResourceLocation getResource(String texture) { return new ResourceLocation("customnpcs", "textures/gui/" + texture); } @@ -104,6 +112,10 @@ public abstract class GuiNPCInterface extends GuiScreen { rotateLeft = new GuiNpcButton(0, guiLeft + 44 + xOffsetNpc + xOffsetButton, guiTop + yOffsetNpc + yOffsetButton, 20, 20, "<"); rotateRight = new GuiNpcButton(0, guiLeft + 66 + xOffsetNpc + xOffsetButton, guiTop + yOffsetNpc + yOffsetButton, 20, 20, ">"); } + + //Important for GuiScriptTextArea + for (GuiNpcTextField tf : textfields.values()) + tf.initGui(); } @Override @@ -202,6 +214,7 @@ public abstract class GuiNPCInterface extends GuiScreen { } } } + @Override public void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) { if (subgui != null) { @@ -237,6 +250,29 @@ public abstract class GuiNPCInterface extends GuiScreen { @Override public void keyTyped(char c, int i) { + /* + Fixes closing sub with escape closes all of its parents. + The outermost GUI (root of all subs) closes the deepest open sub on ESC. + */ + boolean isSub = this instanceof SubGuiInterface; + + boolean shouldClose = closeOnEsc; + if (closeOnEscSupplier != null) { + try { + shouldClose = closeOnEscSupplier.get(); + } catch (Exception ignored) { + } + } + + if (shouldClose && !isSub && (i == 1 || (!GuiNpcTextField.isFieldActive() && isInventoryKey(i)))) { + SubGuiInterface sub = getSubGui(); + if (sub != null) + sub.close(); + else + close(); + } + + if (subgui != null) subgui.keyTyped(c, i); for (GuiNpcTextField tf : textfields.values()) @@ -246,27 +282,18 @@ public abstract class GuiNPCInterface extends GuiScreen { guiScrollableComponent.keyTyped(c, i); } - /* - Fixes closing sub with escape closes all of its parents. - The outermost GUI (root of all subs) closes the deepest open sub on ESC. - */ - boolean isSub = this instanceof SubGuiInterface; - - if (closeOnEsc && !isSub && (i == 1 || !GuiNpcTextField.isFieldActive() && isInventoryKey(i))) { - SubGuiInterface sub = getSubGui(); - if (sub != null) - sub.close(); - else - close(); - } } public void onGuiClosed() { GuiNpcTextField.unfocus(); } + public void closeOnEsc(Supplier close) { + this.closeOnEscSupplier = close; + } + public void close() { - if(GuiNpcTextField.activeTextfield != null) + if (GuiNpcTextField.activeTextfield != null) GuiNpcTextField.unfocus(); Keyboard.enableRepeatEvents(false); @@ -382,6 +409,11 @@ public abstract class GuiNPCInterface extends GuiScreen { button.drawHover(i, j, subGui); } } + for (GuiNpcTextField textField : textfields.values()) { + if (textField.hasHoverText()) { + textField.drawHover(i, j, subGui); + } + } for (GuiScreen gui : extra.values()) gui.drawScreen(i, j, f); @@ -400,23 +432,44 @@ public abstract class GuiNPCInterface extends GuiScreen { GL11.glTranslatef(guiLeft, guiTop, 0); GL11.glScalef(bgScale * bgScaleX, bgScale * bgScaleY, bgScale * bgScaleZ); mc.renderEngine.bindTexture(background); + + // Handle Y dimension: if ySize < texture content height, stitch bottom border + int topHeight = ySize; + int bottomHeight = 0; + int bottomTextureV = 0; + if (ySize < bgTextureHeight) { + // Keep bottom border from texture bottom (last 6 pixels of actual content) + bottomHeight = 6; + topHeight = ySize - bottomHeight; + bottomTextureV = bgTextureHeight - bottomHeight; + } + if (xSize > 256) { // GUI wider than texture: draw left portion, then right portion from texture edge - drawTexturedModalRect(0, 0, 0, 0, 250, ySize); - drawTexturedModalRect(250, 0, 256 - (xSize - 250), 0, xSize - 250, ySize); + drawTexturedModalRect(0, 0, 0, 0, 250, topHeight); + drawTexturedModalRect(250, 0, 256 - (xSize - 250), 0, xSize - 250, topHeight); + if (bottomHeight > 0) { + drawTexturedModalRect(0, topHeight, 0, bottomTextureV, 250, bottomHeight); + drawTexturedModalRect(250, topHeight, 256 - (xSize - 250), bottomTextureV, xSize - 250, bottomHeight); + } } else if (xSize < 256) { // GUI narrower than texture: stitch left and right edges together - // Left portion (first half of GUI) int leftWidth = xSize / 2; - // Right portion width (remaining) int rightWidth = xSize - leftWidth; - // Draw left side from texture start - drawTexturedModalRect(0, 0, 0, 0, leftWidth, ySize); - // Draw right side from texture end (256 - rightWidth) - drawTexturedModalRect(leftWidth, 0, 256 - rightWidth, 0, rightWidth, ySize); + // Draw top portion + drawTexturedModalRect(0, 0, 0, 0, leftWidth, topHeight); + drawTexturedModalRect(leftWidth, 0, 256 - rightWidth, 0, rightWidth, topHeight); + // Draw bottom border strip + if (bottomHeight > 0) { + drawTexturedModalRect(0, topHeight, 0, bottomTextureV, leftWidth, bottomHeight); + drawTexturedModalRect(leftWidth, topHeight, 256 - rightWidth, bottomTextureV, rightWidth, bottomHeight); + } } else { - // GUI exactly 256px: draw full texture - drawTexturedModalRect(0, 0, 0, 0, xSize, ySize); + // GUI exactly 256px wide + drawTexturedModalRect(0, 0, 0, 0, xSize, topHeight); + if (bottomHeight > 0) { + drawTexturedModalRect(0, topHeight, 0, bottomTextureV, xSize, bottomHeight); + } } GL11.glPopMatrix(); } @@ -517,6 +570,7 @@ public abstract class GuiNPCInterface extends GuiScreen { public void drawNpcWithExtras(EntityLivingBase entity, int mouseX, int mouseY, float partialTicks) { drawNpc(entity, mouseX, mouseY, partialTicks); } + public void drawNpc(EntityLivingBase entity, int mouseX, int mouseY, float partialTicks) { if (hasSubGui() && !drawNPConSub) return; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java index 37ffdb807..4ebafd54f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java @@ -2,7 +2,9 @@ package noppes.npcs.client.gui.util; import kamkeel.npcs.util.TextSplitter; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; @@ -126,6 +128,7 @@ public class GuiNpcButton extends GuiButton { } public AtomicBoolean rightClicked = new AtomicBoolean(); + @Override public boolean mousePressed(Minecraft minecraft, int i, int j) { boolean bo = super.mousePressed(minecraft, i, j); @@ -149,8 +152,23 @@ public class GuiNpcButton extends GuiButton { public void drawButton(Minecraft mc, int mouseX, int mouseY) { if (!this.visible) return; + // Auto-truncate display text if wider than button + String original = this.displayString; + FontRenderer fr = mc.fontRenderer; + if (fr != null) { + int maxTextWidth = this.width - 6; + if (fr.getStringWidth(original) > maxTextWidth) { + String ellipsis = "..."; + int targetWidth = maxTextWidth - fr.getStringWidth(ellipsis); + if (targetWidth > 0) { + this.displayString = fr.trimStringToWidth(original, targetWidth) + ellipsis; + } + } + } // First, draw the button normally using the superclass method. super.drawButton(mc, mouseX, mouseY); + // Restore original display string + this.displayString = original; // Then, if an icon texture is set, draw it. if (iconTexture != null) { mc.getTextureManager().bindTexture(iconTexture); @@ -216,17 +234,27 @@ public class GuiNpcButton extends GuiButton { maxHeight += 2 + (textLines.size() - 1) * 10; } - int gameWidth = mc.displayWidth; - int maxTooltipX = gameWidth - maxWidth - 4; + // Get scaled screen dimensions + ScaledResolution scaledRes = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int screenWidth = scaledRes.getScaledWidth(); + int screenHeight = scaledRes.getScaledHeight(); - if (j2 > maxTooltipX) { - int diff = j2 - maxTooltipX; - j2 -= diff; - GL11.glTranslatef(-300, 0, 0); + // If tooltip would go off right edge, draw on left side of mouse + if (j2 + maxWidth + 6 > screenWidth) { + j2 = x - maxWidth - 16; } - if (k2 + maxHeight + 6 > mc.displayHeight) { - k2 = mc.displayHeight - maxHeight - 6; + // Keep tooltip on screen horizontally + if (j2 < 4) { + j2 = 4; + } + + // Keep tooltip on screen vertically + if (k2 + maxHeight + 6 > screenHeight) { + k2 = screenHeight - maxHeight - 6; + } + if (k2 < 4) { + k2 = 4; } this.zLevel = 300.0F; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java index 0f26d9d36..436b47bb5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java @@ -4,6 +4,14 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.GuiTextField; +import net.minecraft.util.StatCollector; +import net.minecraft.client.gui.ScaledResolution; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import net.minecraft.client.renderer.RenderHelper; +import kamkeel.npcs.util.TextSplitter; +import java.util.List; +import java.util.Iterator; public class GuiNpcTextField extends GuiTextField { public boolean enabled = true; @@ -18,6 +26,9 @@ public class GuiNpcTextField extends GuiTextField { public float minFloat = 0, maxFloat = Float.MAX_VALUE, defFloat = 0; protected static GuiNpcTextField activeTextfield = null; public boolean canEdit = true; + public String hoverableText = ""; + private boolean wasHovered = false; + private int hoverCount = 0; private final int[] allowedSpecialChars = {14, 211, 203, 205}; @@ -39,6 +50,9 @@ public class GuiNpcTextField extends GuiTextField { this(id, parent, Minecraft.getMinecraft().fontRenderer, i, j, k, l, s); } + public void initGui() { + } + protected boolean charAllowed(char c, int i) { if (!integersOnly || Character.isDigit(c)) return true; @@ -200,5 +214,151 @@ public class GuiNpcTextField extends GuiTextField { return this; } + public void setHoverText(String text) { + this.hoverableText = StatCollector.translateToLocal(text); + } + public boolean hasHoverText() { + return hoverableText != null && !hoverableText.isEmpty(); + } + + public boolean isMouseOver(int mouseX, int mouseY) { + return mouseX >= xPosition && mouseX < xPosition + width && + mouseY >= yPosition && mouseY < yPosition + height; + } + + public void drawHover(int mouseX, int mouseY, boolean hasSubGui) { + if (hasSubGui || !enabled || hoverableText.isEmpty()) + return; + + boolean isHovered = isMouseOver(mouseX, mouseY); + if (!isHovered) { + wasHovered = false; + hoverCount = 0; + return; + } + + if (!wasHovered) { + wasHovered = true; + hoverCount = 0; + } else if (hoverCount < 65) { + hoverCount++; + } + + if (hoverCount > 60) { + GL11.glPushMatrix(); + Minecraft mc = Minecraft.getMinecraft(); + GL11.glColor4f(1, 1, 1, 1); + List lines = TextSplitter.splitText(hoverableText, 30); + drawHoveringText(lines, mouseX, mouseY, mc); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glPopMatrix(); + } + } + + protected void drawHoveringText(List textLines, int x, int y, Minecraft mc) { + if (mc.fontRenderer == null || textLines.isEmpty()) + return; + GL11.glDisable(GL12.GL_RESCALE_NORMAL); + RenderHelper.disableStandardItemLighting(); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + + int maxWidth = 0; + for (String s : textLines) { + int lineWidth = mc.fontRenderer.getStringWidth(s); + if (lineWidth > maxWidth) { + maxWidth = lineWidth; + } + } + + int j2 = x + 12; + int k2 = y - 12; + int maxHeight = 8; + + if (textLines.size() > 1) { + maxHeight += 2 + (textLines.size() - 1) * 10; + } + + // Get scaled screen dimensions + ScaledResolution scaledRes = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int screenWidth = scaledRes.getScaledWidth(); + int screenHeight = scaledRes.getScaledHeight(); + + // If tooltip would go off right edge, draw on left side of mouse + if (j2 + maxWidth + 6 > screenWidth) { + j2 = x - maxWidth - 16; + } + + // Keep tooltip on screen horizontally + if (j2 < 4) { + j2 = 4; + } + + // Keep tooltip on screen vertically + if (k2 + maxHeight + 6 > screenHeight) { + k2 = screenHeight - maxHeight - 6; + } + if (k2 < 4) { + k2 = 4; + } + + int zLevel = 300; + int bgColor = 0xF0100010; + int borderColor1 = 0x505000FF; + int borderColor2 = (borderColor1 & 0xFEFEFE) >> 1 | borderColor1 & 0xFF000000; + + drawGradientRect(j2 - 3, k2 - 4, j2 + maxWidth + 3, k2 - 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 3, k2 + maxHeight + 3, j2 + maxWidth + 3, k2 + maxHeight + 4, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 3, k2 - 3, j2 + maxWidth + 3, k2 + maxHeight + 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 4, k2 - 3, j2 - 3, k2 + maxHeight + 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 + maxWidth + 3, k2 - 3, j2 + maxWidth + 4, k2 + maxHeight + 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 3, k2 - 3 + 1, j2 - 3 + 1, k2 + maxHeight + 3 - 1, borderColor1, borderColor2, zLevel); + drawGradientRect(j2 + maxWidth + 2, k2 - 3 + 1, j2 + maxWidth + 3, k2 + maxHeight + 3 - 1, borderColor1, borderColor2, zLevel); + drawGradientRect(j2 - 3, k2 - 3, j2 + maxWidth + 3, k2 - 3 + 1, borderColor1, borderColor1, zLevel); + drawGradientRect(j2 - 3, k2 + maxHeight + 2, j2 + maxWidth + 3, k2 + maxHeight + 3, borderColor2, borderColor2, zLevel); + + for (int l = 0; l < textLines.size(); ++l) { + String s1 = textLines.get(l); + mc.fontRenderer.drawStringWithShadow(s1, j2, k2, -1); + if (l == 0) { + k2 += 2; + } + k2 += 10; + } + + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_DEPTH_TEST); + RenderHelper.enableStandardItemLighting(); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + } + + protected void drawGradientRect(int left, int top, int right, int bottom, int startColor, int endColor, int zLevel) { + float f = (float)(startColor >> 24 & 255) / 255.0F; + float f1 = (float)(startColor >> 16 & 255) / 255.0F; + float f2 = (float)(startColor >> 8 & 255) / 255.0F; + float f3 = (float)(startColor & 255) / 255.0F; + float f4 = (float)(endColor >> 24 & 255) / 255.0F; + float f5 = (float)(endColor >> 16 & 255) / 255.0F; + float f6 = (float)(endColor >> 8 & 255) / 255.0F; + float f7 = (float)(endColor & 255) / 255.0F; + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glDisable(GL11.GL_ALPHA_TEST); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glShadeModel(GL11.GL_SMOOTH); + net.minecraft.client.renderer.Tessellator tessellator = net.minecraft.client.renderer.Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_F(f1, f2, f3, f); + tessellator.addVertex((double)right, (double)top, (double)zLevel); + tessellator.addVertex((double)left, (double)top, (double)zLevel); + tessellator.setColorRGBA_F(f5, f6, f7, f4); + tessellator.addVertex((double)left, (double)bottom, (double)zLevel); + tessellator.addVertex((double)right, (double)bottom, (double)zLevel); + tessellator.draw(); + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 9b257f47e..be148c11d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -2,237 +2,926 @@ package noppes.npcs.client.gui.util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; import net.minecraft.util.ChatAllowedCharacters; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; -import noppes.npcs.client.gui.util.TextContainer.LineData; +import noppes.npcs.client.gui.script.GuiScriptInterface; +import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; +import noppes.npcs.client.gui.util.script.BracketMatcher; +import noppes.npcs.client.gui.util.script.CommentHandler; +import noppes.npcs.client.gui.util.script.CursorNavigation; +import noppes.npcs.client.gui.util.script.GoToLineDialog; +import noppes.npcs.client.gui.util.script.IndentHelper; +import noppes.npcs.client.gui.util.script.JavaTextContainer; +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import noppes.npcs.client.gui.util.script.RenameRefactorHandler; +import noppes.npcs.client.gui.util.script.ScrollState; +import noppes.npcs.client.gui.util.script.SearchReplaceBar; +import noppes.npcs.client.gui.util.script.SelectionState; +import noppes.npcs.client.key.impl.ScriptEditorKeys; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.util.ValueUtil; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; +import java.util.function.Supplier; import java.util.regex.Matcher; import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; +/** + * Script text editor component with syntax highlighting, bracket matching, + * smooth scrolling, and IDE-like features. + *

+ * Helper classes used: + * - ScrollState: smooth scroll animation and state management + * - SelectionState: cursor position and text selection management + * - BracketMatcher: bracket matching and brace span computation + * - IndentHelper: indentation utilities and text formatting + * - CommentHandler: line comment toggling + * - CursorNavigation: cursor movement logic + */ public class GuiScriptTextArea extends GuiNpcTextField { - public int id; + + // ==================== DIMENSIONS & POSITION ==================== public int x; public int y; - public int width; - public int height; - private int cursorCounter; - private ITextChangeListener listener; - public String text = null; - private TextContainer container = null; + + // ==================== STATE FLAGS ==================== public boolean active = false; public boolean enabled = true; public boolean visible = true; public boolean clicked = false; public boolean doubleClicked = false; - public boolean clickScrolling = false; - private int startSelection; - private int endSelection; - private int cursorPosition; - private int scrolledLine = 0; - private boolean enableCodeHighlighting = false; - public List undoList = new ArrayList(); - public List redoList = new ArrayList(); - public boolean undoing = false; + public boolean tripleClicked = false; + private int clickCount = 0; private long lastClicked = 0L; - // private static TrueTypeFont font = new TrueTypeFont(new Font("Arial Unicode MS", Font.PLAIN, ConfigClient.FontSize), 1); + + // ==================== TEXT & CONTAINER ==================== + public String text = null; + public String highlightedWord; + private JavaTextContainer container = null; + private boolean enableCodeHighlighting = false; + // Extra empty lines to allow padding at the bottom of the editor viewport + private int bottomPaddingLines = 6; + + // Search bar layout tracking to allow idempotent/resilient resizing + private int searchBaseY = 0; + private int searchBaseHeight = 0; + private int searchAppliedOffset = 0; + private boolean searchBaseInitialized = false; + + private int getPaddedLineCount() { + if (container == null) return 0; + // Only add bottom padding when the content is already scrollable. This avoids + // introducing a scrollbar when there's nothing to scroll for. + if (container.linesCount > container.visibleLines - bottomPaddingLines) { + return Math.max(0, container.linesCount + bottomPaddingLines); + } else { + return container.linesCount; + } + } + + // ==================== HELPER CLASS INSTANCES ==================== + private final ScrollState scroll = new ScrollState(); + private final SelectionState selection = new SelectionState(); + + // ==================== UI COMPONENTS ==================== + private int cursorCounter; + private ITextChangeListener listener; + private static int LINE_NUMBER_GUTTER_WIDTH = 25; + + // ==================== UNDO/REDO ==================== + public List undoList = new ArrayList<>(); + public List redoList = new ArrayList<>(); + public boolean undoing = false; + + // ==================== KEYS ==================== + public static final ScriptEditorKeys KEYS = new ScriptEditorKeys(); + public OverlayKeyPresetViewer KEYS_OVERLAY = new OverlayKeyPresetViewer(KEYS); + + // ==================== SEARCH/REPLACE ==================== + public static final SearchReplaceBar searchBar = new SearchReplaceBar(); + + // ==================== GO TO LINE ==================== + private final GoToLineDialog goToLineDialog = new GoToLineDialog(); + + // ==================== RENAME REFACTOR ==================== + private final RenameRefactorHandler renameHandler = new RenameRefactorHandler(); + + // ==================== CONSTRUCTOR ==================== public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { super(id, guiScreen, x, y, width, height, null); - this.id = id; - this.x = x; - this.y = y; + init(x, y, width, height, text); + } + + public void init(int x, int y, int width, int height, String text) { + this.x = xPosition = x; + this.y = yPosition = y; this.width = width; this.height = height; this.undoing = true; this.setText(text); this.undoing = false; + setCallbacks(); + // Reset search-base tracking whenever the editor is (re)initialized + this.searchBaseY = 0; + this.searchBaseHeight = 0; + this.searchAppliedOffset = 0; + this.searchBaseInitialized = false; + + KEYS_OVERLAY.openOnClick = true; + initGui(); + initializeKeyBindings(); } + public void initGui() { + int endX = x + width, endY = y + height; + int xOffset = hasVerticalScrollbar() ? -8 : -2; + KEYS_OVERLAY.scale = 0.75f; + KEYS_OVERLAY.borderCol1 = KEYS_OVERLAY.borderCol2 = 0xFF3c3c3c; + int overlayWidth = 160; + KEYS_OVERLAY.initGui(x + (width - overlayWidth) / 2 + 5, y + height / 10, overlayWidth, + height - height / 5 - 10); + + KEYS_OVERLAY.viewButton.scale = 0.45f; + KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); + + // Initialize search bar (preserves state across initGui calls) + searchBar.initGui(x, y, width); + if (searchBar.isVisible()) { // If open + // Shift viewport down again using the bar's current height + searchBar.callback.resizeEditor(true, searchBar.getTotalHeight()); + if (!active) // Focus search if opening another script tab & bar is open + searchBar.focus(false); + } + + // Initialize Go To Line dialog + goToLineDialog.initGui(x, y, width); + } + + public void setCallbacks() { + searchBar.setCallback(new SearchReplaceBar.SearchCallback() { + @Override + public String getText() { + return GuiScriptTextArea.this.text; + } + + public String getHighlightedWord() { + return GuiScriptTextArea.this.highlightedWord; + } + + @Override + public int getSelectionStart() { + return GuiScriptTextArea.this.selection.getStartSelection(); + } + + @Override + public int getSelectionEnd() { + return GuiScriptTextArea.this.selection.getEndSelection(); + } + + @Override + public void setText(String newText) { + GuiScriptTextArea.this.setText(newText); + } + + @Override + public void scrollToPosition(int position) { + // Find line containing position and scroll to it + if (container == null || container.lines == null) + return; + + // Calculate offset to account for search bar height + int searchBarOffset = searchBar.getTotalHeight(); + int effectiveHeight = GuiScriptTextArea.this.height - searchBarOffset; + int visibleLines = effectiveHeight / container.lineHeight; + // Calculate how many lines the search bar covers + int linesHiddenBySRB = searchBarOffset > 0 ? (int) Math.ceil( + (double) searchBarOffset / container.lineHeight) : 0; + + for (int i = 0; i < container.lines.size(); i++) { + LineData ld = container.lines.get(i); + if (position >= ld.start && position < ld.end) { + int visible = Math.max(1, visibleLines); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); + int targetLine = i; + + // If search bar is visible and would hide this line, scroll down so it's visible + // The target line should appear below the search bar, not under it + int currentScroll = scroll.getScrolledLine(); + int firstVisibleLine = currentScroll + linesHiddenBySRB; + + if (searchBarOffset > 0 && targetLine < firstVisibleLine) { + // Force scroll so target line appears just below the search bar + scroll.setTargetScroll(Math.max(0, targetLine - linesHiddenBySRB), maxScroll); + } else { + scroll.scrollToLine(targetLine, effectiveVisible, maxScroll); + } + break; + } + } + } + + @Override + public void setSelection(int start, int end) { + selection.setSelection(start, end); + selection.setCursorPositionDirect(end); + } + + @Override + public int getGutterWidth() { + return LINE_NUMBER_GUTTER_WIDTH; + } + + @Override + public void unfocusMainEditor() { + // Save position but unfocus + active = false; + } + + @Override + public void focusMainEditor() { + active = true; + searchBar.resetSelection(); + } + + @Override + public void onMatchesUpdated() { + // Called when matches change - could be used for UI updates + } + + // Robust resize: use base editor bounds and apply the requested offset + public void resizeEditor(boolean open, int barHeight) { + int desiredOffset = Math.max(0, barHeight); + // Initialize base values if not set + if (!searchBaseInitialized) { + searchBaseY = GuiScriptTextArea.this.y; + searchBaseHeight = GuiScriptTextArea.this.height; + searchAppliedOffset = 0; + searchBaseInitialized = true; + } + + int targetOffset = open ? desiredOffset : 0; + if (targetOffset == searchAppliedOffset) + return; // already in desired state + + // Compute new bounds from base values (idempotent) + int newY = searchBaseY + targetOffset; + int newHeight = Math.max(12, searchBaseHeight - targetOffset); + + GuiScriptTextArea.this.y = newY; + GuiScriptTextArea.this.height = newHeight; + searchAppliedOffset = targetOffset; + + if (container != null) + container.visibleLines = Math.max(GuiScriptTextArea.this.height / container.lineHeight - 1, 1); + } + }); + // Initialize Go To Line dialog with callback + goToLineDialog.setCallback(new GoToLineDialog.GoToLineCallback() { + @Override + public int getLineCount() { + return container != null ? container.linesCount : 0; + } + + @Override + public int getColumnCount(int lineIndex) { + if (container == null || container.lines == null || lineIndex < 0 || lineIndex >= container.lines.size()) { + return 0; + } + LineData ld = container.lines.get(lineIndex); + return ld.end - ld.start; + } + + @Override + public void goToLineColumn(int line, int column) { + if (container == null || container.lines == null) + return; + + // Convert 1-indexed line to 0-indexed + int lineIdx = line - 1; + if (lineIdx < 0 || lineIdx >= container.lines.size()) + return; + + LineData ld = container.lines.get(lineIdx); + int lineLength = ld.end - ld.start; + + // Convert 1-indexed column to 0-indexed, clamp to line length + // lineLength - 1 because the line's end is the start of the next line + int col = Math.max(0, Math.min(column - 1, lineLength - 1)); + int position = ld.start + col; + + // Set cursor position + selection.reset(position); + + // Scroll to make the line visible + int visible = GuiScriptTextArea.this.height / (container != null ? container.lineHeight : 12); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); + scroll.scrollToLine(lineIdx, effectiveVisible, maxScroll); + } + + @Override + public void unfocusMainEditor() { + // Save position but unfocus + active = false; + } + + @Override + public void focusMainEditor() { + active = true; + selection.markActivity(); + } + + @Override + public void onDialogClose() { + active = true; + selection.markActivity(); + } + }); + + // Initialize Rename Refactor handler with callback + renameHandler.setCallback(new RenameRefactorHandler.RenameCallback() { + @Override + public String getText() { + return GuiScriptTextArea.this.text; + } + + @Override + public void setText(String newText) { + GuiScriptTextArea.this.setText(newText); + } + + @Override + public List getLines() { + return container != null ? container.lines : new ArrayList<>(); + } + + @Override + public int getCursorPosition() { + return selection.getCursorPosition(); + } + + public SelectionState getSelectionState() { + return selection; + } + + @Override + public void setCursorPosition(int pos) { + selection.reset(pos); + } + + @Override + public void unfocusMainEditor() { + active = false; + } + + @Override + public void focusMainEditor() { + active = true; + selection.markActivity(); + } + + @Override + public int getGutterWidth() { + return LINE_NUMBER_GUTTER_WIDTH; + } + + @Override + public int getLineHeight() { + return container != null ? container.lineHeight : 12; + } + + @Override + public int getScrolledLine() { + return scroll.getScrolledLine(); + } + + @Override + public double getFractionalOffset() { + return scroll.getFractionalOffset(); + } + + @Override + public void scrollToPosition(int pos) { + if (container == null || container.lines == null) + return; + for (int i = 0; i < container.lines.size(); i++) { + LineData ld = container.lines.get(i); + if (pos >= ld.start && pos < ld.end) { + int visible = Math.max(1, container.visibleLines); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); + scroll.scrollToLine(i, effectiveVisible, maxScroll); + break; + } + } + } + + @Override + public JavaTextContainer getContainer() { + return container; + } + + @Override + public void setTextWithoutUndo(String newText) { + // Set text without creating an undo entry (for live rename preview) + boolean wasUndoing = undoing; + undoing = true; + setText(newText); + undoing = wasUndoing; + } + + @Override + public void pushUndoState(String textState, int cursor) { + // Push a specific text state to the undo list + if (!undoing) { + undoList.add(new UndoData(textState, cursor)); + redoList.clear(); + } + } + + @Override + public int getViewportWidth() { + return width - LINE_NUMBER_GUTTER_WIDTH - 8; // Account for gutter and scrollbar + } + }); + } + + public boolean fullscreen() { + return GuiScriptInterface.isFullscreen; + } + + public void setLanguage(String language) { + if (this.container != null) { + // this.container.setLanguage(language); + if (this.enableCodeHighlighting) { + this.container.formatCodeText(); + } + } + } + + // ==================== RENDERING ==================== public void drawTextBox(int xMouse, int yMouse) { if (!visible) return; clampSelectionBounds(); - drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xffa0a0a0); - drawRect(x, y, x + width, y + height, 0xff000000); - container.visibleLines = height / container.lineHeight; + // Dynamically calculate gutter width based on line count digits + if (container != null && container.linesCount > 0) { + int maxLineNum = container.linesCount; + String maxLineStr = "" + maxLineNum; + int digitWidth = ClientProxy.Font.width(maxLineStr); + LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10; // 10px total padding (5px left + 5px right) + } + // Draw outer border around entire area + int offset = fullscreen() ? 2 : 1; + drawRect(x - offset, y - offset - searchBar.getTotalHeight(), x + width + offset, y + height + offset, + 0xffa0a0a0); - if (listener instanceof GuiNPCInterface) { - int k2 = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); - if (k2 != 0) { - this.scrolledLine -= k2 / 40; - this.scrolledLine = Math.max(Math.min(this.scrolledLine, this.container.linesCount - this.height / this.container.lineHeight), 0); + int searchHeight = searchBar.getTotalHeight(); + + + // Draw line number gutter background + drawRect(x, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff000000); + // Draw text viewport background (starts after gutter) + drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + width, y + height, 0xff000000); + // Draw separator line between gutter and text area + drawRect(x + LINE_NUMBER_GUTTER_WIDTH - 1, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff3c3f41); + + // Enable scissor test to clip drawing to the TEXT viewport rectangle (excludes gutter) + GL11.glEnable(GL11.GL_SCISSOR_TEST); + scissorViewport(); + + container.visibleLines = (height / container.lineHeight); + + int maxScroll = Math.max(0, getPaddedLineCount() - container.visibleLines); + + // Handle mouse wheel scroll - only consume when mouse is over this text area + boolean isMouseOverTextArea = xMouse >= x && xMouse < x + width && yMouse >= y && yMouse < y + height; + int wheelDelta = 0; + if (isMouseOverTextArea) { + wheelDelta = Mouse.getDWheel(); + if (listener instanceof GuiNPCInterface) { + ((GuiNPCInterface) listener).mouseScroll = wheelDelta; } + boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; + if (wheelDelta != 0 && canScroll) + scroll.applyWheelScroll(wheelDelta, maxScroll); } + // Handle scrollbar dragging (delegated to ScrollState) + if (scroll.isClickScrolling()) + scroll.handleClickScrolling(yMouse, x, y, height, container.visibleLines, getPaddedLineCount(), maxScroll); + + // Update scroll animation + scroll.initializeIfNeeded(scroll.getScrolledLine()); + scroll.update(maxScroll); + + // Handle click-dragging for selection if (clicked) { clicked = Mouse.isButtonDown(0); int i = getSelectionPos(xMouse, yMouse); - if (i != cursorPosition) { - if (doubleClicked) { - startSelection = endSelection = cursorPosition; + if (i != selection.getCursorPosition()) { + if (doubleClicked || tripleClicked) { + selection.reset(selection.getCursorPosition()); doubleClicked = false; + tripleClicked = false; } setCursor(i, true); } - } else if (doubleClicked) { + } else if (doubleClicked || tripleClicked) { doubleClicked = false; + tripleClicked = false; } - - if (clickScrolling) { - clickScrolling = Mouse.isButtonDown(0); - int diff = container.linesCount - container.visibleLines; - scrolledLine = Math.min(Math.max((int) (1f * diff * (yMouse - y) / height), 0), diff); - } + // y += searchHeight; + // height -= searchHeight; + // Calculate braces next to cursor to highlight int startBracket = 0, endBracket = 0; - if (startSelection >= 0 && startSelection < text.length() && - (endSelection - startSelection == 1 || startSelection == endSelection)) { - char c = text.charAt(startSelection); - int found = 0; - if (c == '{') { - found = findClosingBracket(text.substring(startSelection), '{', '}'); - } else if (c == '[') { - found = findClosingBracket(text.substring(startSelection), '[', ']'); - } else if (c == '(') { - found = findClosingBracket(text.substring(startSelection), '(', ')'); - } else if (c == '}') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '{', '}'); - } else if (c == ']') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '[', ']'); - } else if (c == ')') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '(', ')'); - } - if (found != 0) { - startBracket = startSelection; - endBracket = startSelection + found; + if (selection.getStartSelection() >= 0 && text != null && text.length() > 0 && + (selection.getEndSelection() - selection.getStartSelection() == 1 || !selection.hasSelection())) { + int[] span = BracketMatcher.findBracketSpanAt(text, selection.getStartSelection()); + if (span != null) { + startBracket = span[0]; + endBracket = span[1]; } } - List list = new ArrayList(container.lines); + List list = new ArrayList<>(container.lines); - String wordHightLight = null; - if (startSelection != endSelection) { - Matcher m = container.regexWord.matcher(text); - while (m.find()) { - if (m.start() == startSelection && m.end() == endSelection) { - wordHightLight = text.substring(startSelection, endSelection); + // Build brace spans: {origDepth, open line, close line, adjustedDepth} + List braceSpans = BracketMatcher.computeBraceSpans(text, list); + // Always highlight unmatched braces (positions in text) + List unmatchedBraces = BracketMatcher.findUnmatchedBracePositions(text); + + // Determine which exact brace span (openLine/closeLine) to highlight indent guides for + int highlightedOpenLine = -1; + int highlightedCloseLine = -1; + if (startBracket != endBracket && startBracket >= 0) { + int bracketLineIdx = -1; + // Only consider curly braces for highlighting the indent guides + boolean isCurlyBracket = false; + char bc = text.charAt(startBracket); + if (text != null && startBracket >= 0 && startBracket < text.length()) { + if (bc == '{' || bc == '}') isCurlyBracket = true; + } + for (int li = 0; li < list.size(); li++) { + LineData ld = list.get(li); + if (startBracket >= ld.start && startBracket < ld.end) { + bracketLineIdx = li; + break; } } - } - for (int i = 0; i < list.size(); i++) { - LineData data = list.get(i); - String line = data.text; - int w = line.length(); - if (startBracket != endBracket) { - if (startBracket >= data.start && startBracket < data.end) { - int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start)); - int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1; - int posY = y + 1 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00); - } - if (endBracket >= data.start && endBracket < data.end) { - int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start)); - int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1; - int posY = y + 1 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00); - } - } - if (i >= scrolledLine && i < scrolledLine + container.visibleLines) { - if (wordHightLight != null) { - Matcher m = container.regexWord.matcher(line); - while (m.find()) { - if (line.substring(m.start(), m.end()).equals(wordHightLight)) { - int s = ClientProxy.Font.width(line.substring(0, m.start())); - int e = ClientProxy.Font.width(line.substring(0, m.end())) + 1; - int posY = y + 1 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x99004c00); + if (bracketLineIdx >= 0 && isCurlyBracket) { + // Prefer a span that directly matches the bracket character's line: + // - if the bracket is an opening '{', prefer spans where openLine == bracketLineIdx + // - if the bracket is a closing '}', prefer spans where closeLine == bracketLineIdx + // If no exact match is found, fall back to the smallest enclosing span (innermost). + int bestSize = Integer.MAX_VALUE; + boolean foundExact = false; + char bracketChar = bc; // from earlier + for (int[] span : braceSpans) { + int openLine = span[1]; + int closeLine = span[2]; + if (bracketLineIdx >= openLine && bracketLineIdx <= closeLine) { + int size = closeLine - openLine; + boolean exactMatch = (bracketChar == '{' && openLine == bracketLineIdx) || (bracketChar == '}' && closeLine == bracketLineIdx); + if (exactMatch) { + // Prefer exact matches immediately (still choose smallest exact span) + if (!foundExact || size < bestSize) { + foundExact = true; + bestSize = size; + highlightedOpenLine = openLine; + highlightedCloseLine = closeLine; + } + } else if (!foundExact) { + // keep the smallest enclosing span as a fallback + if (size < bestSize) { + bestSize = size; + highlightedOpenLine = openLine; + highlightedCloseLine = closeLine; + } } } } - if (startSelection != endSelection && endSelection > data.start && startSelection <= data.end) { - if (startSelection < data.end) { - int s = ClientProxy.Font.width(line.substring(0, Math.max(startSelection - data.start, 0))); - int e = ClientProxy.Font.width(line.substring(0, Math.min(endSelection - data.start, w))) + 1; - int posY = y + 1 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x990000ff); - } - } - int yPos = y + (i - scrolledLine) * container.lineHeight + 1; - data.drawString(x + 1, yPos, 0xFFe0e0e0); + } + } - if (active && isEnabled() && (cursorCounter / 6) % 2 == 0 && cursorPosition >= data.start && cursorPosition < data.end) { - int posX = x + ClientProxy.Font.width(line.substring(0, cursorPosition - data.start)); - drawRect(posX + 1, yPos, posX + 2, yPos + 1 + container.lineHeight, -3092272); + highlightedWord = null; + if (selection.hasSelection()) { + Matcher m = container.regexWord.matcher(text); + while (m.find()) { + if (m.start() == selection.getStartSelection() && m.end() == selection.getEndSelection()) { + highlightedWord = text.substring(selection.getStartSelection(), selection.getEndSelection()); } } } + // Apply fractional GL translate for sub-pixel smooth scrolling + double fracOffset = scroll.getFractionalOffset(); + float fracPixels = (float) (fracOffset * container.lineHeight); + GL11.glPushMatrix(); + GL11.glTranslatef(0.0f, -fracPixels, 0.0f); + + + // Expand render range by one line above/below so partially-visible lines are drawn + int renderStart = Math.max(0, scroll.getScrolledLine() - 1); + // Compute the last line index to render, including the last partially-visible line if any. + // Adds the fractional scroll offset (fracOffset * lineHeight) to ensure the bottom line is drawn + // when only part of it is visible in the viewport. + int renderEnd = (int) Math.min(list.size() - 1, + scroll.getScrolledLine() + container.visibleLines + fracPixels + 1); + + // Strings start drawing vertically this much into the line. + int stringYOffset = 2; + + // Render LINE GUTTER numbers + for (int i = renderStart; i <= renderEnd; i++) { + int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; + String lineNum = "" + (i + 1); + int lineNumWidth = ClientProxy.Font.width(lineNum); + int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5; // right-align with 5px padding + int lineNumY = posY + 1; + // Highlight current line number + int lineNumColor = 0xFF606366; + if (active && isEnabled()) { + for (int li = 0; li < list.size(); li++) { + LineData ld = list.get(li); + if (selection.getCursorPosition() >= ld.start && selection.getCursorPosition() < ld.end || (li == list.size() - 1 && selection.getCursorPosition() == text.length())) { + if (li == i) { + lineNumColor = 0xFFb9c7d6; + break; + } + } + } + } + ClientProxy.Font.drawString(lineNum, lineNumX, lineNumY, lineNumColor); + } + + // Render Viewport + for (int i = renderStart; i <= renderEnd; i++) { + LineData data = list.get(i); + String line = data.text; + int w = line.length(); + // Use integer Y relative to scrolledLine; fractional offset applied via GL translate + int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight; + if (i >= renderStart && i <= renderEnd) { + //Highlight braces the cursor position is on + if (startBracket != endBracket) { + if (startBracket >= data.start && startBracket < data.end) { + int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start)); + int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); + } + if (endBracket >= data.start && endBracket < data.end) { + int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start)); + int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); + } + } + + // Highlight unmatched braces in this line (always red) + if (unmatchedBraces != null && !unmatchedBraces.isEmpty()) { + for (int ubPos : unmatchedBraces) { + if (ubPos >= data.start && ubPos < data.end) { + int rel = ubPos - data.start; + int s = ClientProxy.Font.width(line.substring(0, rel)); + int e = ClientProxy.Font.width(line.substring(0, rel + 1)) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0xffcc0000); + } + } + } + //Highlight words + if (highlightedWord != null) { + Matcher m = container.regexWord.matcher(line); + while (m.find()) { + if (line.substring(m.start(), m.end()).equals(highlightedWord)) { + int s = ClientProxy.Font.width(line.substring(0, m.start())); + int e = ClientProxy.Font.width(line.substring(0, m.end())) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x99004c00); + } + } + } + + // Highlight search matches + if (searchBar.isVisible()) { + List searchMatches = searchBar.getMatches(); + int currentMatchIdx = searchBar.getCurrentMatchIndex(); + for (int mi = 0; mi < searchMatches.size(); mi++) { + int[] match = searchMatches.get(mi); + // Check if match overlaps with this line + if (match[1] > data.start && match[0] < data.end) { + int matchStart = Math.max(match[0] - data.start, 0); + int matchEnd = Math.min(match[1] - data.start, line.length()); + if (matchStart < matchEnd) { + int s = ClientProxy.Font.width(line.substring(0, matchStart)); + int e = ClientProxy.Font.width(line.substring(0, matchEnd)) + 1; + boolean isExcluded = searchBar.isMatchExcluded(mi); + // Current match gets brighter highlight, others get dimmer + int highlightColor = (mi == currentMatchIdx) ? 0xBB4488ff : 0x662266aa; + if (isExcluded) { + highlightColor = 0x33666666; // Dimmer for excluded matches + } + int highlightX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + s; + int highlightEndX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + e; + drawRect(highlightX, posY, highlightEndX, posY + container.lineHeight, highlightColor); + + // Draw strikethrough line for excluded matches + if (isExcluded) { + int strikeY = posY + container.lineHeight / 2; + drawRect(highlightX, strikeY, highlightEndX, strikeY + 1, 0xFFaa4444); + } + } + } + } + } + + // Highlight rename refactor occurrences + if (renameHandler.isActive()) { + List renameOccurrences = renameHandler.getOccurrences(); + for (int[] occ : renameOccurrences) { + // Check if occurrence overlaps with this line + if (occ[1] >= data.start && occ[0] <= data.end) { + int occStart = Math.max(occ[0] - data.start, 0); + int occEnd = Math.min(occ[1] - data.start, line.length()); + + // Handle empty word case - draw 1 pixel wide box + boolean isEmpty = (occ[0] == occ[1]); + + if (occStart <= occEnd) { // Changed from < to <= to handle empty case + int s = ClientProxy.Font.width(line.substring(0, occStart)); + int e = isEmpty ? s + 2 : ClientProxy.Font.width( + line.substring(0, occEnd)) + 1; // 2px wide for empty + int occX = x + LINE_NUMBER_GUTTER_WIDTH + s; + int occEndX = x + LINE_NUMBER_GUTTER_WIDTH + 2 + e; + boolean isPrimary = renameHandler.isPrimaryOccurrence(occ[0]); + + // Draw background highlight + int bgColor = isPrimary ? 0x55335577 : 0x33224466; + drawRect(occX, posY, occEndX, posY + container.lineHeight, bgColor); + + // Draw white border for primary occurrence (IntelliJ-like) + if (isPrimary) { + int borderColor = 0xDDFFFFFF; + //RED BG + drawRect(occX, posY, occEndX, posY + container.lineHeight, 0x33ff0000); + + // Top border + drawRect(occX, posY, occEndX, posY + 1, borderColor); + // Bottom border + drawRect(occX, posY + container.lineHeight - 1, occEndX, + posY + container.lineHeight, borderColor); + // Left border + drawRect(occX, posY, occX + 1, posY + container.lineHeight, borderColor); + // Right border + drawRect(occEndX - 1, posY, occEndX, posY + container.lineHeight, borderColor); + + // Draw cursor inside the primary occurrence + if (renameHandler.shouldShowCursor()) { + int cursorInWord = renameHandler.getCursorInWord(); + String currentWord = renameHandler.getCurrentWord(); + if (currentWord != null && cursorInWord >= 0 && cursorInWord <= currentWord.length()) { + String beforeCursor = currentWord.substring(0, + Math.min(cursorInWord, currentWord.length())); + int cursorX = occX + ClientProxy.Font.width(beforeCursor); + // drawRect(cursorX, posY + 1, cursorX + 1, posY + container.lineHeight - 1, + // 0xFFFFFFFF); + } + } + } + } + } + } + } + + // Highlight the current line (light gray) under any selection + if (active && isEnabled() && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { + drawRect(x, posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0); + } + // Highlight selection + if (selection.hasSelection() && selection.getEndSelection() > data.start && selection.getStartSelection() <= data.end) { + if (selection.getStartSelection() < data.end) { + int s = ClientProxy.Font.width( + line.substring(0, Math.max(selection.getStartSelection() - data.start, 0))); + int e = ClientProxy.Font.width( + line.substring(0, Math.min(selection.getEndSelection() - data.start, w))) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x992172ff); + } + } + + // Draw indent guides once per visible block based on brace spans + if (i == Math.max(0, scroll.getScrolledLine()) && !braceSpans.isEmpty()) { + int visStart = Math.max(0, scroll.getScrolledLine()); + int visEnd = Math.min(list.size() - 1, visStart + container.visibleLines - 0); + for (int[] span : braceSpans) { + int originalDepth = span[0]; + int openLine = span[1]; + int closeLine = span[2]; + int depth = span.length > 3 ? span[3] : originalDepth; + // Skip top-level (depth 1) using the original depth to avoid hiding nested guides when adjusted + if (originalDepth <= 1) + continue; + int startLine = openLine + 1; // start under the opening brace + int endLine = closeLine - 1; // stop before the closing brace + if (startLine > endLine) + continue; + if (endLine < visStart || startLine > visEnd) + continue; + + int drawStart = Math.max(startLine, visStart); + int drawEnd = Math.min(endLine, visEnd); + // Compute horizontal position: 4 spaces per indent level, minus a tiny left offset + int safeDepth = Math.max(1, depth); + int spaces = (safeDepth - 1) * 4; + StringBuilder sb = new StringBuilder(); + for (int k = 0; k < spaces; k++) + sb.append(' '); + int px = ClientProxy.Font.width(sb.toString()); + int gx = x + LINE_NUMBER_GUTTER_WIDTH + 4 + px - 2; // shift left ~2px for the IntelliJ feel + + boolean highlighted = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine); + int guideColor = highlighted ? 0x9933cc00 : 0x33FFFFFF; + + int topY = y + (drawStart - scroll.getScrolledLine()) * container.lineHeight; + int bottomY = y + (endLine - scroll.getScrolledLine() + 1) * container.lineHeight; + if (highlighted) + bottomY -= 2; + drawRect(gx, topY, gx + 1, bottomY, guideColor); + } + } + int yPos = posY + stringYOffset; + data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); + + // Draw cursor: pause blinking while user is active recently + boolean recentInput = selection.hadRecentInput(); + if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { + int posX = x + LINE_NUMBER_GUTTER_WIDTH + ClientProxy.Font.width( + line.substring(0, Math.min(selection.getCursorPosition() - data.start, line.length()))); + drawRect(posX + 1, posY, posX + 2, posY + container.lineHeight, 0xffffffff); + } + } + } + GL11.glPopMatrix(); + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + if (hasVerticalScrollbar()) { Minecraft.getMinecraft().renderEngine.bindTexture(GuiCustomScroll.resource); - int sbSize = Math.max((int) (1f * container.visibleLines / container.linesCount * height), 2); + int effLines = Math.max(1, getPaddedLineCount()); + int sbSize = Math.max((int) (1f * (container.visibleLines) / effLines * height), 2); int posX = x + width - 6; - int posY = (int) (y + 1f * scrolledLine / container.linesCount * (height - 4)) + 1; + double linesCount = (double) effLines; + int posY = (int) (y + 1f * scroll.getScrollPos() / linesCount * (height - 4)) + 1; - drawRect(posX, posY, posX + 5, posY + sbSize, 0xFFe0e0e0); - } - } - - private int findClosingBracket(String str, char s, char e) { - int found = 0; - char[] chars = str.toCharArray(); - - for (int i = 0; i < chars.length; ++i) { - char c = chars[i]; - if (c == s) { - ++found; - } else if (c == e) { - --found; - if (found == 0) { - return i; - } - } + drawRect(posX, posY, posX + 5, posY + sbSize + 2, 0xFFe0e0e0); } - return 0; + // Draw search/replace bar (overlays viewport) + searchBar.draw(xMouse, yMouse); + + // Draw go to line dialog (overlays everything) + goToLineDialog.draw(xMouse, yMouse); + KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); } - private int findOpeningBracket(String str, char s, char e) { - int found = 0; - char[] chars = str.toCharArray(); - - for (int i = chars.length - 1; i >= 0; --i) { - char c = chars[i]; - if (c == e) { - ++found; - } else if (c == s) { - --found; - if (found == 0) { - return i - chars.length + 1; - } - } - } - - return 0; + private void scissorViewport() { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scaleFactor = sr.getScaleFactor(); + int scissorX = (this.x) * scaleFactor; + int scissorY = (sr.getScaledHeight() - (this.y + this.height)) * scaleFactor; + int scissorW = (this.width) * scaleFactor; + int scissorH = this.height * scaleFactor; + GL11.glScissor(scissorX, scissorY, scissorW, scissorH); } + // ==================== SELECTION & CURSOR POSITION ==================== + // Get cursor position from mouse coordinates private int getSelectionPos(int xMouse, int yMouse) { - xMouse -= this.x + 1; + xMouse -= (this.x + LINE_NUMBER_GUTTER_WIDTH + 1); yMouse -= this.y + 1; + // Adjust yMouse to account for fractional GL translation (negative offset applied in rendering). + // Use a double here (no integer rounding) so clicks that land on partially + // visible lines (fractional positions) correctly hit that line. + double fracPixels = scroll.getFractionalOffset() * container.lineHeight; + double yMouseD = yMouse + fracPixels; + ArrayList list = new ArrayList(this.container.lines); for (int i = 0; i < list.size(); ++i) { LineData data = (LineData) list.get(i); - if (i >= this.scrolledLine && i < this.scrolledLine + this.container.visibleLines) { - int yPos = (i - this.scrolledLine) * this.container.lineHeight; - if (yMouse >= yPos && yMouse < yPos + this.container.lineHeight) { + //+1 to account for the fractional line + if (i >= scroll.getScrolledLine() && i <= scroll.getScrolledLine() + this.container.visibleLines + 1) { + double yPos = (i - scroll.getScrolledLine()) * this.container.lineHeight; + if (yMouseD >= yPos && yMouseD < yPos + this.container.lineHeight) { int lineWidth = 0; char[] chars = data.text.toCharArray(); @@ -245,7 +934,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { lineWidth = w; } - return data.end - 1; + // Place cursor after the last visible character of the line. + // `data.end - 1` previously pointed at the newline for non-last lines + // which made clicks land on the newline rather than after the text. + // Use data.start + chars.length to return the position directly + // after the line's characters, clamped to the total text length. + int posAfterChars = data.start + chars.length; + return Math.min(posAfterChars, text.length()); } } } @@ -253,70 +948,515 @@ public class GuiScriptTextArea extends GuiNpcTextField { return this.container.text.length(); } + // Find which line the cursor is on (0-indexed) + private int getCursorLineIndex() { + return selection.getCursorLineIndex(container.lines, text != null ? text.length() : 0); + } + + + // Scroll viewport to keep cursor visible (minimal adjustment, like IntelliJ) + // Only scrolls if cursor is outside the visible area + private void scrollToCursor() { + if (container == null || container.lines == null || container.lines.isEmpty()) return; + + int lineIdx = getCursorLineIndex(); + int visible = Math.max(1, container.visibleLines); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); + + scroll.scrollToLine(lineIdx, effectiveVisible, maxScroll); + } + + // ==================== KEY BINDINGS INITIALIZATION ==================== + + /** + * Initialize key bindings for editor shortcuts using ScriptEditorKeys. + * Centralized active/enabled checking ensures shortcuts only fire when appropriate. + */ + private void initializeKeyBindings() { + // Helper: execute action only if text area is active and enabled + Supplier isActive = () -> active && isEnabled() && !KEYS_OVERLAY.isVisible(); + Supplier openBoxes = () -> !KEYS_OVERLAY.isVisible(); + + // CUT: Copy selection to clipboard and delete it. If no selection, cut the current sentence. + KEYS.CUT.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + if (selection.hasSelection()) { + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + String s = getSelectionBeforeText(); + setText(s + getSelectionAfterText()); + selection.reset(s.length()); + scrollToCursor(); + return; + } + + // No selection: cut the current sentence (heuristic based on .!? or newline) + if (text == null || text.isEmpty()) + return; + int cursor = selection.getCursorPosition(); + int start = cursor; + int pos = cursor - 1; + while (pos >= 0) { + char ch = text.charAt(pos); + if (ch == '.' || ch == '!' || ch == '?' || ch == '\n') { + start = pos + 1; + break; + } + pos--; + } + while (start < cursor && Character.isWhitespace(text.charAt(start))) start++; + + int end = cursor; + pos = cursor; + while (pos < text.length()) { + char ch = text.charAt(pos); + if (ch == '.' || ch == '!' || ch == '?' || ch == '\n') { + end = pos + 1; + break; + } + pos++; + } + if (end == cursor) end = text.length(); + while (end > start && Character.isWhitespace(text.charAt(end - 1))) end--; + + if (start >= end) { + // fallback: cut whole current line + int ls = text.lastIndexOf('\n', Math.max(0, cursor - 1)); + start = ls == -1 ? 0 : ls + 1; + int le = text.indexOf('\n', cursor); + end = le == -1 ? text.length() : le; + } + + if (start < end) { + String cut = text.substring(start, end); + NoppesStringUtils.setClipboardContents(cut); + setText(text.substring(0, start) + text.substring(end)); + selection.reset(start); + scrollToCursor(); + } + }); + + // COPY: Copy selection to clipboard + KEYS.COPY.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + if (selection.hasSelection()) + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + }); + + // PASTE: Insert clipboard contents at caret + KEYS.PASTE.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + addText(NoppesStringUtils.getClipboardContents()); + scrollToCursor(); + }); + + // UNDO: Restore last edit from undo list + // Works in search bar. + KEYS.UNDO.setTask(e -> { + if ((!e.isPress() && !e.isHold()) || !openBoxes.get()) + return; + + if (searchBar.hasFocus()) { + searchBar.undo(); + } else { + if (undoList.isEmpty()) + return; + undoing = true; + redoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = undoList.remove(undoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + searchBar.updateMatches(); + if (!active) + active = true; + } + }); + + // REDO: Restore last undone edit from redo list + // Works in search bar. + KEYS.REDO.setTask(e -> { + if ((!e.isPress() && !e.isHold()) || !openBoxes.get()) + return; + + if (searchBar.hasFocus()) { + searchBar.redo(); + } else { + if (redoList.isEmpty()) + return; + undoing = true; + undoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = redoList.remove(redoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + searchBar.updateMatches(); + if (!active) + active = true; + } + }); + + // FORMAT: Format/indent code + KEYS.FORMAT.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + formatText(); + }); + + // TOGGLE_COMMENT: Toggle comment for selection or current line + KEYS.TOGGLE_COMMENT.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + if (selection.hasSelection()) + toggleCommentSelection(); + else + toggleCommentLineAtCursor(); + }); + + // DUPLICATE: Duplicate selection or current line + KEYS.DUPLICATE.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + if (selection.hasSelection()) { + // Multi-line selection duplication + LineData firstLine = null, lastLine = null; + for (LineData line : container.lines) { + if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) { + if (firstLine == null) + firstLine = line; + lastLine = line; + } + } + if (firstLine != null && lastLine != null) { + String selectedText = text.substring(firstLine.start, lastLine.end); + int savedStart = selection.getStartSelection(); + int savedEnd = selection.getEndSelection(); + int insertAt = lastLine.end; + setText(text.substring(0, insertAt) + selectedText + text.substring(insertAt)); + selection.setStartSelection(savedStart); + selection.setEndSelection(savedEnd); + selection.setCursorPositionDirect(savedEnd); + } + } else { + // Duplicate current line + for (LineData line : container.lines) { + if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { + int safeStart = Math.max(0, Math.min(line.start, text.length())); + int safeEnd = Math.max(safeStart, Math.min(line.end, text.length())); + String lineText = text.substring(safeStart, safeEnd); + String insertText = lineText.endsWith("\n") ? lineText : "\n" + lineText; + int insertionPoint = Math.min(line.end, text.length()); + setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); + int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); + selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); + break; + } + } + } + }); + + + // Check if can open just for SearchReplaceBar and GoToLine + + // SEARCH: Open search bar (Ctrl+R) + // Works in search bar. + KEYS.SEARCH.setTask(e -> { + if (!e.isPress() || !openBoxes.get()) + return; + + unfocusAll(); + searchBar.openSearch(); + }); + + // SEARCH_REPLACE: Open search+replace bar (Ctrl+Shift+R) + // Works in search bar. + KEYS.SEARCH_REPLACE.setTask(e -> { + if (!e.isPress() || !openBoxes.get()) + return; + + unfocusAll(); + searchBar.openSearchReplace(); + }); + + // GO_TO_LINE: Open go to line dialog (Ctrl+G) + // Works in search bar. + KEYS.GO_TO_LINE.setTask(e -> { + if (!e.isPress() || !openBoxes.get()) + return; + + unfocusAll(); + goToLineDialog.toggle(); + }); + + // RENAME: Start rename refactoring (Shift+F6) + KEYS.RENAME.setTask(e -> { + if (!e.isPress() || !openBoxes.get()) + return; + + + if (!renameHandler.isActive()) { + unfocusAll(); + active = true; + renameHandler.startRename(); + } + }); + } + + public void unfocusAll() { + if (searchBar.hasFocus()) searchBar.unfocus(); + if (goToLineDialog.hasFocus()) goToLineDialog.unfocus(); + if (renameHandler.isActive()) + renameHandler.cancel(); + } + // ==================== KEYBOARD INPUT HANDLING ==================== + + /** + * Handles keyboard input for the text area, delegating to specialized handlers + * for different types of input: navigation, deletion, shortcuts, and character input. + */ @Override public boolean textboxKeyTyped(char c, int i) { + if (KEYS_OVERLAY.keyTyped(c, i)) + return true; + + // Handle rename refactor input first if active + if (renameHandler.isActive() && renameHandler.keyTyped(c, i)) + return true; + + // Handle Go To Line dialog input first if it has focus + if (goToLineDialog.isVisible() && goToLineDialog.keyTyped(c, i)) + return true; + + // Handle search bar input first if it has focus + if (searchBar.isVisible() && searchBar.keyTyped(c, i)) + return true; + if (!active) return false; if (this.isKeyComboCtrlA(i)) { - startSelection = cursorPosition = 0; - endSelection = text.length(); + selection.selectAll(text.length()); return true; } - if (!isEnabled()) - return false; + if (!isEnabled()) return false; - String original = text; + if (handleNavigationKeys(i)) return true; + if (handleInsertionKeys(i)) return true; + if (handleDeletionKeys(i)) return true; + if (handleCharacterInput(c)) return true; + + return true; + } + + /** + * Handles cursor navigation keys (arrows) with support for word-jumping (Ctrl) + * and selection (Shift). Updates cursor position and scrolls viewport if needed. + */ + private boolean handleNavigationKeys(int i) { + // LEFT ARROW: move cursor left; with Ctrl -> jump by word if (i == Keyboard.KEY_LEFT) { - int j = 1; + int j = 1; // default: move one character if (isCtrlKeyDown()) { - Matcher m = container.regexWord.matcher(text.substring(0, cursorPosition)); + // When Ctrl is down, compute distance to previous word boundary. + // We match words in the text slice before the cursor and pick + // the last match start as the new boundary. + Matcher m = container.regexWord.matcher(text.substring(0, selection.getCursorPosition())); while (m.find()) { if (m.start() == m.end()) - continue; - j = cursorPosition - m.start(); + continue; // skip empty matches + // j becomes the number of chars to move left to reach word start + j = selection.getCursorPosition() - m.start(); } } - setCursor(cursorPosition - j, GuiScreen.isShiftKeyDown()); + int newPos = Math.max(selection.getCursorPosition() - j, 0); + // If Shift is held, extend selection; otherwise place caret. + setCursor(newPos, GuiScreen.isShiftKeyDown()); return true; } + + // RIGHT ARROW: move cursor right; with Ctrl -> jump to next word start if (i == Keyboard.KEY_RIGHT) { - int j = 1; + int j = 1; // default: move one character if (isCtrlKeyDown()) { - Matcher m = container.regexWord.matcher(text.substring(cursorPosition)); - if (m.find() && m.start() > 0 || m.find()) { - j = m.start(); + String after = text.substring(selection.getCursorPosition()); + Matcher m = container.regexWord.matcher(after); + if (m.find()) { + if (m.start() == 0) { + // If the first match starts at 0 (cursor at word start), + // try to find the next match so we advance past the current word. + if (m.find()) + j = m.start(); + else + j = Math.max(1, after.length()); + } else { + j = m.start(); + } + } else { + // No word match found after cursor -> jump to end + j = Math.max(1, after.length()); } } - setCursor(cursorPosition + j, GuiScreen.isShiftKeyDown()); + int newPos = Math.min(selection.getCursorPosition() + j, text.length()); + setCursor(newPos, GuiScreen.isShiftKeyDown()); return true; } + + // UP/DOWN: logical cursor movement across lines while preserving + // column where possible. After moving, ensure the caret remains visible + // by adjusting the scroll if necessary. if (i == Keyboard.KEY_UP) { setCursor(cursorUp(), GuiScreen.isShiftKeyDown()); + scrollToCursor(); return true; } if (i == Keyboard.KEY_DOWN) { setCursor(cursorDown(), GuiScreen.isShiftKeyDown()); + scrollToCursor(); return true; } + + return false; // not a navigation key + } + + /** + * Handles insertion-related keys such as Tab indentation and Enter behavior. + */ + private boolean handleInsertionKeys(int i) { + // TAB: indent or unindent depending on Shift + if (i == Keyboard.KEY_TAB) { + boolean shift = isShiftKeyDown(); + if (shift) { + handleShiftTab(); + } else { + handleTab(); + } + scrollToCursor(); + return true; + } + + // RETURN/ENTER: special handling when preceding char is an opening brace '{' + if (i == Keyboard.KEY_RETURN) { + int cursorPos = selection.getCursorPosition(); + int prevNonWs = cursorPos - 1; + while (prevNonWs >= 0 && prevNonWs < (text != null ? text.length() : 0) && Character.isWhitespace( + text.charAt(prevNonWs))) { + prevNonWs--; + } + + if (prevNonWs >= 0 && cursorPos <= (text != null ? text.length() : 0) && text.charAt(prevNonWs) == '{') { + String indent = ""; + for (LineData ld : this.container.lines) { + if (prevNonWs >= ld.start && prevNonWs < ld.end) { + indent = ld.text.substring(0, IndentHelper.getLineIndent(ld.text)); + break; + } + } + if (indent == null) + indent = ""; + String childIndent = indent + " "; + String before = getSelectionBeforeText(); + String after = getSelectionAfterText(); + + int firstNewline = after.indexOf('\n'); + String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); + if (leadingSegment.trim().length() > 0) { + addText("\n" + childIndent); + scrollToCursor(); + return true; + } + + boolean hasMatchingCloseSameIndent = false; + try { + int openLineIdx = -1; + int bracePos = prevNonWs; + for (int li = 0; li < this.container.lines.size(); li++) { + LineData ld = this.container.lines.get(li); + if (bracePos >= ld.start && bracePos < ld.end) { + openLineIdx = li; + break; + } + } + + if (openLineIdx >= 0) { + List spans = BracketMatcher.computeBraceSpans(text, this.container.lines); + for (int[] span : spans) { + int spanOpen = span[1]; + int spanClose = span[2]; + if (spanOpen == openLineIdx) { + int closeIndent = IndentHelper.getLineIndent(this.container.lines.get(spanClose).text); + if (closeIndent == indent.length()) { + hasMatchingCloseSameIndent = true; + } + break; + } + } + } + } catch (Exception ex) { + hasMatchingCloseSameIndent = false; + } + + if (hasMatchingCloseSameIndent) { + addText("\n" + childIndent); + scrollToCursor(); + } else { + String insert = "\n" + childIndent + "\n" + indent + "}"; + setText(before + insert + after); + int newCursor = before.length() + 1 + childIndent.length(); + selection.reset(newCursor); + scrollToCursor(); + } + } else { + addText(Character.toString('\n') + getAutoIndentForEnter()); + scrollToCursor(); + } + return true; + } + + return false; + } + + /** + * Handles deletion keys: Delete, Backspace, and Ctrl+Backspace. + * Includes smart backspace behavior for indentation-aware line merging, + * auto-pair deletion for brackets/quotes, and word-level deletion. + */ + private boolean handleDeletionKeys(int i) { + // DELETE key: remove the character under the cursor if no selection, + // otherwise remove the selected region's tail. if (i == Keyboard.KEY_DELETE) { String s = getSelectionAfterText(); - if (!s.isEmpty() && startSelection == endSelection) + if (!s.isEmpty() && !selection.hasSelection()) + // remove single character after caret when nothing is selected s = s.substring(1); setText(getSelectionBeforeText() + s); - endSelection = cursorPosition = startSelection; + // Keep caret at same start selection + selection.reset(selection.getStartSelection()); return true; } + + // CTRL+BACKSPACE: delete to previous word or whitespace boundary. if (isKeyComboCtrlBackspace(i)) { String s = getSelectionBeforeText(); - if (startSelection > 0 && startSelection == endSelection) { - int nearestCondition = cursorPosition; + if (selection.getStartSelection() > 0 && !selection.hasSelection()) { + int nearestCondition = selection.getCursorPosition(); int g; - boolean cursorInWhitespace = Character.isWhitespace(s.charAt(cursorPosition - 1)); + // If the char left of caret is whitespace, find the first non-space to the left; + // otherwise find first whitespace/newline to the left (word boundary). + boolean cursorInWhitespace = Character.isWhitespace(s.charAt(selection.getCursorPosition() - 1)); if (cursorInWhitespace) { - // Find the nearest word if we are starting in whitespace - for (g = cursorPosition - 1; g >= 0; g--) { + // Scan left until non-whitespace (start of previous word) + for (g = selection.getCursorPosition() - 1; g >= 0; g--) { char currentChar = s.charAt(g); if (!Character.isWhitespace(currentChar)) { nearestCondition = g; @@ -327,8 +1467,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } else { - // Find the nearest blank space or new line - for (g = cursorPosition - 1; g >= 0; g--) { + // Scan left until whitespace/newline is found (word boundary) + for (g = selection.getCursorPosition() - 1; g >= 0; g--) { char currentChar = s.charAt(g); if (Character.isWhitespace(currentChar) || currentChar == '\n') { nearestCondition = g; @@ -340,83 +1480,364 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } - // Remove all text to the left up to the nearest boundary + // Trim the prefix up to the discovered boundary s = s.substring(0, nearestCondition); - startSelection -= (cursorPosition - nearestCondition); + // Adjust selection start to match removed characters + selection.setStartSelection( + selection.getStartSelection() - (selection.getCursorPosition() - nearestCondition)); } setText(s + getSelectionAfterText()); - endSelection = cursorPosition = startSelection; + selection.reset(selection.getStartSelection()); return true; } + + // BACKSPACE: complex handling with a few cases: + // 1) If a selection exists, delete it + // 2) If at start, nothing to do + // 3) Smart indent-aware merge with previous line when caret is at/near expected indent + // 4) Auto-pair deletion (remove both opening and closing chars when deleting an opener) + // 5) Fallback: delete a single char to the left if (i == Keyboard.KEY_BACK) { - String s = getSelectionBeforeText(); - if (startSelection > 0 && startSelection == endSelection) { - s = s.substring(0, s.length() - 1); - startSelection--; - } - setText(s + getSelectionAfterText()); - endSelection = cursorPosition = startSelection; - return true; - } - if (this.isKeyComboCtrlX(i)) { - if (startSelection != endSelection) { - NoppesStringUtils.setClipboardContents(text.substring(startSelection, endSelection)); + // 1) selection deletion + if (selection.hasSelection()) { String s = getSelectionBeforeText(); setText(s + getSelectionAfterText()); - endSelection = startSelection = cursorPosition = s.length(); + selection.reset(selection.getStartSelection()); + scrollToCursor(); + return true; + } + // 2) nothing to delete + if (selection.getStartSelection() <= 0) { + return true; + } + + // If the current line is whitespace-only, delete the whole line + // (including the trailing newline if present). This makes Backspace + // intuitive on blank/indented lines outside any recognized scope. + LineData currCheck = selection.findCurrentLine(container.lines); + if (currCheck != null && currCheck.text.trim().length() == 0) { + int removeEnd = text.indexOf('\n', currCheck.start - 1); + if (removeEnd == -1) { + removeEnd = text.length(); + } else { + removeEnd = removeEnd + 1; // include the newline + } + String before = text.substring(0, ValueUtil.clamp(currCheck.start - 1, 0, text.length())); + String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; + setText(before + after); + int newCursor = Math.max(0, currCheck.start - 1); + selection.reset(newCursor); + scrollToCursor(); + return true; + } + + // 3) indent-aware merge: find current line and compute expected indent + LineData curr = selection.findCurrentLine(container.lines); + if (curr != null && curr.start > 0) { + int col = selection.getCursorPosition() - curr.start; + int actualIndent = IndentHelper.getLineIndent(curr.text); + int expectedIndent = IndentHelper.getExpectedIndent(curr, container.lines); + + // Trigger smart merge only when caret is at or before the expected indent. + if (col <= expectedIndent) { + boolean lineHasContent = curr.text.trim().length() > 0; + int newlinePos = curr.start - 1; // index of newline before this line + + if (!lineHasContent) { + // Empty or whitespace-only line: remove it including its trailing newline + int removeEnd = text.indexOf('\n', curr.start); + if (removeEnd == -1) { + removeEnd = text.length(); + } else { + removeEnd = removeEnd + 1; // include the newline in removal + } + String before = text.substring(0, curr.start); + String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; + setText(before + after); + // Place caret at end of previous line + int newCursor = Math.max(0, curr.start - 1); + selection.reset(newCursor); + scrollToCursor(); + return true; + } else { + // Merge current line content with the previous line preserving spacing + int contentStart = curr.start + actualIndent; + String before = newlinePos >= 0 ? text.substring(0, newlinePos) : ""; + String content = contentStart <= text.length() ? text.substring(contentStart) : ""; + + // Decide whether a space is needed between concatenated fragments. + String spacer = ""; + if (before.length() > 0 && content.length() > 0) { + char lastChar = before.charAt(before.length() - 1); + char firstChar = content.charAt(0); + // Avoid adding space when punctuation/brackets are adjacent + if (!Character.isWhitespace(lastChar) && + lastChar != '{' && lastChar != '(' && lastChar != '[' && + firstChar != '}' && firstChar != ')' && firstChar != ']' && + firstChar != ';' && firstChar != ',' && firstChar != '.' && + firstChar != '\n') { + spacer = " "; + } + } + + setText(before + spacer + content); + int newCursor = before.length() + spacer.length(); + selection.reset(newCursor); + scrollToCursor(); + return true; + } + } + } + + // 4) Auto-pair deletion: when deleting an opener and a matching closer follows, + // remove both so the pair is cleaned up in one backspace. + if (selection.getStartSelection() > 0 && selection.getStartSelection() < text.length()) { + char prev = text.charAt(selection.getStartSelection() - 1); + char nextc = text.charAt(selection.getStartSelection()); + if ((prev == '(' && nextc == ')') || + (prev == '[' && nextc == ']') || + (prev == '{' && nextc == '}') || + (prev == '\'' && nextc == '\'') || + (prev == '"' && nextc == '"')) { + String before = text.substring(0, selection.getStartSelection() - 1); + String after = selection.getStartSelection() + 1 < text.length() ? text.substring( + selection.getStartSelection() + 1) : ""; + setText(before + after); + selection.setStartSelection(selection.getStartSelection() - 1); + selection.reset(selection.getStartSelection()); + scrollToCursor(); + return true; + } + } + + // 5) Normal single-character backspace + String s = getSelectionBeforeText(); + s = s.substring(0, s.length() - 1); + selection.setStartSelection(selection.getStartSelection() - 1); + setText(s + getSelectionAfterText()); + selection.reset(selection.getStartSelection()); + scrollToCursor(); + return true; + } + + return false; + } + + /** + * Handles keyboard shortcuts: clipboard operations (cut/copy/paste), + * undo/redo, tab indentation, code formatting, enter with brace handling, + * comment toggling, and line duplication. + */ + private boolean handleShortcutKeys(int i) { + // CTRL+X: Cut + if (this.isKeyComboCtrlX(i)) { + if (selection.hasSelection()) { + // Copy selected text into clipboard, then remove the selection + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + String s = getSelectionBeforeText(); + setText(s + getSelectionAfterText()); + selection.reset(s.length()); + scrollToCursor(); } return true; } + + // CTRL+C: Copy if (this.isKeyComboCtrlC(i)) { - if (startSelection != endSelection) { - NoppesStringUtils.setClipboardContents(text.substring(startSelection, endSelection)); + if (selection.hasSelection()) { + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); } return true; } + + // CTRL+V: Paste (insert clipboard contents at caret) if (this.isKeyComboCtrlV(i)) { addText(NoppesStringUtils.getClipboardContents()); + scrollToCursor(); return true; } + + // UNDO (Ctrl+Z): restore last entry from undoList and push current state to redoList if (i == Keyboard.KEY_Z && isCtrlKeyDown()) { if (undoList.isEmpty()) - return false; + return false; // nothing to undo undoing = true; - redoList.add(new UndoData(this.text, this.cursorPosition)); + redoList.add(new UndoData(this.text, selection.getCursorPosition())); UndoData data = undoList.remove(undoList.size() - 1); setText(data.text); - endSelection = startSelection = cursorPosition = data.cursorPosition; + selection.reset(data.cursorPosition); undoing = false; + scrollToCursor(); return true; } + + // REDO (Ctrl+Y): opposite of undo if (i == Keyboard.KEY_Y && isCtrlKeyDown()) { if (redoList.isEmpty()) return false; undoing = true; - undoList.add(new UndoData(this.text, this.cursorPosition)); + undoList.add(new UndoData(this.text, selection.getCursorPosition())); UndoData data = redoList.remove(redoList.size() - 1); setText(data.text); - endSelection = startSelection = cursorPosition = data.cursorPosition; + selection.reset(data.cursorPosition); undoing = false; + scrollToCursor(); return true; } - if (i == Keyboard.KEY_TAB) { - addText(" "); - } - if (i == Keyboard.KEY_RETURN) { - addText(Character.toString('\n') + getIndentCurrentLine()); - } - if (ChatAllowedCharacters.isAllowedCharacter(c)) { - addText(Character.toString(c)); + + // CTRL+F: format the text according to IndentHelper rules + if (i == Keyboard.KEY_F && isCtrlKeyDown()) { + formatText(); + return true; } - return true; + // CTRL+/ : toggle comment for selection or current line + if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { + if (selection.hasSelection()) { + toggleCommentSelection(); + } else { + toggleCommentLineAtCursor(); + } + return true; + } + + // CTRL+D : duplicate selection or current line + if (i == Keyboard.KEY_D && isCtrlKeyDown()) { + if (selection.hasSelection()) { + // Multi-line selection duplication: find first and last covered lines, + // then insert the whole block after the last line without adding extra newline. + LineData firstLine = null, lastLine = null; + for (LineData line : container.lines) { + if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) { + if (firstLine == null) firstLine = line; + lastLine = line; + } + } + if (firstLine != null && lastLine != null) { + String selectedText = text.substring(firstLine.start, lastLine.end); + String insertText = selectedText; + int savedStart = selection.getStartSelection(); + int savedEnd = selection.getEndSelection(); + int insertAt = lastLine.end; + setText(text.substring(0, insertAt) + insertText + text.substring(insertAt)); + // Restore prior selection / cursor positions + selection.setStartSelection(savedStart); + selection.setEndSelection(savedEnd); + selection.setCursorPositionDirect(savedEnd); + return true; + } + } else { + // Duplicate current line when nothing is selected + for (LineData line : container.lines) { + if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { + int lineStart = line.start, lineEnd = line.end; + String lineText = text.substring(lineStart, lineEnd); + // If the line already ends with a newline, reuse it; otherwise + // prefix a newline so duplicate appears after current line. + String insertText; + if (lineText.endsWith("\n")) { + insertText = lineText; + } else { + insertText = "\n" + lineText; + } + int insertionPoint = lineEnd; + setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); + int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); + selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); + return true; + } + } + } + return true; + } + + return false; + } + + /** + * Handles printable character input with auto-pairing for quotes and brackets, + * and smart skipping over existing closing characters. + */ + private boolean handleCharacterInput(char c) { + if (ChatAllowedCharacters.isAllowedCharacter(c)) { + String before = getSelectionBeforeText(); + String after = getSelectionAfterText(); + + // If the user types a closing character and that same closer is + // already immediately after the caret, move caret past it instead + // of inserting another closer. This prevents duplicate closers + // when the editor auto-inserts pairs. + if ((c == ')' || c == ']' || c == '"' || c == '\'') && after.length() > 0 && after.charAt(0) == c) { + // Move caret forward by one (skip over existing closer) + selection.reset(before.length() + 1); + scrollToCursor(); + return true; + } + + // Auto-pair insertion: when opening a quote/brace/bracket is typed, + // insert a matching closer and place caret between the pair. + if (c == '"') { + setText(before + "\"\"" + after); + selection.reset(before.length() + 1); + scrollToCursor(); + return true; + } + if (c == '\'') { + setText(before + "''" + after); + selection.reset(before.length() + 1); + scrollToCursor(); + return true; + } + if (c == '[') { + setText(before + "[]" + after); + selection.reset(before.length() + 1); + scrollToCursor(); + return true; + } + if (c == '(') { + setText(before + "()" + after); + selection.reset(before.length() + 1); + scrollToCursor(); + return true; + } + + // Default insertion for printable characters: insert at caret (replacing selection) + addText(Character.toString(c)); + scrollToCursor(); + return true; + } + return false; } private boolean isShiftKeyDown() { return Keyboard.isKeyDown(42) || Keyboard.isKeyDown(54); } + // ==================== COMMENT TOGGLING ==================== + // Uses CommentHandler helper for comment operations + + private void toggleCommentSelection() { + CommentHandler.SelectionToggleResult result = CommentHandler.toggleCommentSelection( + text, container.lines, selection.getStartSelection(), selection.getEndSelection()); + setText(result.newText); + selection.setStartSelection(result.newStartSelection); + selection.setEndSelection(result.newEndSelection); + } + + private void toggleCommentLineAtCursor() { + CommentHandler.SingleLineToggleResult result = CommentHandler.toggleCommentAtCursor( + text, container.lines, selection.getCursorPosition()); + setText(result.newText); + setCursor(result.newCursorPosition, false); + } + + public boolean closeOnEsc() { + return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); + } + + // ==================== KEYBOARD MODIFIERS ==================== + private boolean isAltKeyDown() { return Keyboard.isKeyDown(56) || Keyboard.isKeyDown(184); } @@ -442,112 +1863,246 @@ public class GuiScriptTextArea extends GuiNpcTextField { } private String getIndentCurrentLine() { - Iterator var1 = this.container.lines.iterator(); - - LineData data; - do { - if (!var1.hasNext()) { - return ""; + for (LineData data : this.container.lines) { + if (selection.getCursorPosition() > data.start && selection.getCursorPosition() <= data.end) { + int i; + for (i = 0; i < data.text.length() && data.text.charAt(i) == ' '; ++i) { + } + return data.text.substring(0, i); } - - data = (LineData) var1.next(); - } while (this.cursorPosition <= data.start || this.cursorPosition > data.end); - - int i; - for (i = 0; i < data.text.length() && data.text.charAt(i) == 32; ++i) { - ; } - - return data.text.substring(0, i); + return ""; } - private void setCursor(int i, boolean select) { - i = Math.min(Math.max(i, 0), this.text.length()); - if (i != this.cursorPosition) { - if (!select) { - this.endSelection = this.startSelection = this.cursorPosition = i; - } else { - int diff = this.cursorPosition - i; - if (this.cursorPosition == this.startSelection) { - this.startSelection -= diff; - } else if (this.cursorPosition == this.endSelection) { - this.endSelection -= diff; - } + private String getAutoIndentForEnter() { + LineData currentLine = selection.findCurrentLine(container.lines); - if (this.startSelection > this.endSelection) { - int j = this.endSelection; - this.endSelection = this.startSelection; - this.startSelection = j; - } - - this.cursorPosition = i; - } - - clampSelectionBounds(); + if (currentLine == null) { + return ""; } + return IndentHelper.getAutoIndentForEnter(currentLine.text, selection.getCursorPosition() - currentLine.start); + } + // ==================== TEXT FORMATTING ==================== + + private int getTabSize() { + return IndentHelper.TAB_SIZE; + } + + private String repeatSpace(int count) { + return IndentHelper.spaces(count); + } + + private void formatText() { + // Calculate viewport width for line wrapping (account for gutter and scrollbar) + int viewportWidth = this.width - LINE_NUMBER_GUTTER_WIDTH - 10; + IndentHelper.FormatResult result = IndentHelper.formatText(text, selection.getCursorPosition(), viewportWidth); + setText(result.text); + selection.reset(Math.max(0, Math.min(result.cursorPosition, this.text.length()))); + } + + // ==================== TAB HANDLING ==================== + + private void handleTab() { + LineData currentLine = selection.findCurrentLine(container.lines); + if (currentLine == null) { + addText(" "); + return; + } + int tab = getTabSize(); + int indentLen = IndentHelper.getLineIndent(currentLine.text); + int textStartPos = currentLine.start + indentLen; + + if (selection.getCursorPosition() <= textStartPos) { + // Cursor before any text: if cursor is exactly at text start, move forward to next tab stop. + // If cursor is inside leading whitespace (before text start), choose nearest tab stop (tie -> forward). + int targetIndent; + if (selection.getCursorPosition() == textStartPos) { + targetIndent = ((indentLen / tab) + 1) * tab; + } else { + int remainder = indentLen % tab; + if (remainder == 0) { + targetIndent = indentLen + tab; // already aligned -> next level + } else { + int down = indentLen - remainder; + int up = indentLen + (tab - remainder); + int distDown = remainder; + int distUp = tab - remainder; + if (distUp < distDown) targetIndent = up; + else if (distUp > distDown) targetIndent = down; + else targetIndent = up; // tie -> forward + } + } + if (targetIndent < 0) targetIndent = 0; + String newIndent = repeatSpace(targetIndent); + String rest = currentLine.text.substring(indentLen); + String before = text.substring(0, currentLine.start); + int contentEnd = Math.min(currentLine.start + currentLine.text.length(), text.length()); + int sepEnd = Math.min(currentLine.end, text.length()); + String sep = contentEnd < sepEnd ? text.substring(contentEnd, sepEnd) : ""; + String after = text.substring(sepEnd); + setText(before + newIndent + rest + sep + after); + int newCursor = currentLine.start + targetIndent; + selection.reset(Math.min(newCursor, this.text.length())); + } else { + // Cursor is after start of text: insert spaces at cursor to move following text to next tab stop + int column = selection.getCursorPosition() - currentLine.start; + int targetColumn = ((column / tab) + 1) * tab; + int toInsert = Math.max(0, targetColumn - column); + if (toInsert > 0) { + String spaces = repeatSpace(toInsert); + addText(spaces); + } + } + } + + private void handleShiftTab() { + LineData currentLine = selection.findCurrentLine(container.lines); + if (currentLine == null) + return; + int tab = getTabSize(); + int indentLen = IndentHelper.getLineIndent(currentLine.text); + int textStartPos = currentLine.start + indentLen; + + if (selection.getCursorPosition() <= textStartPos) { + // Cursor before any text: reduce leading indent to previous tab stop + int targetIndent = Math.max(0, ((indentLen - 1) / tab) * tab); + String newIndent = repeatSpace(targetIndent); + String rest = currentLine.text.substring(indentLen); + String before = text.substring(0, currentLine.start); + int contentEnd = Math.min(currentLine.start + currentLine.text.length(), text.length()); + int sepEnd = Math.min(currentLine.end, text.length()); + String sep = contentEnd < sepEnd ? text.substring(contentEnd, sepEnd) : ""; + String after = text.substring(sepEnd); + setText(before + newIndent + rest + sep + after); + int newCursor = currentLine.start + targetIndent; + selection.reset(Math.min(newCursor, this.text.length())); + } else { + // Cursor after start of text: remove up to previous tab stop worth of spaces immediately before cursor + int column = selection.getCursorPosition() - currentLine.start; + int mod = column % tab; + int toRemove = mod == 0 ? tab : mod; + int removed = 0; + int pos = selection.getCursorPosition() - 1; + while (pos >= currentLine.start && removed < toRemove && text.charAt(pos) == ' ') { + pos--; + removed++; + } + if (removed > 0) { + int removeStart = pos + 1; + String before = text.substring(0, removeStart); + String after = text.substring(selection.getCursorPosition()); + setText(before + after); + int newCursor = removeStart; + selection.reset(Math.min(newCursor, this.text.length())); + } + } + } + + // ==================== CURSOR MANAGEMENT ==================== + + private void setCursor(int i, boolean select) { + selection.setCursor(i, text != null ? text.length() : 0, select); } private void addText(String s) { + int insertPos = selection.getStartSelection(); this.setText(this.getSelectionBeforeText() + s + this.getSelectionAfterText()); - this.endSelection = this.startSelection = this.cursorPosition = this.startSelection + s.length(); + selection.afterTextInsert(insertPos + s.length()); } private int cursorUp() { - for (int i = 0; i < this.container.lines.size(); ++i) { - LineData data = (LineData) this.container.lines.get(i); - if (this.cursorPosition >= data.start && this.cursorPosition < data.end) { - if (i == 0) { - return 0; - } - - int var10000 = this.cursorPosition - data.start; - return this.getSelectionPos(this.x + 1 + ClientProxy.Font.width(data.text.substring(0, this.cursorPosition - data.start)), this.y + 1 + (i - 1 - this.scrolledLine) * this.container.lineHeight); - } - } - - return 0; + return CursorNavigation.cursorUp(selection.getCursorPosition(), container.lines, text); } private int cursorDown() { - for (int i = 0; i < this.container.lines.size(); ++i) { - LineData data = (LineData) this.container.lines.get(i); - if (this.cursorPosition >= data.start && this.cursorPosition < data.end) { - int var10000 = this.cursorPosition - data.start; - return this.getSelectionPos(this.x + 1 + ClientProxy.Font.width(data.text.substring(0, this.cursorPosition - data.start)), this.y + 1 + (i + 1 - this.scrolledLine) * this.container.lineHeight); - } - } - - return this.text.length(); + return CursorNavigation.cursorDown(selection.getCursorPosition(), container.lines, text); } public String getSelectionBeforeText() { - return this.startSelection == 0 ? "" : this.text.substring(0, this.startSelection); + return selection.getTextBefore(text); } public String getSelectionAfterText() { - return this.text.substring(this.endSelection); + return selection.getTextAfter(text); } + // ==================== MOUSE HANDLING ==================== + public void mouseClicked(int xMouse, int yMouse, int mouseButton) { + // Check go to line dialog clicks first + if (goToLineDialog.isVisible() && goToLineDialog.mouseClicked(xMouse, yMouse, mouseButton)) { + return; + } + + // Check search bar clicks first + if (searchBar.isVisible() && searchBar.mouseClicked(xMouse, yMouse, mouseButton)) { + return; + } + + // If search bar is visible but click was outside it, unfocus the search bar + if (searchBar.isVisible()) { + searchBar.unfocus(); + } + + // Let the overlay consume clicks (it returns true when it handled the event) + if (KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton)) + return; + + // Determine whether click occurred inside the text area bounds this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; if (this.active) { - this.startSelection = this.endSelection = this.cursorPosition = this.getSelectionPos(xMouse, yMouse); + // Compute logical click position in text + int clickPos = this.getSelectionPos(xMouse, yMouse); + + // Check if rename refactoring is active and click is in the rename box + if (renameHandler.isActive() && renameHandler.handleClick(clickPos)) { + // Click was handled by rename handler - don't reset selection or do other click handling + this.clicked = false; + activeTextfield = this; + return; + } + + // Normal click handling - reset selection/caret + selection.reset(clickPos); + selection.markActivity(); + + // Prepare click state (left button starts most interactions) this.clicked = mouseButton == 0; this.doubleClicked = false; + this.tripleClicked = false; long time = System.currentTimeMillis(); - if (this.clicked && this.container.linesCount * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { - this.clicked = false; - this.clickScrolling = true; - } else if (time - this.lastClicked < 500L) { - this.doubleClicked = true; - Matcher m = this.container.regexWord.matcher(this.text); - while (m.find()) { - if (this.cursorPosition > m.start() && this.cursorPosition < m.end()) { - this.startSelection = m.start(); - this.endSelection = m.end(); - break; - } + // Prefer delegating scrollbar-start logic to ScrollState. If the click + // is on the scrollbar area and the scrollbar can be dragged, we start + // click-scrolling mode and cancel the normal text click-drag behavior. + if (this.clicked && getPaddedLineCount() * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { + // We consumed the mouse-down as a scrollbar drag start + this.clicked = false; + scroll.startScrollbarDrag(yMouse, this.y, this.height, getPaddedLineCount()); + } else { + // Handle double/triple click selection counting + if (time - this.lastClicked < 300L) { + this.clickCount++; + } else { + this.clickCount = 1; + } + + if (this.clickCount == 2) { + // Double-click: select the word under the caret using the container's word regex + this.doubleClicked = true; + selection.selectWordAtCursor(this.text, this.container.regexWord); + // Prevent subsequent mouse-drag handling in the render loop from + // treating this double-click as a normal click-drag which would + // immediately reset and extend the selection. Clearing `clicked` + // keeps the double-click selection stable. + this.clicked = false; + } else if (this.clickCount >= 3) { + // Triple-click: select the entire logical line that contains the caret + this.tripleClicked = true; + selection.selectLineAtCursor(container.lines); + // Same as double-click: clear `clicked` to avoid accidental drag-extension + this.clicked = false; + this.clickCount = 0; } } @@ -556,10 +2111,20 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } + // Called from GuiScreen.updateScreen() public void updateCursorCounter() { + // Only process KeyPresets if search bar and go-to-line dialog don't have focus + // This prevents COPY, PASTE, UNDO, etc. from firing when typing in dialogs + KEYS.tick(); + + searchBar.updateCursor(); + goToLineDialog.updateCursor(); + renameHandler.updateCursor(); ++this.cursorCounter; } + // ==================== TEXT MANAGEMENT ==================== + public void setText(String text) { if (text == null) { return; @@ -567,28 +2132,37 @@ public class GuiScriptTextArea extends GuiNpcTextField { text = text.replace("\r", ""); text = text.replace("\t", " "); + // preserve trailing newlines here — JavaTextContainer.init will + // ignore an extra empty final split when rendering lines. if (this.text == null || !this.text.equals(text)) { if (this.listener != null) { this.listener.textUpdate(text); } if (!this.undoing) { - this.undoList.add(new GuiScriptTextArea.UndoData(this.text, this.cursorPosition)); + this.undoList.add(new GuiScriptTextArea.UndoData(this.text, selection.getCursorPosition())); this.redoList.clear(); } this.text = text; - this.container = new TextContainer(text); - this.container.init(this.width, this.height); - if (this.enableCodeHighlighting) { + //this.container = new TextContainer(text); + if (this.container == null) + this.container = new JavaTextContainer(text); + + this.container.init(text, this.width, this.height); + + if (this.enableCodeHighlighting) this.container.formatCodeText(); - } - if (this.scrolledLine > this.container.linesCount - this.container.visibleLines) { - this.scrolledLine = Math.max(0, this.container.linesCount - this.container.visibleLines); - } + // Ensure scroll state stays in bounds after text change + int maxScroll = Math.max(0, getPaddedLineCount() - this.container.visibleLines); + scroll.clampToBounds(maxScroll); - clampSelectionBounds(); + selection.clamp(this.text.length()); + + // Consider text changes user activity to pause caret blinking briefly + selection.markActivity(); + searchBar.updateMatches(); } } @@ -602,7 +2176,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } public boolean hasVerticalScrollbar() { - return this.container.visibleLines < this.container.linesCount; + return this.container != null && this.container.visibleLines < getPaddedLineCount(); } public void enableCodeHighlighting() { @@ -610,23 +2184,39 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.container.formatCodeText(); } + /** + * Set the script context (NPC, PLAYER, BLOCK, ITEM, etc.). + * This determines which hooks and event types are available for autocomplete. + * + * @param context The script context + */ + public void setScriptContext(ScriptContext context) { + if (this.container != null) { + // this.container.setScriptContext(context); + } + } + + /** + * Get the current script context. + * + * @return The script context (NPC, PLAYER, BLOCK, ITEM, etc.) + */ + public ScriptContext getScriptContext() { + // return this.container != null ? this.container.getScriptContext() : ScriptContext.GLOBAL; + return ScriptContext.GLOBAL; + } + public void setListener(ITextChangeListener listener) { this.listener = listener; } private void clampSelectionBounds() { - if (this.text == null || this.text.isEmpty()) { - this.startSelection = this.endSelection = this.cursorPosition = 0; - return; - } - - int length = this.text.length(); - this.startSelection = Math.max(0, Math.min(this.startSelection, length)); - this.endSelection = Math.max(0, Math.min(this.endSelection, length)); - this.cursorPosition = Math.max(0, Math.min(this.cursorPosition, length)); + selection.clamp(text != null ? text.length() : 0); } - class UndoData { + // ==================== INNER CLASSES ==================== + + public static class UndoData { public String text; public int cursorPosition; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java index 3555b30fd..3273f1f6c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java @@ -143,6 +143,7 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC } } } + public void mouseEvent(int i, int j, int k) { } @@ -207,6 +208,8 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC return scrolls.get(id); } + public int backgroundColor = 0x66000000; + @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { scrollY = (float) lerp(scrollY, nextScrollY, partialTicks); @@ -220,7 +223,7 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC this.drawDefaultBackground = false; if (drawBackground) - this.drawGradientRect(0, 0, this.width, this.height, 0x66000000, 0x88000000); + this.drawGradientRect(0, 0, this.width, this.height, backgroundColor, backgroundColor); if (maxScrollY > 0) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java new file mode 100644 index 000000000..7350668b8 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java @@ -0,0 +1,202 @@ +package noppes.npcs.client.gui.util; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.Tessellator; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +public class GuiUtil { + + public static void setScissorClip(int x, int y, int width, int height) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution res = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scale = res.getScaleFactor(); + + + // Adjust position to Top-Left origin (OpenGL window/screen space uses bottom-left origin) + y = mc.displayHeight - y * scale; + height *= scale; + + // Set clip + GL11.glScissor(x * scale, y - height, width * scale, height); + } + + public static void drawHorizontalLine(double y, double startX, double endX, int color) { + if (endX < startX) { + double i1 = startX; + startX = endX; + endX = i1; + } + + drawRectD(startX, y, endX + 1, y + 1, color); + } + + public static void drawVerticalLine(double x, double startY, double endY, int color) { + if (endY < startY) { + double i1 = startY; + startY = endY; + endY = i1; + } + + drawRectD(x, startY + 1, x + 1, endY, color); + } + + public static void drawRectD(double left, double top, double right, double bottom) { + double j1; + + if (left < right) { + j1 = left; + left = right; + right = j1; + } + + if (top < bottom) { + j1 = top; + top = bottom; + bottom = j1; + } + + Tessellator tessellator = Tessellator.instance; + GL11.glEnable(GL11.GL_BLEND); + GL11.glDisable(GL11.GL_TEXTURE_2D); + OpenGlHelper.glBlendFunc(770, 771, 1, 0); + tessellator.startDrawingQuads(); + tessellator.addVertex(left, bottom, 0.0D); + tessellator.addVertex(right, bottom, 0.0D); + tessellator.addVertex(right, top, 0.0D); + tessellator.addVertex(left, top, 0.0D); + tessellator.draw(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_BLEND); + } + + + public static void drawRectD(double left, double top, double right, double bottom, int color) { + float f3 = (float) (color >> 24 & 255) / 255.0F; + float f = (float) (color >> 16 & 255) / 255.0F; + float f1 = (float) (color >> 8 & 255) / 255.0F; + float f2 = (float) (color & 255) / 255.0F; + GL11.glColor4f(f, f1, f2, f3); + drawRectD(left, top, right, bottom); + } + + public static void drawGradientRect(int left, int top, int right, int bottom, int startColor, int endColor) { + double zLevel = 0; + + float f = (float) (startColor >> 24 & 255) / 255.0F; + float f1 = (float) (startColor >> 16 & 255) / 255.0F; + float f2 = (float) (startColor >> 8 & 255) / 255.0F; + float f3 = (float) (startColor & 255) / 255.0F; + float f4 = (float) (endColor >> 24 & 255) / 255.0F; + float f5 = (float) (endColor >> 16 & 255) / 255.0F; + float f6 = (float) (endColor >> 8 & 255) / 255.0F; + float f7 = (float) (endColor & 255) / 255.0F; + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glDisable(GL11.GL_ALPHA_TEST); + OpenGlHelper.glBlendFunc(770, 771, 1, 0); + GL11.glShadeModel(GL11.GL_SMOOTH); + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_F(f1, f2, f3, f); + tessellator.addVertex(right, top, zLevel); + tessellator.addVertex(left, top, zLevel); + tessellator.setColorRGBA_F(f5, f6, f7, f4); + tessellator.addVertex(left, bottom, zLevel); + tessellator.addVertex(right, bottom, zLevel); + tessellator.draw(); + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + + public static void drawGradientRectHorizontal(int left, int top, int right, int bottom, int startColor, int endColor) { + double zLevel = 0; + + float sA = (float) (startColor >> 24 & 255) / 255.0F; + float sR = (float) (startColor >> 16 & 255) / 255.0F; + float sG = (float) (startColor >> 8 & 255) / 255.0F; + float sB = (float) (startColor & 255) / 255.0F; + + float eA = (float) (endColor >> 24 & 255) / 255.0F; + float eR = (float) (endColor >> 16 & 255) / 255.0F; + float eG = (float) (endColor >> 8 & 255) / 255.0F; + float eB = (float) (endColor & 255) / 255.0F; + + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glDisable(GL11.GL_ALPHA_TEST); + OpenGlHelper.glBlendFunc(770, 771, 1, 0); + GL11.glShadeModel(GL11.GL_SMOOTH); + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_F(sR, sG, sB, sA); + tessellator.addVertex(left, top, zLevel); + tessellator.addVertex(left, bottom, zLevel); + tessellator.setColorRGBA_F(eR, eG, eB, eA); + tessellator.addVertex(right, bottom, zLevel); + tessellator.addVertex(right, top, zLevel); + tessellator.draw(); + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + + public static void drawTexturedModalRect(double x, double y, double width, double height, int textureX, int textureY) { + double zLevel = 0; + float f = 1 / 256f; + float f1 = 1 / 256f; + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.addVertexWithUV(x + 0, y + height, zLevel, (float) (textureX) * f, (float) (textureY + height) * f1); + tessellator.addVertexWithUV(x + width, y + height, zLevel, (float) (textureX + width) * f, (float) (textureY + height) * f1); + tessellator.addVertexWithUV(x + width, y + 0, zLevel, (float) (textureX + width) * f, (float) (textureY) * f1); + tessellator.addVertexWithUV(x + 0, y + 0, zLevel, (float) (textureX) * f, (float) (textureY) * f1); + tessellator.draw(); + } + + public static void setMouse(int guiX, int guiY) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + + int factor = scaledResolution.getScaleFactor(); + + Mouse.setCursorPosition(guiX * factor, mc.displayHeight - (guiY * factor)); + } + + public static void setMouseX(int guiX) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + + int factor = scaledResolution.getScaleFactor(); + Mouse.setCursorPosition(guiX * factor, Mouse.getY()); + } + + public static void setMouseY(int guiY) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + + int factor = scaledResolution.getScaleFactor(); + Mouse.setCursorPosition(Mouse.getX(), mc.displayHeight - (guiY * factor)); + } + + public static double preciseMouseX() { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + + double width = scaledResolution.getScaledWidth(); + return Mouse.getX() * width / mc.displayWidth; + } + + public static double preciseMouseY() { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + + double height = scaledResolution.getScaledHeight(); + return height - Mouse.getY() * height / mc.displayHeight - 1; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java b/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java index ec186d066..4012c9f73 100644 --- a/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java +++ b/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java @@ -9,6 +9,7 @@ import kamkeel.npcs.controllers.data.ability.Ability; public interface IAbilityConfigCallback { /** * Called when an ability has been edited and saved. + * * @param ability The edited ability */ void onAbilitySaved(Ability ability); diff --git a/src/main/java/noppes/npcs/client/gui/util/SubGuiInterface.java b/src/main/java/noppes/npcs/client/gui/util/SubGuiInterface.java index 8b35c65dc..b10ba84bd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/SubGuiInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/SubGuiInterface.java @@ -12,6 +12,8 @@ public class SubGuiInterface extends GuiNPCInterface { @Override public void close() { + save(); + if (parent instanceof ISubGuiListener) ((ISubGuiListener) parent).subGuiClosed(this); diff --git a/src/main/java/noppes/npcs/client/gui/util/TextContainer.java b/src/main/java/noppes/npcs/client/gui/util/TextContainer.java index dc66332cb..2dc588c0f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/TextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/TextContainer.java @@ -8,14 +8,15 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; public class TextContainer { - private final static char colorChar = '\u00A7'; + protected final static char colorChar = '\u00A7'; //public final Pattern regexString = Pattern.compile("\"(?:[^\"\\\\]|\\\\.)*?(\"|$)|'(?:[^'\\\\]|\\\\.)*?('|$)"); //public final Pattern regexString = Pattern.compile("\"(.*?(? makeup = new ArrayList(); public List lines = new ArrayList(); - public int lineHeight; + public int lineHeight = 13; public int totalHeight; public int visibleLines = 1; public int linesCount; @@ -73,7 +74,7 @@ public class TextContainer { } } - private MarkUp getNextMatching(int start) { + protected MarkUp getNextMatching(int start) { MarkUp markup = null; String s = text.substring(start); Matcher matcher = regexNumber.matcher(s); @@ -128,7 +129,7 @@ public class TextContainer { makeup.add(new MarkUp(start, end, c, level)); } - private boolean removeConflictingMarkUp(int start, int end, int level) { + protected boolean removeConflictingMarkUp(int start, int end, int level) { List conflicting = new ArrayList(); for (MarkUp m : makeup) { if (start >= m.start && start <= m.end || end >= m.start && end <= m.end || start < m.start && end > m.start) { diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java new file mode 100644 index 000000000..5d9cfdaa0 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -0,0 +1,499 @@ +package noppes.npcs.client.gui.util.key; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.gui.util.GuiUtil; +import noppes.npcs.client.key.KeyPreset; +import noppes.npcs.client.key.KeyPresetManager; +import noppes.npcs.client.util.Color; +import noppes.npcs.util.ValueUtil; +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +import java.util.LinkedList; +import java.util.List; + +public class OverlayKeyPresetViewer { + public static final ResourceLocation TEXTURE = new ResourceLocation("customnpcs:textures/gui/keypreset_highres.png"); + + public int startX, startY, endX, endY, width, height; + public int mouseX, mouseY; + public boolean showOverlay; + public boolean aboveButton, aboveOverlay; + public int elementSpacing = 5, yStartSpacing = 5; + public float scale = 0.75f; + public Scrollable scroll = new Scrollable(); + + public KeyPresetManager manager; + public OverlayButton viewButton = new OverlayButton(); + public LinkedList list = new LinkedList<>(); + + // from 0-1, where 0.5 is half overlay width; + public float RELATIVE_MAX_DESC_WIDTH = 0.5f; + private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; + + public int bgCol1 = 0xdd101010, bgCol2 = 0xf8303030; + public int borderCol1 = 0x22ffffff, borderCol2 = 0xffffffff; + public boolean hasBorder = true; + public boolean openOnClick; + + public OverlayKeyPresetViewer(KeyPresetManager manager) { + this.manager = manager; + + manager.keys.forEach((key) -> { + PresetElement element = new PresetElement(key); + list.add(element); + }); + } + + public void initGui(int startX, int startY, int width, int height) { + this.startX = startX; + this.startY = startY; + this.width = width; + this.height = height; + this.endX = startX + width; + this.endY = startY + height; + scroll.init(); + } + + public void draw(int mouseX, int mouseY, int wheel) { + this.mouseX = mouseX; + this.mouseY = mouseY; + aboveButton = viewButton.isMouseAbove(mouseX, mouseY); + aboveOverlay = isMouseAbove(mouseX, mouseY); + // If in hover mode, hovering the button opens the overlay. + if (aboveButton && !openOnClick) + showOverlay = true; + + if (showOverlay) { + drawOverlay(wheel); + + // In click-to-open mode we don't auto-close on mouse-out; closure is handled by clicks. + if (!openOnClick && !aboveOverlay && !aboveButton) + showOverlay = false; + } + viewButton.drawButton(); + } + + public void drawOverlay(int wheel) { + scroll.update(wheel); + + if (hasBorder) { + // int borderCol2 = 0xffff00ff,borderCol2 = 0xff00ffff; NEON + + // Top + GuiUtil.drawGradientRectHorizontal(startX - 1, startY - 1, endX + 1, startY, borderCol2, borderCol1); + // Bottom + GuiUtil.drawGradientRectHorizontal(startX - 1, endY, endX + 1, endY + 1, borderCol1, borderCol2); + // Left + GuiUtil.drawGradientRect(startX - 1, startY - 1, startX, endY, borderCol2, borderCol1); + //Right + GuiUtil.drawGradientRect(endX, startY, endX + 1, endY, borderCol1, borderCol2); + } + + GuiUtil.drawGradientRect(startX, startY, endX, endY, bgCol1, bgCol2); + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GuiUtil.setScissorClip(startX, startY, scroll.maxScroll > 0 ? width - scroll.barWidth : width, height); + GL11.glPushMatrix(); + GL11.glTranslatef((startX + 2), startY + yStartSpacing - scroll.scrollY, 0); + + for (int i = 0, height = 0; i < list.size(); i++) { + PresetElement element = list.get(i); + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, 1); + GL11.glTranslatef(0, Math.round(height / scale), 0); + element.drawDescription(); + GL11.glPopMatrix(); + + element.drawBox(height); + height += element.getHeight(); + } + + GL11.glPopMatrix(); + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + if (scroll.maxScroll > 0) + scroll.drawBar(); + } + + public boolean keyTyped(char c, int i) { + if (!showOverlay) + return false; + + for (PresetElement element : list) { + if (element.isEditing) { + element.keyTyped(i); + return true; + } + } + + if (i == Keyboard.KEY_ESCAPE) { + showOverlay = false; + return true; + } + + // If overlay is visible but no element consumed the key, consider it handled + return true; + } + + public boolean mouseClicked(int mouseX, int mouseY, int button) { + // Always allow clicking the view button to toggle when openOnClick is enabled + boolean isAboveButton = viewButton.isMouseAbove(mouseX, mouseY); + if (isAboveButton) { + if (openOnClick) { + showOverlay = !showOverlay; + return true; + } else { + // If not click-to-open mode, clicking the button should still open the overlay + showOverlay = true; + return true; + } + } + + // If overlay is not visible, no other clicks matter + if (!showOverlay) + return false; + + boolean isAboveOverlay = isMouseAbove(mouseX, mouseY); + // In click-to-open mode, clicking outside the overlay (and not the button) should close it + if (openOnClick && !isAboveOverlay && !isAboveButton) { + showOverlay = false; + return true; + } + + boolean consumed = isAboveOverlay; + for (PresetElement element : list) { + if (!element.isMouseAboveBox(mouseX, mouseY)) { + if (element.isEditing) { + element.cancelEdit(); + consumed = true; + } + } else { + element.boxClicked(button); + consumed = true; + } + + if (element.isMouseAboveReset(mouseX, mouseY) && !element.key.isDefault()) { + element.key.defaultState.writeTo(element.key.currentState); + manager.save(); + consumed = true; + } + } + + return consumed; + } + + public boolean isMouseAbove(int mouseX, int mouseY) { + if (scroll.isMouseDragging) + return true; + + return mouseX >= startX && mouseX < endX && mouseY >= startY && mouseY < endY; + } + + public int getWidth() { + return (int) (width / scale); + } + + public boolean isVisible() { + return showOverlay; + } + + + public class Scrollable { + private float scrollY, targetScrollY, maxScroll; + private int totalHeight, scrollbarHeight, barWidth = 2; + private float heightFactor, scrollFactor; + + private boolean isMouseDragging; + private int startDragY; + + public Scrollable() { + } + + public void init() { + totalHeight = 0; + for (PresetElement element : list) + totalHeight += element.getHeight(); + // contentTotal includes top spacing before first element + float contentTotal = totalHeight + yStartSpacing; + + // maxScroll is how much content exceeds the visible area + maxScroll = Math.max(0f, contentTotal - height); + + // compute scrollbar height proportional to visible area vs content + heightFactor = contentTotal > 0f ? (float) height / contentTotal : 1f; + scrollbarHeight = Math.max((int) (height * heightFactor), 20); + + // scrollFactor maps scrollY -> pixel offset for scrollbar + scrollFactor = maxScroll > 0f ? (height - yStartSpacing - scrollbarHeight) / maxScroll : 0f; + } + + public void update(int wheel) { + if (wheel != 0 && isMouseAbove(mouseX, mouseY)) + targetScrollY = ValueUtil.clamp(targetScrollY - wheel / 15, 0, maxScroll); + + + if (Mouse.isButtonDown(0)) { + if (!isMouseDragging && isMouseAboveBar(mouseX, mouseY)) { + isMouseDragging = true; + startDragY = (int) (mouseY - scrollY * scrollFactor); + } + } else + isMouseDragging = false; + + if (isMouseDragging) + scrollY = targetScrollY = ValueUtil.clamp((mouseY - startDragY) / scrollFactor, 0, maxScroll); + + if (scrollY != targetScrollY) { + scrollY = ValueUtil.lerp(scrollY, targetScrollY, 0.1f); + + if (Math.abs(scrollY - targetScrollY) < 0.001) // Snap to exact target value + scrollY = targetScrollY; + } + + scrollY = ValueUtil.clamp(scrollY, 0, maxScroll); + } + + public void drawBar() { + GL11.glPushMatrix(); + GL11.glTranslatef(endX - barWidth - 2, startY + yStartSpacing + scrollY * scrollFactor, 1); + GuiUtil.drawRectD(0, 1, barWidth, scrollbarHeight - 1, (isMouseAboveBar(mouseX, mouseY) || isMouseDragging) ? 0xffbababa : 0xff767676); + GL11.glPopMatrix(); + } + + public boolean isMouseAboveBar(int mouseX, int mouseY) { + int barX = endX - barWidth - 2; + int barY = (int) (startY + yStartSpacing + scrollY * scrollFactor); + + return mouseX >= barX && mouseX < barX + barWidth && mouseY >= barY && mouseY < barY + scrollbarHeight; + } + } + + public class PresetElement { + public KeyPreset key; + public boolean isEditing; + public float boxScreenX, boxScreenY; + + public KeyPreset.KeyState newState = new KeyPreset.KeyState(); + + public PresetElement(KeyPreset key) { + this.key = key; + } + + public void drawDescription() { + boolean hasDescription = key.description != null; + String name = String.format("> %s", key.name); + font.drawString(name, 0, hasDescription ? 0 : 10, 0xffffff); + + if (!key.shouldConflict) + font.drawString("(Conflict-free)", font.getStringWidth(name) + 5, 0, 0xface40); + + if (hasDescription) { + String description = String.format("- %s", key.description); + font.drawSplitString(description, 9, font.FONT_HEIGHT + 3, getMaxStringWidth() - 9, 0x888888); + } + } + + public void drawBox(int offsetY) { + //////////////////////////////////////////// + //////////////////////////////////////////// + //Box texture + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + GL11.glPushMatrix(); + GL11.glColor4f(1, 1, 1, 1); + float boxScaleX = 1.75f; + float boxWidth = 32 * boxScaleX; + // compute desired box X based on description width + float desiredBoxX = getMaxStringWidth() * scale + 8; //remove scaling from maxStringWidth + // clamp so the box + reset button always fit inside the content area + float resetWidth = 10f; + float contentAvailable = (scroll.maxScroll > 0 ? width - scroll.barWidth : width) - 4f; // small padding + float maxBoxX = Math.max(0f, contentAvailable - boxWidth - resetWidth - 4f); + boxScreenX = Math.min(desiredBoxX, maxBoxX); + GL11.glScalef(boxScaleX, 1, 0); + + if (isEditing) + new Color(0x4772b3, 1).glColor(); + else if (isMouseAboveBox(mouseX, mouseY)) + new Color(0x656565, 1).glColor(); + else + new Color(0x545454, 1).glColor(); + GuiUtil.drawTexturedModalRect(boxScreenX / boxScaleX, boxScreenY = offsetY - 5, 32, 20, 0, 492); + GL11.glPopMatrix(); + + //Reset box + boolean isDefault = key.isDefault(); + resetWidth = 10; + float resetScreenX = boxScreenX + boxWidth + 2; + + if (isMouseAboveReset(mouseX, mouseY) && !isDefault) + new Color(0x656565, 1).glColor(); + else + new Color(0x545454, 1).glColor(); + GuiUtil.drawTexturedModalRect(resetScreenX, boxScreenY, 10, 20, 33, 492); + + //////////////////////////////////////////// + //////////////////////////////////////////// + //Key name + String name = getBoxKeyName(); + float nameWidth = font.getStringWidth(name); + float nameScale = scale; + float nameBoxRatio = nameWidth * scale / boxWidth; + + GL11.glPushMatrix(); + if (nameBoxRatio > 0.9) { + nameScale /= nameWidth / boxWidth / 1.2f; + GL11.glTranslatef(0, nameBoxRatio * 0.5f, 0); + } + + float nameX = boxScreenX + boxWidth / 2 - (nameWidth / 2 * nameScale); + float nameY = offsetY + 11.5f - (font.FONT_HEIGHT / 2); + GL11.glScalef(nameScale, nameScale, 1); + GL11.glTranslatef(nameX / nameScale, nameY / nameScale, 1); + font.drawStringWithShadow(name, 0, 0, conflicts() ? 0xff5555 : 0xffffffff); + GL11.glPopMatrix(); + + //Reset letter + char letter = 'X'; + float letterX = resetScreenX + (resetWidth / 2) - (font.getCharWidth(letter) / 2 * scale) + 0.25f; + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, 1); + GL11.glTranslatef(letterX / scale, nameY / scale, 1); + font.drawStringWithShadow("" + letter, 0, 0, isDefault ? 0xff8a8a8a : 0xffffffff); + GL11.glPopMatrix(); + + //////////////////////////////////////////// + //////////////////////////////////////////// + } + + public void boxClicked(int button) { + if (isEditing) + setKey(-100 + button); + else if (button == 0) + isEditing = true; + } + + public void keyTyped(int typedKey) { + if (typedKey == 1) { + setKey(0); + return; + } + + newState.setState(newState.keyCode, KeyPreset.isCtrlKeyDown(), KeyPreset.isAltKeyDown(), KeyPreset.isShiftKeyDown()); + if (KeyPreset.isNotCtrlAltShift(typedKey)) + setKey(typedKey); + } + + public void setKey(int keyCode) { + if (keyCode == 0) + newState.clear(); + + newState.keyCode = keyCode; + newState.writeTo(key.currentState); + cancelEdit(); + manager.save(); + } + + public void cancelEdit() { + newState.clear(); + isEditing = false; + } + + public boolean conflicts() { + return list.stream().anyMatch(element -> key.shouldConflict && element.key.shouldConflict && element.key != this.key && element.key.equals(this.key)); + } + + public String getBoxKeyName() { + if (isEditing) { + String modifiers = ""; + if (KeyPreset.isCtrlKeyDown()) + modifiers += "CTRL "; + if (KeyPreset.isAltKeyDown()) + modifiers += "ALT "; + if (KeyPreset.isShiftKeyDown()) + modifiers += "SHIFT "; + + return !modifiers.isEmpty() ? modifiers : "Press a key"; + } + + return key.currentState.getName(); + } + + public boolean isMouseAboveBox(int mouseX, int mouseY) { + mouseX -= startX + 2; + mouseY -= startY + 5 - scroll.scrollY; + + float boxScaleX = 1.75f; + float screenY = boxScreenY + 5; + + return mouseX >= boxScreenX && mouseX < boxScreenX + 32 * boxScaleX && mouseY >= screenY && mouseY < screenY + 10; + } + + public boolean isMouseAboveReset(int mouseX, int mouseY) { + mouseX -= startX + 2; + mouseY -= startY + 5 - scroll.scrollY; + + float boxScaleX = 1.75f; + float resetWidth = 10, boxWidth = 32 * boxScaleX; + float screenX = boxScreenX + boxWidth + 2; + float screenY = boxScreenY + 10; + + return mouseX >= screenX && mouseX < screenX + resetWidth && mouseY >= screenY && mouseY < screenY + 10; + } + + public int getHeight() { + List wrappedDescription = font.listFormattedStringToWidth(String.format("- %s", key.description), getMaxStringWidth()); + + int nameHeight = font.FONT_HEIGHT; + int translations = font.FONT_HEIGHT; + int descriptionHeight = wrappedDescription.size() * (font.FONT_HEIGHT); + + return Math.round((nameHeight + descriptionHeight + translations) * scale) + elementSpacing; + } + + public int getMaxStringWidth() { + return (int) Math.max(getWidth() * RELATIVE_MAX_DESC_WIDTH, 0); + } + } + + public class OverlayButton { + public int startX, startY, endX, width, height; + public int textureWidth = 252, textureHeight = 109; + public float scale = 0.25f; + + public OverlayButton() { + } + + public void initGui(int endX, int startY) { + this.endX = endX; + this.startY = startY; + + startX = (int) (endX - textureWidth * scale / 2); + } + + public void drawButton() { + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + + double screenX = startX / scale, screenY = startY / scale; + int oldMinFilter = GL11.glGetTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GL11.glDisable(GL11.GL_BLEND); + + GL11.glPushMatrix(); + float color = aboveButton ? 1f : 0.4f; + GL11.glColor4f(color, color, color, 1); + GL11.glScalef(scale, scale, 1); + GuiUtil.drawTexturedModalRect(screenX, screenY, textureWidth, textureHeight, 0, 0); + GL11.glPopMatrix(); + + GL11.glEnable(GL11.GL_BLEND); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, oldMinFilter); + } + + public boolean isMouseAbove(int mouseX, int mouseY) { + return mouseX >= startX && mouseX < startX + textureWidth * scale / 2 && mouseY >= startY - 4 && mouseY < startY + textureHeight * scale / 2; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java new file mode 100644 index 000000000..b7b9ab63a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java @@ -0,0 +1,372 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + +import java.util.ArrayList; +import java.util.List; + +/** + * Handles bracket matching, brace span computation, and indent guide generation. + */ +public class BracketMatcher { + + /** + * Find matching bracket at cursor position. + * + * @return int[2] with {startPos, endPos} or null if no bracket found + */ + public static int[] findBracketSpanAt(String text, int pos) { + if (text == null || text.isEmpty()) return null; + + // Try at current position + if (pos >= 0 && pos < text.length()) { + char c = text.charAt(pos); + int found = 0; + if (c == '{') found = findClosingBracket(text.substring(pos), '{', '}'); + else if (c == '[') found = findClosingBracket(text.substring(pos), '[', ']'); + else if (c == '(') found = findClosingBracket(text.substring(pos), '(', ')'); + else if (c == '}') found = findOpeningBracket(text.substring(0, pos + 1), '{', '}'); + else if (c == ']') found = findOpeningBracket(text.substring(0, pos + 1), '[', ']'); + else if (c == ')') found = findOpeningBracket(text.substring(0, pos + 1), '(', ')'); + if (found != 0) return new int[]{pos, pos + found}; + } + + // Scan backwards, skipping whitespace on same line + if (pos > 0) { + int scan = pos - 1; + while (scan >= 0) { + char sc = text.charAt(scan); + if (sc == ' ' || sc == '\t') { + scan--; + continue; + } + if (sc == '\n') break; // Don't cross line boundary + + int found2 = 0; + if (sc == '{') found2 = findClosingBracket(text.substring(scan), '{', '}'); + else if (sc == '[') found2 = findClosingBracket(text.substring(scan), '[', ']'); + else if (sc == '(') found2 = findClosingBracket(text.substring(scan), '(', ')'); + else if (sc == '}') found2 = findOpeningBracket(text.substring(0, scan + 1), '{', '}'); + else if (sc == ']') found2 = findOpeningBracket(text.substring(0, scan + 1), '[', ']'); + else if (sc == ')') found2 = findOpeningBracket(text.substring(0, scan + 1), '(', ')'); + + if (found2 != 0) return new int[]{scan, scan + found2}; + break; // Not a bracket; stop + } + } + return null; + } + + /** + * Find all unmatched brace positions (both opens and closes) in the text. + * Returns a list of character indices into the text for braces that have no matching pair. + */ + public static List findUnmatchedBracePositions(String text) { + List unmatched = new ArrayList<>(); + if (text == null || text.isEmpty()) return unmatched; + + List stack = new ArrayList<>(); // {char, pos} + + boolean inLineComment = false; + boolean inBlockComment = false; + boolean inString = false; + boolean escape = false; + char stringDelimiter = 0; + + for (int pos = 0; pos < text.length(); pos++) { + char c = text.charAt(pos); + char next = pos + 1 < text.length() ? text.charAt(pos + 1) : 0; + + if (inString) { + if (escape) { + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == stringDelimiter) { + inString = false; + } else if (c == '\n') { + inString = false; + } + continue; + } + + if (inBlockComment) { + if (c == '*' && next == '/') { + inBlockComment = false; + pos++; + } + continue; + } + + if (inLineComment) { + if (c == '\n') { + inLineComment = false; + } + continue; + } + + if (c == '/' && next == '/') { + inLineComment = true; + pos++; + continue; + } + if (c == '/' && next == '*') { + inBlockComment = true; + pos++; + continue; + } + + if (c == '"' || c == '\'') { + inString = true; + stringDelimiter = c; + escape = false; + continue; + } + + if (c == '{' || c == '[' || c == '(') { + stack.add(new int[]{c, pos}); + } else if (c == '}' || c == ']' || c == ')') { + if (!stack.isEmpty()) { + int[] top = stack.get(stack.size() - 1); + char topc = (char) top[0]; + char expectOpen = (c == '}') ? '{' : (c == ']') ? '[' : '('; + if (topc == expectOpen) { + stack.remove(stack.size() - 1); + continue; + } else { + // mismatched: mark this close as unmatched + unmatched.add(pos); + continue; + } + } else { + // closing with no opens + unmatched.add(pos); + } + } + } + + // any remaining opens on stack are unmatched + for (int[] open : stack) { + unmatched.add(open[1]); + } + + return unmatched; + } + + /** + * Find closing bracket, returns offset from start or 0 if not found + */ + public static int findClosingBracket(String str, char open, char close) { + int depth = 0; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c == open) depth++; + else if (c == close) { + depth--; + if (depth == 0) return i; + } + } + return 0; + } + + /** + * Find opening bracket, returns negative offset from end or 0 if not found + */ + public static int findOpeningBracket(String str, char open, char close) { + int depth = 0; + for (int i = str.length() - 1; i >= 0; i--) { + char c = str.charAt(i); + if (c == close) depth++; + else if (c == open) { + depth--; + if (depth == 0) return i - str.length() + 1; + } + } + return 0; + } + + /** + * Compute brace spans for indent guides. + * Returns list of int[4]: {originalDepth, openLineIdx, closeLineIdx, adjustedDepth} + */ + public static List computeBraceSpans(String text, List lines) { + List spans = new ArrayList<>(); + if (text == null || text.isEmpty() || lines == null || lines.isEmpty()) + return spans; + + int lineIdx = 0; + int lineEnd = lines.get(0).end; + List openStack = new ArrayList<>(); + + boolean inLineComment = false; + boolean inBlockComment = false; + boolean inString = false; + boolean escape = false; + char stringDelimiter = 0; + + for (int pos = 0; pos < text.length(); pos++) { + // Advance line index + while (lineIdx < lines.size() - 1 && pos >= lineEnd) { + lineIdx++; + lineEnd = lines.get(lineIdx).end; + } + + char c = text.charAt(pos); + char next = pos + 1 < text.length() ? text.charAt(pos + 1) : 0; + + // String handling + if (inString) { + if (escape) { + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == stringDelimiter) { + inString = false; + } else if (c == '\n') { + inString = false; + } // Unterminated string + continue; + } + + // Block comment handling + if (inBlockComment) { + if (c == '*' && next == '/') { + inBlockComment = false; + pos++; + } + continue; + } + + // Line comment handling + if (inLineComment) { + if (c == '\n') { + inLineComment = false; + } + continue; + } + + // Start of comments + if (c == '/' && next == '/') { + inLineComment = true; + pos++; + continue; + } + if (c == '/' && next == '*') { + inBlockComment = true; + pos++; + continue; + } + + // Start of string + if (c == '"' || c == '\'') { + inString = true; + stringDelimiter = c; + escape = false; + continue; + } + + // Brace matching + if (c == '{') { + openStack.add(lineIdx); + } else if (c == '}') { + if (!openStack.isEmpty()) { + int openLine = openStack.remove(openStack.size() - 1); + int spanDepth = openStack.size() + 1; + spans.add(new int[]{spanDepth, openLine, lineIdx}); + } + } + } + + // Normalize depths for unmatched braces + if (!openStack.isEmpty()) { + int baseline = openStack.size(); + List adjusted = new ArrayList<>(spans.size()); + for (int[] span : spans) { + int adjustedDepth = Math.max(1, span[0] - baseline); + adjusted.add(new int[]{span[0], span[1], span[2], adjustedDepth}); + } + return adjusted; + } else { + for (int i = 0; i < spans.size(); i++) { + int[] span = spans.get(i); + spans.set(i, new int[]{span[0], span[1], span[2], span[0]}); + } + } + return spans; + } + + /** + * Find which brace span should be highlighted based on bracket at position + * + * @return int[2] {openLine, closeLine} or null + */ + public static int[] findHighlightedSpan(int bracketPos, List lines, List spans) { + if (bracketPos < 0 || lines == null || spans == null) return null; + + // Find line containing bracket + int bracketLineIdx = -1; + for (int i = 0; i < lines.size(); i++) { + LineData ld = lines.get(i); + if (bracketPos >= ld.start && bracketPos < ld.end) { + bracketLineIdx = i; + break; + } + } + + if (bracketLineIdx >= 0) { + for (int[] span : spans) { + int openLine = span[1]; + int closeLine = span[2]; + if (bracketLineIdx >= openLine && bracketLineIdx <= closeLine) { + return new int[]{openLine, closeLine}; + } + } + } + return null; + } + + /** + * Check if a brace at given position has a matching close with same indent + */ + public static boolean hasMatchingCloseWithSameIndent(String text, int bracePos, List lines, int currentIndent) { + if (text == null || lines == null || bracePos < 0) return false; + + // Find line containing the brace + int openLineIdx = -1; + for (int i = 0; i < lines.size(); i++) { + LineData ld = lines.get(i); + if (bracePos >= ld.start && bracePos < ld.end) { + openLineIdx = i; + break; + } + } + + if (openLineIdx < 0) return false; + + List spans = computeBraceSpans(text, lines); + for (int[] span : spans) { + if (span[1] == openLineIdx) { + int closeLineIdx = span[2]; + if (closeLineIdx < lines.size()) { + String closeLine = lines.get(closeLineIdx).text; + int closeIndent = getLineIndent(closeLine); + return closeIndent == currentIndent; + } + } + } + return false; + } + + /** + * Get leading whitespace count of a line + */ + private static int getLineIndent(String line) { + int indent = 0; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (c == ' ' || c == '\t') indent++; + else break; + } + return indent; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java new file mode 100644 index 000000000..844da165b --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -0,0 +1,693 @@ +package noppes.npcs.client.gui.util.script; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Helper class for resolving Java classpath imports and determining class types. + * Handles: + * - Class resolution via Class.forName + * - Inner class notation (Outer.Inner -> Outer$Inner) + * - Package validation + * - Type detection (interface/enum/class) + * - Result caching for performance + */ +public class ClassPathFinder { + + // Cache: fully-qualified class name -> ClassInfo (null if not found) + private final Map classCache = new HashMap<>(); + + // Cache: package prefix -> whether it's a valid package (has classes under it) + private final Set validPackages = new HashSet<>(); + + // Common java.lang classes (auto-imported in Java) + public static final Set JAVA_LANG_CLASSES = new HashSet<>(Arrays.asList( + "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", + "Character", "Boolean", "Number", "Void", "Thread", "Runnable", "Exception", "RuntimeException", + "Error", "Throwable", "StringBuilder", "StringBuffer", "Enum", "Comparable", "Iterable", + "CharSequence", "Cloneable", "Process", "ProcessBuilder", "Runtime", "SecurityManager", + "ClassLoader", "Package", "ArithmeticException", "ArrayIndexOutOfBoundsException", + "ClassCastException", "IllegalArgumentException", "IllegalStateException", + "IndexOutOfBoundsException", "NullPointerException", "NumberFormatException", + "UnsupportedOperationException", "AssertionError", "OutOfMemoryError", "StackOverflowError" + )); + + /** + * Information about a resolved class + */ + public static class ClassInfo { + public final String resolvedName; // Full class name with $ for inner classes + public final ClassType type; // INTERFACE, ENUM, or CLASS + public final int packageEndIndex; // Index in original path where package ends (for "java.util.List" -> 9) + + public ClassInfo(String resolvedName, ClassType type, int packageEndIndex) { + this.resolvedName = resolvedName; + this.type = type; + this.packageEndIndex = packageEndIndex; + } + } + + public enum ClassType { + INTERFACE, + ENUM, + CLASS + } + + /** + * Result of resolving an import path + */ + public static class ResolveResult { + public final boolean found; + public final ClassInfo classInfo; + public final String packagePortion; // The package part (e.g., "java.util") + public final String classPortion; // The class part (e.g., "List" or "IOverlay.ColorType") + public final List classSegments; // Each class/inner-class segment with its type + public final int invalidStartOffset; // Character offset where invalid portion starts (-1 if all valid or all invalid) + public final int packageSegmentCount; // Number of dot-separated identifier tokens that belong to the package portion + + private ResolveResult(boolean found, ClassInfo classInfo, String packagePortion, + String classPortion, List classSegments, int invalidStartOffset, + int packageSegmentCount) { + this.found = found; + this.classInfo = classInfo; + this.packagePortion = packagePortion; + this.classPortion = classPortion; + this.classSegments = classSegments != null ? classSegments : Collections.emptyList(); + this.invalidStartOffset = invalidStartOffset; + this.packageSegmentCount = packageSegmentCount; + } + + public static ResolveResult notFound(String packagePortion, String classPortion, int invalidStartOffset, int packageSegmentCount) { + return new ResolveResult(false, null, packagePortion, classPortion, null, invalidStartOffset, packageSegmentCount); + } + + public static ResolveResult found(ClassInfo info, String packagePortion, + String classPortion, List segments, int packageSegmentCount) { + return new ResolveResult(true, info, packagePortion, classPortion, segments, -1, packageSegmentCount); + } + } + + /** + * Represents a segment of the class portion (e.g., "IOverlay" or "ColorType") + */ + public static class ClassSegment { + public final String name; + public final ClassType type; + + public ClassSegment(String name, ClassType type) { + this.name = name; + this.type = type; + } + } + + /** + * Clear all caches (call when imports change) + */ + public void clearCache() { + classCache.clear(); + validPackages.clear(); + } + + /** + * Resolve an import path to determine if it's a valid class and get its type. + * Handles inner classes like "kamkeel.npcdbc.api.client.overlay.IOverlay.ColorType" + * + * @param importPath The import path (e.g., "java.util.List" or "kamkeel...IOverlay.ColorType") + * @return ResolveResult with resolution details + */ + public ResolveResult resolve(String importPath) { + if (importPath == null || importPath.isEmpty()) { + return ResolveResult.notFound("", "", -1, 0); + } + + // Normalize whitespace/newlines around dots so wrapped imports like + // "kamkeel.npcdbc.api.client.overlay.IOverlay.\n TextureFunction" become + // "kamkeel.npcdbc.api.client.overlay.IOverlay.TextureFunction" before resolving. + String normalizedPath = importPath.replaceAll("\\s*\\.\\s*", ".").trim(); + + // Handle trailing dot (user is still typing a package path like "java.util.") + boolean trailingDot = normalizedPath.endsWith("."); + String pathToResolve = trailingDot ? normalizedPath.substring(0, normalizedPath.length() - 1) : normalizedPath; + + if (pathToResolve.isEmpty()) { + return ResolveResult.notFound("", "", -1, 0); + } + + String[] segments = pathToResolve.split("\\."); + int n = segments.length; + + // Find the first uppercase segment (likely start of class name) + int firstUpperIndex = findFirstUppercaseSegment(segments); + + // No uppercase segments at all: this is a package-like path. + // Validate as far as we can and mark invalid tail red. + if (firstUpperIndex == n) { + int longestValid = findLongestValidPackagePrefixIndex(segments); + if (longestValid == n) { + // Entire path is a valid package (or cached as valid) + return ResolveResult.notFound(pathToResolve, "", -1, n); + } + + if (longestValid > 0) { + String validPrefix = buildPackagePortion(segments, longestValid); + String invalidRemainder = buildClassPortion(segments, longestValid, n); + int invalidOffset = validPrefix.length() + 1; // after the dot + return ResolveResult.notFound(validPrefix, invalidRemainder, invalidOffset, longestValid); + } + + // Nothing validated + return ResolveResult.notFound("", pathToResolve, 0, 0); + } + + // Trailing dot is handled above for lowercase-only paths; for mixed-case paths + // we continue resolving as usual. + + // Try different splits: package vs class boundary + // Start from firstUpperIndex and try treating each uppercase segment as potential class start + for (int classStartIdx = firstUpperIndex; classStartIdx < n; classStartIdx++) { + // Try different inner class depths (deepest first) + for (int classEndIdx = n; classEndIdx > classStartIdx; classEndIdx--) { + String candidate = buildClassName(segments, classStartIdx, classEndIdx); + ClassInfo info = tryResolveClass(candidate); + + if (info != null) { + // Found a valid class! + String packagePortion = buildPackagePortion(segments, classStartIdx); + String validClassPortion = buildClassPortion(segments, classStartIdx, classEndIdx); + + // Register this package as valid for future lookups + if (!packagePortion.isEmpty()) { + registerValidPackage(packagePortion); + } + + // Check if there are MORE segments after what we resolved (invalid inner class) + if (classEndIdx < n) { + // e.g., IOverlay.ColorType resolved but there's more like ".idk" + // Mark the extra parts as invalid + List classSegments = buildClassSegments(segments, classStartIdx, classEndIdx); + String fullClassPortion = buildClassPortion(segments, classStartIdx, n); + // Calculate where the invalid portion starts (after valid class + dot) + int validEndOffset = packagePortion.length() + (packagePortion.isEmpty() ? 0 : 1) + validClassPortion.length() + 1; + return new ResolveResult(false, info, packagePortion, + fullClassPortion, classSegments, validEndOffset, classStartIdx); + } + + // Full resolution successful + List classSegments = buildClassSegments(segments, classStartIdx, n); + return ResolveResult.found( + new ClassInfo(info.resolvedName, info.type, packagePortion.length()), + packagePortion, validClassPortion, classSegments, classStartIdx + ); + } + } + } + + // Class resolution failed completely + // Strategy: Use heuristic - everything before first uppercase = package, rest = class + // This ensures consistent behavior regardless of cache state + + if (firstUpperIndex > 0 && firstUpperIndex < n) { + // There's a package portion (lowercase) and class portion (uppercase start) + String packagePortion = buildPackagePortion(segments, firstUpperIndex); + String classPortion = buildClassPortion(segments, firstUpperIndex, n); + + // Check if we can detect a typo in the package by comparing with cached packages + int typoIndex = findPackageTypoIndex(segments, firstUpperIndex); + if (typoIndex >= 0 && typoIndex < firstUpperIndex) { + // Found a divergence from a known valid package + String validPrefix = buildPackagePortion(segments, typoIndex); + int invalidOffset = validPrefix.isEmpty() ? 0 : validPrefix.length() + 1; + return ResolveResult.notFound(validPrefix, buildClassPortion(segments, typoIndex, n), invalidOffset, typoIndex); + } + + // No typo detected - assume package is valid, only class portion is invalid + int invalidOffset = packagePortion.length() + 1; // After the package dot + return ResolveResult.notFound(packagePortion, classPortion, invalidOffset, firstUpperIndex); + + } else if (firstUpperIndex == 0) { + // Entire path starts with uppercase (e.g., "String" or "MyClass.Inner") + // Treat as class with no package + return ResolveResult.notFound("", pathToResolve, 0, 0); + + } else { + // All lowercase, no uppercase found - entire path is package-like + // If trailing dot was handled above, this is just a package without class + return ResolveResult.notFound(pathToResolve, "", -1, n); + } + } + + /** + * Find index where the typed package diverges from a known cached package. + * Returns -1 if no typo detected, or the segment index where divergence starts. + */ + private int findPackageTypoIndex(String[] segments, int classStartIdx) { + // Build the typed package path segment by segment and compare with cache + StringBuilder sb = new StringBuilder(); + int lastValidIndex = -1; + + for (int i = 0; i < classStartIdx; i++) { + if (i > 0) sb.append("."); + sb.append(segments[i]); + String currentPath = sb.toString(); + + // Check if this exact path is in cache + if (validPackages.contains(currentPath)) { + lastValidIndex = i + 1; // This segment is valid + } else if (i > 0) { + // Not in cache - check if parent is valid but this segment is wrong + String parentPath = buildPackagePortion(segments, i); + if (validPackages.contains(parentPath)) { + // Parent is valid, but current segment isn't in any cached path + // Check if there ARE cached paths extending from parent + boolean hasAlternative = false; + for (String cached : validPackages) { + if (cached.startsWith(parentPath + ".") && cached.length() > parentPath.length() + 1) { + hasAlternative = true; + break; + } + } + if (hasAlternative) { + // There are valid continuations from parent, but this segment doesn't match any + return i; // This segment is likely a typo + } + } + } + } + + // If we found some valid prefix but not all, return where it stopped being valid + if (lastValidIndex > 0 && lastValidIndex < classStartIdx) { + // Check if segment at lastValidIndex diverges from any known path + String validPrefix = buildPackagePortion(segments, lastValidIndex); + for (String cached : validPackages) { + if (cached.startsWith(validPrefix + ".")) { + // There's a known continuation - check if typed segment matches + String nextTyped = segments[lastValidIndex]; + String cachedRemainder = cached.substring(validPrefix.length() + 1); + String nextCached = cachedRemainder.contains(".") ? + cachedRemainder.substring(0, cachedRemainder.indexOf('.')) : cachedRemainder; + if (!nextTyped.equals(nextCached)) { + return lastValidIndex; // Divergence point + } + } + } + } + + return -1; // No typo detected + } + + /** + * Check if a package path is valid (has been seen in successful resolutions or can be validated) + */ + public boolean isValidPackage(String packagePath) { + if (packagePath == null || packagePath.isEmpty()) { + return false; + } + + // Check cache first + if (validPackages.contains(packagePath)) { + return true; + } + + // Try to consult the global PackageFinder index if initialized (fast O(1) lookup) + if (PackageFinder.find(packagePath)) { + registerValidPackage(packagePath); + return true; + } + + // Fallback: try to find any class under this package to validate it + String[] testClasses = getTestClassesForPackage(packagePath); + for (String testClass : testClasses) { + try { + Class.forName(testClass); + registerValidPackage(packagePath); + return true; + } catch (ClassNotFoundException | LinkageError ignored) { + } + } + + return false; + } + + /** + * Get the ClassInfo for a simple class name, using provided imports map + */ + public ClassInfo resolveSimpleName(String simpleName, Map importedClasses) { + return resolveSimpleName(simpleName, importedClasses, Collections.emptySet()); + } + + /** + * Resolve a simple class name using explicit imports, java.lang, or wildcard imports. + * Supports package wildcard imports (e.g., java.util.*) and class-member wildcards + * that have been added to `importedPackages` (e.g., kamkeel..IOverlay.* treated as + * attempting to resolve inner types of IOverlay). + */ + public ClassInfo resolveSimpleName(String simpleName, Map importedClasses, Set importedPackages) { + if (simpleName == null || simpleName.isEmpty()) { + return null; + } + + // Check imported classes first + String fullName = importedClasses.get(simpleName); + if (fullName != null) { + ClassInfo info = tryResolveClass(fullName); + if (info != null) return info; + } + + // Check java.lang classes + if (JAVA_LANG_CLASSES.contains(simpleName)) { + ClassInfo info = tryResolveClass("java.lang." + simpleName); + if (info != null) return info; + } + + // Check wildcard imports: these can be package wildcards (pkg.*) or + // class-member wildcards (Outer.*) where we want to allow resolving + // inner types of the Outer class. Heuristic: if the last segment of + // the imported path starts with uppercase, treat it as a class; otherwise + // treat it as a package. + if (importedPackages != null && !importedPackages.isEmpty()) { + for (String p : importedPackages) { + if (p == null || p.isEmpty()) continue; + String[] segs = p.split("\\."); + String last = segs[segs.length - 1]; + boolean lastIsClass = last.length() > 0 && Character.isUpperCase(last.charAt(0)); + + // Try package wildcard: pkg.SimpleName + if (!lastIsClass) { + String candidate = p + "." + simpleName; + ClassInfo info = tryResolveClass(candidate); + if (info != null) return info; + } else { + // Try inner type under the class: Outer$Inner + String candidateInner = p + "$" + simpleName; + ClassInfo info = tryResolveClass(candidateInner); + if (info != null) return info; + + // Also try package-like (in case someone imported a class-looking path but it is actually a package) + String candidatePkg = p + "." + simpleName; + info = tryResolveClass(candidatePkg); + if (info != null) return info; + } + } + } + + return null; + } + + /** + * Try to resolve a fully-qualified class name (with $ for inner classes) + */ + private ClassInfo tryResolveClass(String className) { + if (className == null || className.isEmpty()) { + return null; + } + + // Check cache + if (classCache.containsKey(className)) { + return classCache.get(className); + } + + ClassInfo result = null; + + try { + Class clazz = Class.forName(className); + result = createClassInfo(className, clazz); + classCache.put(className, result); + + int lastDot = className.lastIndexOf("."); + if (lastDot > 0) + registerValidPackage(className.substring(0, lastDot)); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // Not found with given name, will be cached as null + } catch (LinkageError e) { + // Linkage error - treat as not found + } + + return result; + } + + /** + * Public wrapper to attempt resolving a fully-qualified class name. + * Returns null if not found. + */ + public ClassInfo tryResolveClassName(String className) { + return tryResolveClass(className); + } + + private ClassInfo createClassInfo(String resolvedName, Class clazz) { + ClassType type; + if (clazz.isInterface()) { + type = ClassType.INTERFACE; + } else if (clazz.isEnum()) { + type = ClassType.ENUM; + } else { + type = ClassType.CLASS; + } + + // Calculate package end index + int pkgEnd = resolvedName.lastIndexOf('.'); + if (pkgEnd < 0) + pkgEnd = 0; + + return new ClassInfo(resolvedName, type, pkgEnd); + } + + /** + * Build a class name candidate from segments, using $ for inner classes + */ + private String buildClassName(String[] segments, int classStartIdx, int classEndIdx) { + StringBuilder sb = new StringBuilder(); + + // Package portion (dot-separated) + for (int i = 0; i < classStartIdx; i++) { + if (i > 0) + sb.append('.'); + sb.append(segments[i]); + } + + // Class portion ($ for inner classes after the first class) + for (int i = classStartIdx; i < classEndIdx; i++) { + if (i == classStartIdx) { + if (sb.length() > 0) + sb.append('.'); + } else { + sb.append('$'); + } + sb.append(segments[i]); + } + + return sb.toString(); + } + + private String buildPackagePortion(String[] segments, int classStartIdx) { + if (classStartIdx <= 0) + return ""; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < classStartIdx; i++) { + if (i > 0) + sb.append('.'); + sb.append(segments[i]); + } + return sb.toString(); + } + + private String buildClassPortion(String[] segments, int classStartIdx, int endIdx) { + StringBuilder sb = new StringBuilder(); + for (int i = classStartIdx; i < endIdx; i++) { + if (i > classStartIdx) + sb.append('.'); + sb.append(segments[i]); + } + return sb.toString(); + } + + private List buildClassSegments(String[] segments, int classStartIdx, int endIdx) { + List result = new ArrayList<>(); + StringBuilder fullName = new StringBuilder(); + + // Build package portion + for (int i = 0; i < classStartIdx; i++) { + if (i > 0) + fullName.append('.'); + fullName.append(segments[i]); + } + + // Add each class segment + for (int i = classStartIdx; i < endIdx; i++) { + if (i == classStartIdx) { + if (fullName.length() > 0) + fullName.append('.'); + } else { + fullName.append('$'); + } + fullName.append(segments[i]); + + // Try to resolve this partial class name + ClassInfo info = tryResolveClass(fullName.toString()); + ClassType type = (info != null) ? info.type : ClassType.CLASS; + result.add(new ClassSegment(segments[i], type)); + } + + return result; + } + + private int findFirstUppercaseSegment(String[] segments) { + for (int i = 0; i < segments.length; i++) { + if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) { + return i; + } + } + return segments.length; // No uppercase segment + } + + private int findLongestValidPackagePrefixIndex(String[] segments) { + int longestValid = 0; + for (int i = 1; i <= segments.length; i++) { + String pkg = buildPackagePortion(segments, i); + if (isValidPackage(pkg)) { + longestValid = i; + } else { + // If a prefix isn't valid, deeper prefixes can't be valid packages either. + break; + } + } + return longestValid; + } + + private void registerValidPackage(String packagePath) { + validPackages.add(packagePath); + + // Also register all parent packages + int lastDot; + String current = packagePath; + while ((lastDot = current.lastIndexOf('.')) > 0) { + current = current.substring(0, lastDot); + validPackages.add(current); + } + } + + private String[] getTestClassesForPackage(String packagePath) { + // Return common classes for well-known packages + switch (packagePath) { + case "java": + return new String[]{"java.lang.Object"}; + case "java.util": + return new String[]{"java.util.List", "java.util.Map", "java.util.Set"}; + case "java.io": + return new String[]{"java.io.File", "java.io.InputStream"}; + case "java.net": + return new String[]{"java.net.URL", "java.net.Socket"}; + case "java.lang": + return new String[]{"java.lang.Object", "java.lang.String"}; + default: + // For unknown packages, try common class suffixes + return new String[]{ + packagePath + ".package-info", // Won't work but harmless + }; + } + } + + // ==================== GENERIC TYPE PARSING ==================== + + /** + * Parse generic type content and return type names with their positions. + * Handles arbitrary nesting depth like "Map>>" + * + * @param genericContent The content inside <...> (without the outer angle brackets) + * @param importedClasses Map of simple name -> full class name for resolution + * @return List of TypeOccurrence with position and type info + */ + public List parseGenericTypes(String genericContent, Map importedClasses) { + return parseGenericTypes(genericContent, importedClasses, Collections.emptySet()); + } + + public List parseGenericTypes(String genericContent, Map importedClasses, Set importedPackages) { + List results = new ArrayList<>(); + parseGenericTypesRecursive(genericContent, 0, importedClasses, importedPackages, results); + return results; + } + + /** + * Represents a type occurrence within generic content + */ + public static class TypeOccurrence { + public final int startOffset; // Relative to the start of genericContent + public final int endOffset; + public final String typeName; + public final ClassType type; + + public TypeOccurrence(int startOffset, int endOffset, String typeName, ClassType type) { + this.startOffset = startOffset; + this.endOffset = endOffset; + this.typeName = typeName; + this.type = type; + } + } + + private void parseGenericTypesRecursive(String content, int baseOffset, + Map importedClasses, + Set importedPackages, + List results) { + if (content == null || content.isEmpty()) + return; + + int i = 0; + while (i < content.length()) { + char c = content.charAt(i); + + // Skip whitespace and punctuation + if (!Character.isJavaIdentifierStart(c)) { + i++; + continue; + } + + // Found start of identifier + int start = i; + while (i < content.length() && Character.isJavaIdentifierPart(content.charAt(i))) { + i++; + } + String typeName = content.substring(start, i); + + // Only process if it looks like a type name (starts with uppercase) + if (Character.isUpperCase(typeName.charAt(0))) { + // Resolve the type + ClassInfo info = resolveSimpleName(typeName, importedClasses, importedPackages); + ClassType classType = (info != null) ? info.type : ClassType.CLASS; + + results.add(new TypeOccurrence(baseOffset + start, baseOffset + i, typeName, classType)); + } + + // Skip whitespace + while (i < content.length() && Character.isWhitespace(content.charAt(i))) { + i++; + } + + // Check for nested generic + if (i < content.length() && content.charAt(i) == '<') { + int nestedStart = i + 1; + int depth = 1; + i++; + + // Find matching > + while (i < content.length() && depth > 0) { + if (content.charAt(i) == '<') + depth++; + else if (content.charAt(i) == '>') + depth--; + i++; + } + + // Recursively parse nested content + if (nestedStart < i - 1) { + String nestedContent = content.substring(nestedStart, i - 1); + parseGenericTypesRecursive(nestedContent, baseOffset + nestedStart, importedClasses, importedPackages, results); + } + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java new file mode 100644 index 000000000..ed586e304 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java @@ -0,0 +1,218 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + +import java.util.List; + +/** + * Handles line comment toggling for the script editor. + */ +public class CommentHandler { + + /** + * Result of comment toggle operation + */ + public static class ToggleResult { + public final String text; + public final int delta; // Change in length (negative if comment removed) + public final int nonWsIndex; // Index of first non-whitespace character + + public ToggleResult(String text, int delta, int nonWsIndex) { + this.text = text; + this.delta = delta; + this.nonWsIndex = nonWsIndex; + } + + public boolean wasCommented() { + return delta < 0; + } + } + + /** + * Toggle line comment on a single line + */ + public static ToggleResult toggleLine(String lineText) { + // Find first non-whitespace + int nonWs = 0; + while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) { + nonWs++; + } + + boolean hasContent = nonWs < lineText.length(); + boolean hasComment = hasContent && lineText.startsWith("//", nonWs); + String newText; + + if (hasComment) { + // Remove comment + int cut = Math.min(nonWs + 2, lineText.length()); + newText = lineText.substring(0, nonWs) + lineText.substring(cut); + } else if (hasContent) { + // Add comment + newText = lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs); + } else { + // Empty/whitespace line - no change + newText = lineText; + } + + return new ToggleResult(newText, newText.length() - lineText.length(), nonWs); + } + + /** + * Result of toggling comments on a selection + */ + public static class SelectionToggleResult { + public final String newText; + public final int newStartSelection; + public final int newEndSelection; + + public SelectionToggleResult(String text, int start, int end) { + this.newText = text; + this.newStartSelection = start; + this.newEndSelection = end; + } + } + + /** + * Toggle comments on all lines in a selection + */ + public static SelectionToggleResult toggleCommentSelection( + String text, + List lines, + int startSelection, + int endSelection) { + + StringBuilder newText = new StringBuilder(text.length() + 100); + int prevEnd = 0; + int newStart = -1; + int newEnd = -1; + int newIndex = 0; + + for (LineData line : lines) { + int safeLineStart = Math.max(0, Math.min(line.start, text.length())); + int safeLineEnd = Math.max(0, Math.min(line.end, text.length())); + + // Handle gap between lines (shouldn't happen normally) + if (safeLineStart > prevEnd) { + if (newStart == -1 && startSelection >= prevEnd && startSelection <= safeLineStart) { + newStart = newIndex + (startSelection - prevEnd); + } + if (newEnd == -1 && endSelection >= prevEnd && endSelection <= safeLineStart) { + newEnd = newIndex + (endSelection - prevEnd); + } + newText.append(text, prevEnd, safeLineStart); + newIndex += safeLineStart - prevEnd; + } + + // Check if this line is in selection + if (safeLineEnd > startSelection && safeLineStart < endSelection) { + String lineText = text.substring(safeLineStart, safeLineEnd); + ToggleResult tr = toggleLine(lineText); + String newLineText = tr.text; + int nonWs = tr.nonWsIndex; + int delta = tr.delta; + + // Update start selection position + if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { + int offsetInOld = startSelection - safeLineStart; + int newOffset = offsetInOld; + if (offsetInOld >= nonWs) { + newOffset = offsetInOld + delta; + } + newStart = newIndex + Math.max(0, newOffset); + } + + // Update end selection position + if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { + int offsetInOld = endSelection - safeLineStart; + int newOffset = offsetInOld; + if (offsetInOld >= nonWs) { + newOffset = offsetInOld + delta; + } + newEnd = newIndex + Math.max(0, newOffset); + } + + newText.append(newLineText); + newIndex += newLineText.length(); + } else { + // Line not in selection - keep unchanged + if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { + newStart = newIndex + (startSelection - safeLineStart); + } + if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { + newEnd = newIndex + (endSelection - safeLineStart); + } + newText.append(text, safeLineStart, safeLineEnd); + newIndex += safeLineEnd - safeLineStart; + } + prevEnd = safeLineEnd; + } + + // Handle remaining text after last line + if (prevEnd < text.length()) { + if (newStart == -1 && startSelection >= prevEnd && startSelection <= text.length()) { + newStart = newIndex + (startSelection - prevEnd); + } + if (newEnd == -1 && endSelection >= prevEnd && endSelection <= text.length()) { + newEnd = newIndex + (endSelection - prevEnd); + } + newText.append(text, prevEnd, text.length()); + newIndex += text.length() - prevEnd; + } + + // Fallback for positions not found + if (newStart == -1) newStart = Math.max(0, Math.min(startSelection, newText.length())); + if (newEnd == -1) newEnd = Math.max(0, Math.min(endSelection, newText.length())); + + return new SelectionToggleResult(newText.toString(), newStart, newEnd); + } + + /** + * Toggle comment on a single line at cursor position + */ + public static SingleLineToggleResult toggleCommentAtCursor( + String text, + List lines, + int cursorPosition) { + + for (LineData line : lines) { + int lineStart = Math.max(0, Math.min(line.start, text.length())); + int lineEnd = Math.max(0, Math.min(line.end, text.length())); + + if (cursorPosition >= lineStart && cursorPosition <= lineEnd) { + String lineText = text.substring(lineStart, lineEnd); + ToggleResult tr = toggleLine(lineText); + boolean hadComment = tr.delta < 0; + + String before = text.substring(0, lineStart); + String after = lineEnd <= text.length() ? text.substring(lineEnd) : ""; + String newText = before + tr.text + after; + + // Calculate new cursor position + int cursorDelta = hadComment ? -2 : 2; + int newCursor = cursorPosition + cursorDelta; + if (cursorPosition < lineStart + tr.nonWsIndex + (hadComment ? 2 : 0)) { + newCursor = cursorPosition; + } + newCursor = Math.max(lineStart, newCursor); + + return new SingleLineToggleResult(newText, newCursor); + } + } + + // No line found at cursor - return unchanged + return new SingleLineToggleResult(text, cursorPosition); + } + + /** + * Result of toggling comment on single line + */ + public static class SingleLineToggleResult { + public final String newText; + public final int newCursorPosition; + + public SingleLineToggleResult(String text, int cursor) { + this.newText = text; + this.newCursorPosition = cursor; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java b/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java new file mode 100644 index 000000000..52faebe0c --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java @@ -0,0 +1,218 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Handles cursor navigation logic (up, down, left, right, word movement). + */ +public class CursorNavigation { + + /** + * Move cursor up one line, maintaining column position + */ + public static int cursorUp(int cursorPosition, List lines, String text) { + if (lines == null || lines.isEmpty()) return 0; + + for (int i = 0; i < lines.size(); i++) { + LineData data = lines.get(i); + boolean isLastLine = (i == lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= data.start && cursorPosition <= data.end) + : (cursorPosition >= data.start && cursorPosition < data.end); + + if (isOnLine) { + if (i == 0) { + return 0; // Already on first line + } + + int column = Math.min(cursorPosition - data.start, data.text.length()); + LineData target = lines.get(i - 1); + int targetColumn = Math.min(column, target.text.length()); + int targetIndent = getLineIndent(target.text); + int minPos = target.start + Math.min(targetIndent, target.text.length()); + int targetPos = target.start + targetColumn; + return Math.max(minPos, targetPos); + } + } + return 0; + } + + /** + * Move cursor down one line, maintaining column position + */ + public static int cursorDown(int cursorPosition, List lines, String text) { + if (lines == null || lines.isEmpty()) return text != null ? text.length() : 0; + if (text != null && cursorPosition == text.length()) return cursorPosition; + + for (int i = 0; i < lines.size(); i++) { + LineData data = lines.get(i); + boolean isLastLine = (i == lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= data.start && cursorPosition <= data.end) + : (cursorPosition >= data.start && cursorPosition < data.end); + + if (isOnLine) { + if (i >= lines.size() - 1) { + return cursorPosition; // Already on last line + } + + int column = Math.min(cursorPosition - data.start, data.text.length()); + LineData target = lines.get(i + 1); + int targetColumn = Math.min(column, target.text.length()); + int targetIndent = getLineIndent(target.text); + int minPos = target.start + Math.min(targetIndent, target.text.length()); + int targetPos = target.start + targetColumn; + return Math.max(minPos, targetPos); + } + } + return text != null ? text.length() : 0; + } + + /** + * Move cursor left, optionally by word + */ + public static int cursorLeft(int cursorPosition, String text, Pattern wordPattern, boolean byWord) { + if (text == null || cursorPosition <= 0) return 0; + + int moveAmount = 1; + if (byWord && wordPattern != null) { + Matcher m = wordPattern.matcher(text.substring(0, cursorPosition)); + while (m.find()) { + if (m.start() != m.end()) { + moveAmount = cursorPosition - m.start(); + } + } + } + return Math.max(cursorPosition - moveAmount, 0); + } + + /** + * Move cursor right, optionally by word + */ + public static int cursorRight(int cursorPosition, String text, Pattern wordPattern, boolean byWord) { + if (text == null) return 0; + int textLen = text.length(); + if (cursorPosition >= textLen) return textLen; + + int moveAmount = 1; + if (byWord && wordPattern != null) { + Matcher m = wordPattern.matcher(text.substring(cursorPosition)); + if (m.find() && m.start() > 0) { + moveAmount = m.start(); + } else if (m.find()) { + moveAmount = m.start(); + } + } + return Math.min(cursorPosition + moveAmount, textLen); + } + + /** + * Get cursor position from mouse click coordinates + */ + public static int getPositionFromMouse( + int mouseX, int mouseY, + int areaX, int areaY, + int gutterWidth, + int lineHeight, + int scrolledLine, + double scrollFracOffset, + List lines, + String text, + FontWidthProvider fontWidth) { + + int xMouse = mouseX - (areaX + gutterWidth + 1); + int yMouse = mouseY - areaY - 1; + + // Account for fractional scroll offset + double fracPixels = scrollFracOffset * lineHeight; + yMouse = (int) Math.round(yMouse + fracPixels); + + if (lines == null || lines.isEmpty()) { + return text != null ? text.length() : 0; + } + + int visibleLines = Math.min(lines.size(), scrolledLine + 50); // Reasonable limit + + for (int i = 0; i < lines.size(); i++) { + LineData data = lines.get(i); + + if (i >= scrolledLine && i <= visibleLines) { + int yPos = (i - scrolledLine) * lineHeight; + + if (yMouse >= yPos && yMouse < yPos + lineHeight) { + // Found the line, now find character position + int lineWidth = 0; + char[] chars = data.text.toCharArray(); + + for (int j = 1; j <= chars.length; j++) { + int w = fontWidth.getWidth(data.text.substring(0, j)); + if (xMouse < lineWidth + (w - lineWidth) / 2) { + return data.start + j - 1; + } + lineWidth = w; + } + + // Click past end of line - place cursor at end of text (not on newline) + int posAfterChars = data.start + chars.length; + return Math.min(posAfterChars, text.length()); + } + } + } + + return text != null ? text.length() : 0; + } + + /** + * Interface for font width measurement + */ + public interface FontWidthProvider { + int getWidth(String text); + } + + /** + * Get leading whitespace count of a line + */ + private static int getLineIndent(String lineText) { + int indent = 0; + for (int i = 0; i < lineText.length(); i++) { + char c = lineText.charAt(i); + if (c == ' ' || c == '\t') indent++; + else break; + } + return indent; + } + + /** + * Find line index containing given position + */ + public static int findLineIndex(int position, List lines) { + if (lines == null || lines.isEmpty()) return 0; + + for (int i = 0; i < lines.size(); i++) { + LineData line = lines.get(i); + boolean isLastLine = (i == lines.size() - 1); + boolean isOnLine = isLastLine + ? (position >= line.start && position <= line.end) + : (position >= line.start && position < line.end); + if (isOnLine) { + return i; + } + } + return lines.size() - 1; + } + + /** + * Find line data containing given position + */ + public static LineData findLineAt(int position, List lines) { + int idx = findLineIndex(position, lines); + if (idx >= 0 && idx < lines.size()) { + return lines.get(idx); + } + return null; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java new file mode 100644 index 000000000..a1eedce0b --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java @@ -0,0 +1,1276 @@ +package noppes.npcs.client.gui.util.script; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Configurable code formatter with IntelliJ-like settings. + *

+ * Features: + * - Operator spacing (a+b → a + b) + * - Whitespace normalization (a b → a b) + * - Long line wrapping + * - Bracket spacing configuration + * - Preserves string content and comments + */ +public class FormatHelper { + + // ==================== SETTINGS ==================== + + /** + * Formatting configuration - similar to IntelliJ Code Style settings + */ + public static class FormatSettings { + // Spaces around operators + public boolean spaceAroundAssignment = true; // a = b + public boolean spaceAroundArithmetic = true; // a + b - c * d / e + public boolean spaceAroundComparison = true; // a == b, a != b, a <= b + public boolean spaceAroundLogical = true; // a && b || c + public boolean spaceAroundBitwise = true; // a & b | c ^ d + + // Spaces within brackets + public boolean spaceWithinParens = false; // (a) vs ( a ) + public boolean spaceWithinBrackets = false; // [a] vs [ a ] + public boolean spaceWithinBraces = true; // {a} vs { a } (for initializers) + + // Spaces before/after + public boolean spaceAfterComma = true; // a, b vs a,b + public boolean spaceBeforeComma = false; // a, b vs a , b + public boolean spaceAfterSemicolon = true; // for (a; b; c) vs for (a;b;c) + public boolean spaceBeforeSemicolon = false; + public boolean spaceAfterColon = true; // case a: b vs case a:b + public boolean spaceBeforeColon = false; + + // Method calls + public boolean spaceBeforeMethodParens = false; // foo() vs foo () + public boolean spaceAfterTypeCast = true; // (int) x vs (int)x + + // Control statements + public boolean spaceBeforeIfParens = true; // if (...) vs if(...) + public boolean spaceBeforeWhileParens = true; + public boolean spaceBeforeForParens = true; + public boolean spaceBeforeSwitchParens = true; + public boolean spaceBeforeCatchParens = true; + + // Braces + public boolean spaceBeforeOpenBrace = true; // if () { vs if (){ + + // Line length + public int maxLineLength = 120; + public boolean wrapLongLines = false; // Auto-wrap lines longer than maxLineLength + public int wrapIndentSpaces = 4; // Extra indent for wrapped lines + public boolean wrapComments = true; // Also wrap long comments + + // Wrap break preferences (where to break long lines) + public boolean wrapAfterComma = true; // Prefer breaking after commas + public boolean wrapAfterOperator = true; // Prefer breaking after operators + public boolean wrapBeforeDot = true; // Break before . for chained calls + public boolean wrapMethodArguments = true; // Each arg on new line when wrapping method calls + + // Whitespace normalization + public boolean normalizeWhitespace = true; // Multiple spaces → single space + public boolean trimTrailingWhitespace = true; + + // Unary operators + public boolean spaceAroundUnaryNot = false; // !a vs ! a + public boolean spaceAroundUnaryIncDec = false; // ++i vs ++ i + + /** + * Create default settings (similar to IntelliJ defaults) + */ + public static FormatSettings defaults() { + return new FormatSettings(); + } + + /** + * Create compact settings (minimal spacing) + */ + public static FormatSettings compact() { + FormatSettings s = new FormatSettings(); + s.spaceWithinParens = false; + s.spaceWithinBrackets = false; + s.spaceWithinBraces = false; + s.wrapLongLines = false; + return s; + } + } + + private FormatSettings settings; + + public FormatHelper() { + this.settings = FormatSettings.defaults(); + } + + public FormatHelper(FormatSettings settings) { + this.settings = settings != null ? settings : FormatSettings.defaults(); + } + + public void setSettings(FormatSettings settings) { + this.settings = settings; + } + + public FormatSettings getSettings() { + return settings; + } + + // ==================== MAIN FORMAT METHOD ==================== + + /** + * Format the entire text with all configured options + */ + public String format(String text) { + if (text == null || text.isEmpty()) return text; + + // Process line by line to preserve structure + String[] lines = text.split("\n", -1); + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < lines.length; i++) { + String line = formatLine(lines[i], text, getLineStartOffset(lines, i)); + + if (settings.trimTrailingWhitespace) { + line = trimTrailing(line); + } + + result.append(line); + if (i < lines.length - 1) { + result.append("\n"); + } + } + + return result.toString(); + } + + /** + * Split multiple statements on one line into separate lines with proper indentation. + * E.g., " this.a = 1; this.b = 2;" becomes two lines + * Also handles scope: "if(x){ int y = 20; }" becomes proper multi-line structure + */ + private String splitStatementsOntoSeparateLines(String text) { + String[] lines = text.split("\n", -1); + StringBuilder result = new StringBuilder(); + + for (int lineIdx = 0; lineIdx < lines.length; lineIdx++) { + String line = lines[lineIdx]; + + // Skip empty lines or comment-only lines + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) { + result.append(line); + if (lineIdx < lines.length - 1) result.append("\n"); + continue; + } + + // Extract leading indentation + int indent = 0; + while (indent < line.length() && (line.charAt(indent) == ' ' || line.charAt(indent) == '\t')) { + indent++; + } + String indentation = line.substring(0, indent); + String content = line.substring(indent); + + // Split by semicolons (but preserve those in strings, for loops, etc.) + List statements = splitBySemicolons(content); + + for (int si = 0; si < statements.size(); si++) { + String stmt = statements.get(si).trim(); + if (stmt.isEmpty()) continue; + + // Check if statement opens a scope { and has code after it + int braceIdx = stmt.indexOf('{'); + if (braceIdx >= 0 && braceIdx < stmt.length() - 1) { + String afterBrace = stmt.substring(braceIdx + 1).trim(); + if (!afterBrace.isEmpty() && !afterBrace.equals("}")) { + // Split: keep up to and including {, put rest on new line with extra indent + result.append(indentation).append(stmt, 0, braceIdx + 1).append("\n"); + // Calculate new indent (add 4 spaces) + String newIndent = indentation + " "; + result.append(newIndent).append(afterBrace); + if (si < statements.size() - 1) result.append("\n"); + continue; + } + } + + result.append(indentation).append(stmt); + // Add semicolon back if it was removed + if (si < statements.size() - 1) { + result.append("\n"); + } + } + + if (lineIdx < lines.length - 1) result.append("\n"); + } + + return result.toString(); + } + + /** + * Split content by semicolons, preserving those in strings and for-loops + */ + private List splitBySemicolons(String content) { + List statements = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + int parenDepth = 0; + boolean inString = false; + boolean inChar = false; + boolean escaped = false; + int forLoopDepth = 0; // Track for(...) depth to preserve semicolons + + for (int i = 0; i < content.length(); i++) { + char c = content.charAt(i); + char prev = i > 0 ? content.charAt(i - 1) : ' '; + + // Track strings + if (c == '\"' && !escaped && !inChar) { + inString = !inString; + } else if (c == '\'' && !escaped && !inString) { + inChar = !inChar; + } + + escaped = (c == '\\' && !escaped); + + if (inString || inChar) { + current.append(c); + continue; + } + + // Track for loops + if (c == '(' && i >= 3 && content.substring(Math.max(0, i - 3), i + 1).matches(".*\\bfor\\s*\\(")) { + forLoopDepth++; + parenDepth++; + } else if (c == '(') { + parenDepth++; + } else if (c == ')') { + parenDepth--; + if (forLoopDepth > 0 && parenDepth < forLoopDepth) { + forLoopDepth = 0; + } + } + + // Semicolon: split if not in for-loop header + if (c == ';' && forLoopDepth == 0) { + current.append(c); + statements.add(current.toString()); + current = new StringBuilder(); + continue; + } + + current.append(c); + } + + // Add remaining content + if (current.length() > 0) { + statements.add(current.toString()); + } + + return statements; + } + + private int getLineStartOffset(String[] lines, int lineIndex) { + int offset = 0; + for (int i = 0; i < lineIndex; i++) { + offset += lines[i].length() + 1; // +1 for \n + } + return offset; + } + + /** + * Format a single line of code + */ + private String formatLine(String line, String fullText, int lineOffset) { + if (line.trim().isEmpty()) return line; + + // Preserve leading indentation + int indent = 0; + while (indent < line.length() && (line.charAt(indent) == ' ' || line.charAt(indent) == '\t')) { + indent++; + } + String indentation = line.substring(0, indent); + String content = line.substring(indent); + + if (content.isEmpty()) return line; + + // Skip formatting for lines that are entirely comments + String trimmed = content.trim(); + if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) { + return line; + } + + // Skip formatting for continuation lines (lines that start with operators, dots, etc.) + // These are wrapped lines from a previous format and should be preserved as-is + if (isContinuationLine(trimmed)) { + return line; + } + + // Format the content + String formatted = formatContent(content, fullText, lineOffset + indent); + + return indentation + formatted; + } + + /** + * Check if a line appears to be a continuation of a previous line + * (i.e., starts with an operator, dot, or other continuation character) + */ + private boolean isContinuationLine(String trimmedContent) { + if (trimmedContent.isEmpty()) return false; + char firstChar = trimmedContent.charAt(0); + // Lines starting with these are likely continuations + if (firstChar == '.' || firstChar == '+' || firstChar == '-' || + firstChar == '*' || firstChar == '/' || firstChar == '%' || + firstChar == '&' || firstChar == '|' || firstChar == '^' || + firstChar == '?' || firstChar == ':' || firstChar == ',') { + return true; + } + // Also check for && and || + if (trimmedContent.startsWith("&&") || trimmedContent.startsWith("||")) { + return true; + } + return false; + } + + /** + * Format line content, preserving strings and comments + */ + private String formatContent(String content, String fullText, int offset) { + // Find string/comment regions to preserve + List preserveRegions = findPreserveRegions(content); + + if (preserveRegions.isEmpty()) { + // No strings/comments, format everything + return formatCodeSegment(content); + } + + // Build result by formatting non-preserved regions + StringBuilder result = new StringBuilder(); + int lastEnd = 0; + + for (int[] region : preserveRegions) { + // Format code before this preserved region + if (region[0] > lastEnd) { + String codeSegment = content.substring(lastEnd, region[0]); + result.append(formatCodeSegment(codeSegment)); + } + + // Append preserved region unchanged + result.append(content.substring(region[0], region[1])); + lastEnd = region[1]; + } + + // Format remaining code after last preserved region + if (lastEnd < content.length()) { + result.append(formatCodeSegment(content.substring(lastEnd))); + } + + return result.toString(); + } + + /** + * Find string literals and comments that should not be formatted + */ + private List findPreserveRegions(String content) { + List regions = new ArrayList<>(); + + int i = 0; + while (i < content.length()) { + char c = content.charAt(i); + + // String literal + if (c == '"') { + int end = findStringEnd(content, i, '"'); + regions.add(new int[]{i, end}); + i = end; + continue; + } + + // Char literal + if (c == '\'') { + int end = findStringEnd(content, i, '\''); + regions.add(new int[]{i, end}); + i = end; + continue; + } + + // Line comment + if (c == '/' && i + 1 < content.length() && content.charAt(i + 1) == '/') { + regions.add(new int[]{i, content.length()}); + break; + } + + // Block comment (could span multiple lines but we process line-by-line) + if (c == '/' && i + 1 < content.length() && content.charAt(i + 1) == '*') { + int end = content.indexOf("*/", i + 2); + if (end >= 0) { + regions.add(new int[]{i, end + 2}); + i = end + 2; + } else { + regions.add(new int[]{i, content.length()}); + break; + } + continue; + } + + i++; + } + + return regions; + } + + /** + * Find end of string/char literal + */ + private int findStringEnd(String content, int start, char quote) { + int i = start + 1; + while (i < content.length()) { + char c = content.charAt(i); + if (c == '\\' && i + 1 < content.length()) { + i += 2; // Skip escape sequence + continue; + } + if (c == quote) { + return i + 1; + } + i++; + } + return content.length(); + } + + // ==================== CODE SEGMENT FORMATTING ==================== + + /** + * Format a code segment (no strings/comments) + */ + private String formatCodeSegment(String code) { + if (code.isEmpty()) return code; + + String result = code; + + // Normalize whitespace first + if (settings.normalizeWhitespace) { + result = normalizeWhitespace(result); + } + + // Apply operator spacing + result = formatOperators(result); + + // Apply bracket spacing + result = formatBrackets(result); + + // Apply keyword spacing + result = formatKeywords(result); + + // Apply punctuation spacing + result = formatPunctuation(result); + + return result; + } + + /** + * Normalize whitespace (multiple spaces → single space) + */ + private String normalizeWhitespace(String code) { + // Replace multiple spaces with single space, but preserve indentation + return code.replaceAll(" +", " "); + } + + /** + * Format operator spacing + */ + private String formatOperators(String code) { + String result = code; + + // Assignment operators + if (settings.spaceAroundAssignment) { + // Compound assignments first (longer patterns first) + result = formatBinaryOp(result, "+=", " += "); + result = formatBinaryOp(result, "-=", " -= "); + result = formatBinaryOp(result, "*=", " *= "); + result = formatBinaryOp(result, "/=", " /= "); + result = formatBinaryOp(result, "%=", " %= "); + result = formatBinaryOp(result, "&=", " &= "); + result = formatBinaryOp(result, "|=", " |= "); + result = formatBinaryOp(result, "^=", " ^= "); + result = formatBinaryOp(result, "<<=", " <<= "); + result = formatBinaryOp(result, ">>=", " >>= "); + result = formatBinaryOp(result, ">>>=", " >>>= "); + + // Simple assignment (but not ==, !=, <=, >=) + result = formatSimpleAssignment(result); + } + + // Comparison operators + if (settings.spaceAroundComparison) { + result = formatBinaryOp(result, "==", " == "); + result = formatBinaryOp(result, "!=", " != "); + result = formatBinaryOp(result, "<=", " <= "); + result = formatBinaryOp(result, ">=", " >= "); + // Simple < > (but not generics) + result = formatComparisonOperators(result); + } + + // Logical operators + if (settings.spaceAroundLogical) { + result = formatBinaryOp(result, "&&", " && "); + result = formatBinaryOp(result, "||", " || "); + } + + // Arithmetic operators + if (settings.spaceAroundArithmetic) { + // Handle ++ and -- first to avoid conflicts + result = formatArithmeticOperators(result); + } + + // Bitwise operators (but not in generics) + if (settings.spaceAroundBitwise) { + result = formatBitwiseOperators(result); + } + + return result; + } + + /** + * Format a binary operator with proper spacing + */ + private String formatBinaryOp(String code, String op, String replacement) { + // First normalize: remove extra spaces around the operator + String pattern = "\\s*" + Pattern.quote(op) + "\\s*"; + return code.replaceAll(pattern, replacement); + } + + /** + * Format simple assignment = (not ==, !=, <=, >=, etc.) + */ + private String formatSimpleAssignment(String code) { + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < code.length()) { + char c = code.charAt(i); + if (c == '=') { + // Check it's not part of ==, !=, <=, >=, +=, etc. + char prev = i > 0 ? code.charAt(i - 1) : ' '; + char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; + + boolean isCompound = prev == '!' || prev == '<' || prev == '>' || + prev == '+' || prev == '-' || prev == '*' || + prev == '/' || prev == '%' || prev == '&' || + prev == '|' || prev == '^'; + boolean isEquality = next == '='; + + if (!isCompound && !isEquality) { + // Ensure space before = + if (result.length() > 0 && result.charAt(result.length() - 1) != ' ') { + result.append(' '); + } + result.append('='); + // Ensure space after = + if (next != ' ') { + result.append(' '); + } + } else { + result.append(c); + } + } else { + result.append(c); + } + i++; + } + return result.toString(); + } + + /** + * Format < and > comparison operators (avoiding generics) + */ + private String formatComparisonOperators(String code) { + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < code.length()) { + char c = code.charAt(i); + + if ((c == '<' || c == '>') && !isLikelyGeneric(code, i, c)) { + char prev = i > 0 ? code.charAt(i - 1) : ' '; + char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; + + // Skip if part of <<, >>, <=, >= + if ((c == '<' && next == '<') || (c == '>' && next == '>') || + next == '=' || prev == '<' || prev == '>') { + result.append(c); + } else { + // Add spacing + if (result.length() > 0 && result.charAt(result.length() - 1) != ' ') { + result.append(' '); + } + result.append(c); + if (next != ' ' && next != '=') { + result.append(' '); + } + } + } else { + result.append(c); + } + i++; + } + return result.toString(); + } + + /** + * Check if < or > is likely part of a generic type declaration + */ + private boolean isLikelyGeneric(String code, int pos, char bracket) { + if (bracket == '<') { + // Check for pattern like List<, Map<, etc. + if (pos > 0) { + int start = pos - 1; + while (start > 0 && Character.isLetterOrDigit(code.charAt(start - 1))) { + start--; + } + String before = code.substring(start, pos); + // Common generic types or capital letter start (type name) + if (!before.isEmpty() && Character.isUpperCase(before.charAt(0))) { + return true; + } + } + // Check if there's a matching > + int depth = 1; + for (int i = pos + 1; i < code.length() && depth > 0; i++) { + char c = code.charAt(i); + if (c == '<') depth++; + else if (c == '>') depth--; + else if (c == ';' || c == '{' || c == '}') break; // Not a generic + } + return depth == 0; + } else { // '>' + // Check if there's a matching < before + int depth = 1; + for (int i = pos - 1; i >= 0 && depth > 0; i--) { + char c = code.charAt(i); + if (c == '>') depth++; + else if (c == '<') depth--; + else if (c == ';' || c == '{' || c == '}') break; + } + return depth == 0; + } + } + + /** + * Format arithmetic operators (+, -, *, /, %) + */ + private String formatArithmeticOperators(String code) { + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < code.length()) { + char c = code.charAt(i); + char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; + char prev = i > 0 ? code.charAt(i - 1) : ' '; + + boolean isArithOp = c == '+' || c == '-' || c == '*' || c == '/' || c == '%'; + + if (isArithOp) { + // Skip ++, --, +=, -=, *=, /=, %=, and unary +/- + if ((c == '+' && next == '+') || (c == '-' && next == '-') || + (c == '+' && prev == '+') || (c == '-' && prev == '-') || + next == '=') { + result.append(c); + } else if (isUnaryContext(code, i)) { + // Unary + or - (after operator, open paren, comma, etc.) + result.append(c); + } else { + // Binary operator - add spacing + if (result.length() > 0 && result.charAt(result.length() - 1) != ' ') { + result.append(' '); + } + result.append(c); + if (next != ' ' && next != '=' && !((c == '+' && next == '+') || (c == '-' && next == '-'))) { + result.append(' '); + } + } + } else { + result.append(c); + } + i++; + } + return result.toString(); + } + + /** + * Check if position is in a unary context (operator is unary, not binary) + */ + private boolean isUnaryContext(String code, int pos) { + if (pos == 0) return true; + + // Look back for context + int i = pos - 1; + while (i >= 0 && code.charAt(i) == ' ') { + i--; + } + + if (i < 0) return true; + + char prev = code.charAt(i); + // Unary context after: ( [ { , ; = + - * / % < > & | ^ ? : + return prev == '(' || prev == '[' || prev == '{' || prev == ',' || + prev == ';' || prev == '=' || prev == '+' || prev == '-' || + prev == '*' || prev == '/' || prev == '%' || prev == '<' || + prev == '>' || prev == '&' || prev == '|' || prev == '^' || + prev == '?' || prev == ':' || prev == '!'; + } + + /** + * Format bitwise operators + */ + private String formatBitwiseOperators(String code) { + String result = code; + + // Shift operators first + result = formatBinaryOp(result, ">>>", " >>> "); + result = formatBinaryOp(result, "<<", " << "); + result = formatBinaryOp(result, ">>", " >> "); + + // Single & | ^ (not && ||) + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < result.length()) { + char c = result.charAt(i); + char next = i + 1 < result.length() ? result.charAt(i + 1) : ' '; + char prev = i > 0 ? result.charAt(i - 1) : ' '; + + if ((c == '&' && next != '&' && prev != '&' && next != '=') || + (c == '|' && next != '|' && prev != '|' && next != '=') || + (c == '^' && next != '=')) { + if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') { + sb.append(' '); + } + sb.append(c); + if (next != ' ') { + sb.append(' '); + } + } else { + sb.append(c); + } + i++; + } + + return sb.toString(); + } + + // ==================== BRACKET FORMATTING ==================== + + /** + * Format bracket spacing + */ + private String formatBrackets(String code) { + String result = code; + + // Parentheses + if (settings.spaceWithinParens) { + result = result.replaceAll("\\(\\s*", "( "); + result = result.replaceAll("\\s*\\)", " )"); + } else { + result = result.replaceAll("\\(\\s+", "("); + result = result.replaceAll("\\s+\\)", ")"); + } + + // Square brackets + if (settings.spaceWithinBrackets) { + result = result.replaceAll("\\[\\s*", "[ "); + result = result.replaceAll("\\s*\\]", " ]"); + } else { + result = result.replaceAll("\\[\\s+", "["); + result = result.replaceAll("\\s+\\]", "]"); + } + + // Curly braces (for array initializers inline) + // Note: standalone braces for blocks are handled by indentation logic + + return result; + } + + // ==================== KEYWORD FORMATTING ==================== + + /** + * Format keyword spacing (if, while, for, etc.) + */ + private String formatKeywords(String code) { + String result = code; + + if (settings.spaceBeforeIfParens) { + result = result.replaceAll("\\bif\\s*\\(", "if ("); + } else { + result = result.replaceAll("\\bif\\s+\\(", "if("); + } + + if (settings.spaceBeforeWhileParens) { + result = result.replaceAll("\\bwhile\\s*\\(", "while ("); + } else { + result = result.replaceAll("\\bwhile\\s+\\(", "while("); + } + + if (settings.spaceBeforeForParens) { + result = result.replaceAll("\\bfor\\s*\\(", "for ("); + } else { + result = result.replaceAll("\\bfor\\s+\\(", "for("); + } + + if (settings.spaceBeforeSwitchParens) { + result = result.replaceAll("\\bswitch\\s*\\(", "switch ("); + } else { + result = result.replaceAll("\\bswitch\\s+\\(", "switch("); + } + + if (settings.spaceBeforeCatchParens) { + result = result.replaceAll("\\bcatch\\s*\\(", "catch ("); + } else { + result = result.replaceAll("\\bcatch\\s+\\(", "catch("); + } + + // Space before opening brace + if (settings.spaceBeforeOpenBrace) { + result = result.replaceAll("\\)\\s*\\{", ") {"); + } else { + result = result.replaceAll("\\)\\s+\\{", "){"); + } + + return result; + } + + // ==================== PUNCTUATION FORMATTING ==================== + + /** + * Format punctuation spacing (commas, semicolons, colons) + */ + private String formatPunctuation(String code) { + String result = code; + + // Commas + if (settings.spaceAfterComma && !settings.spaceBeforeComma) { + result = result.replaceAll("\\s*,\\s*", ", "); + } else if (settings.spaceAfterComma && settings.spaceBeforeComma) { + result = result.replaceAll("\\s*,\\s*", " , "); + } else if (!settings.spaceAfterComma && settings.spaceBeforeComma) { + result = result.replaceAll("\\s*,\\s*", " ,"); + } else { + result = result.replaceAll("\\s*,\\s*", ","); + } + + // Semicolons (but not in for loops - handle more carefully) + // This is tricky because for(;;) should be handled differently + // For now, just handle trailing semicolons + if (settings.trimTrailingWhitespace) { + result = result.replaceAll("\\s+;", ";"); + } + + return result; + } + + /** + * Trim trailing whitespace from a line + */ + private String trimTrailing(String line) { + int end = line.length(); + while (end > 0 && (line.charAt(end - 1) == ' ' || line.charAt(end - 1) == '\t')) { + end--; + } + return line.substring(0, end); + } + + // ==================== STATIC UTILITIES ==================== + + /** + * Format code with default settings + */ + public static String formatCode(String code) { + return new FormatHelper().format(code); + } + + /** + * Format code with custom settings + */ + public static String formatCode(String code, FormatSettings settings) { + return new FormatHelper(settings).format(code); + } + + // ==================== LINE WRAPPING ==================== + + /** + * Wrap long lines according to settings. + * Call this after format() if wrapLongLines is enabled. + * + * @param text The text to wrap + * @param maxWidth Maximum line width (use settings.maxLineLength if 0) + * @return Text with wrapped lines + */ + public String wrapLines(String text, int maxWidth) { + if (text == null || text.isEmpty()) return text; + + int width = maxWidth > 0 ? maxWidth : settings.maxLineLength; + String[] lines = text.split("\n", -1); + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + + if (getVisualLength(line) <= width) { + result.append(line); + } else { + // Determine if this is a comment line + String trimmed = line.trim(); + if (trimmed.startsWith("//")) { + result.append(wrapCommentLine(line, width)); + } else if (trimmed.startsWith("/*") || trimmed.startsWith("*")) { + result.append(wrapBlockCommentLine(line, width)); + } else { + result.append(wrapCodeLine(line, width)); + } + } + + if (i < lines.length - 1) { + result.append("\n"); + } + } + + return result.toString(); + } + + /** + * Get visual length of a line (tabs count as 4 spaces) + */ + private int getVisualLength(String line) { + int len = 0; + for (char c : line.toCharArray()) { + if (c == '\t') { + len += 4; + } else { + len++; + } + } + return len; + } + + /** + * Extract leading indentation from a line + */ + private String extractIndent(String line) { + int i = 0; + while (i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t')) { + i++; + } + return line.substring(0, i); + } + + /** + * Wrap a single-line comment (// style) + */ + private String wrapCommentLine(String line, int maxWidth) { + if (!settings.wrapComments) return line; + + String indent = extractIndent(line); + String content = line.substring(indent.length()); + + // Must start with // + if (!content.startsWith("//")) return line; + + String commentContent = content.substring(2).trim(); + String prefix = indent + "// "; + int prefixLen = getVisualLength(prefix); + int contentWidth = maxWidth - prefixLen; + + if (contentWidth <= 10) return line; // Too narrow to wrap + + StringBuilder result = new StringBuilder(); + result.append(indent).append("// "); + + String[] words = commentContent.split("\\s+"); + int lineLen = prefixLen; + + for (int i = 0; i < words.length; i++) { + String word = words[i]; + int wordLen = word.length(); + + if (lineLen + wordLen > maxWidth && lineLen > prefixLen) { + // Start new line + result.append("\n").append(prefix); + lineLen = prefixLen; + } + + if (lineLen > prefixLen) { + result.append(" "); + lineLen++; + } + + result.append(word); + lineLen += wordLen; + } + + return result.toString(); + } + + /** + * Wrap a block comment line (* style) + */ + private String wrapBlockCommentLine(String line, int maxWidth) { + if (!settings.wrapComments) return line; + + String indent = extractIndent(line); + String content = line.substring(indent.length()).trim(); + + // Determine prefix for wrapped lines + String prefix; + String textContent; + + if (content.startsWith("/**")) { + prefix = indent + " * "; + textContent = content.substring(3).trim(); + } else if (content.startsWith("/*")) { + prefix = indent + " * "; + textContent = content.substring(2).trim(); + } else if (content.startsWith("*")) { + prefix = indent + " * "; + textContent = content.substring(1).trim(); + } else { + return line; + } + + // Handle closing */ + boolean hasClose = textContent.endsWith("*/"); + if (hasClose) { + textContent = textContent.substring(0, textContent.length() - 2).trim(); + } + + int prefixLen = getVisualLength(prefix); + int contentWidth = maxWidth - prefixLen; + + if (contentWidth <= 10) return line; + + StringBuilder result = new StringBuilder(); + + // First line keeps original prefix + if (content.startsWith("/**")) { + result.append(indent).append("/** "); + } else if (content.startsWith("/*")) { + result.append(indent).append("/* "); + } else { + result.append(indent).append(" * "); + } + + String[] words = textContent.split("\\s+"); + int lineLen = prefixLen; + + for (int i = 0; i < words.length; i++) { + String word = words[i]; + int wordLen = word.length(); + + if (lineLen + wordLen > maxWidth && lineLen > prefixLen) { + result.append("\n").append(prefix); + lineLen = prefixLen; + } + + if (lineLen > prefixLen) { + result.append(" "); + lineLen++; + } + + result.append(word); + lineLen += wordLen; + } + + if (hasClose) { + result.append(" */"); + } + + return result.toString(); + } + + /** + * Wrap a code line at appropriate break points + */ + private String wrapCodeLine(String line, int maxWidth) { + String indent = extractIndent(line); + String content = line.substring(indent.length()); + + // Additional indent for wrapped lines + String wrapIndent = indent + spaces(settings.wrapIndentSpaces); + + // Find break points - positions where we can break the line + List breakPoints = findBreakPoints(content); + + if (breakPoints.isEmpty()) { + // No good break points, just return as-is + return line; + } + + StringBuilder result = new StringBuilder(); + result.append(indent); + + int currentLen = getVisualLength(indent); + int lastBreak = 0; + boolean isFirstLine = true; + + for (BreakPoint bp : breakPoints) { + int segmentEnd = bp.position; + String segment = content.substring(lastBreak, segmentEnd); + int segmentLen = getVisualLength(segment); + + // Check if adding this segment would exceed max width + if (currentLen + segmentLen > maxWidth && !isFirstLine) { + // Break here + result.append("\n").append(wrapIndent); + currentLen = getVisualLength(wrapIndent); + isFirstLine = false; + + // Trim leading space from continuation if any + segment = trimLeading(segment); + } else if (currentLen + segmentLen > maxWidth && isFirstLine && lastBreak > 0) { + // Even first segment is too long after indent, break before it + result.append("\n").append(wrapIndent); + currentLen = getVisualLength(wrapIndent); + isFirstLine = false; + } + + result.append(segment); + currentLen += getVisualLength(segment); + lastBreak = segmentEnd; + isFirstLine = false; + } + + // Append remaining content + if (lastBreak < content.length()) { + String remaining = content.substring(lastBreak); + int remLen = getVisualLength(remaining); + + if (currentLen + remLen > maxWidth && !isFirstLine) { + result.append("\n").append(wrapIndent); + remaining = trimLeading(remaining); + } + + result.append(remaining); + } + + return result.toString(); + } + + /** + * Trim leading whitespace (Java 8 compatible) + */ + private String trimLeading(String s) { + int start = 0; + while (start < s.length() && Character.isWhitespace(s.charAt(start))) { + start++; + } + return s.substring(start); + } + + /** + * A potential line break point + */ + private static class BreakPoint { + int position; // Position in string (after this char) + @SuppressWarnings("unused") + int priority; // Lower = better place to break (for future sorting) + + BreakPoint(int pos, int prio) { + this.position = pos; + this.priority = prio; + } + } + + /** + * Find good positions to break a line + */ + private List findBreakPoints(String content) { + List points = new ArrayList<>(); + + int parenDepth = 0; + int bracketDepth = 0; + int braceDepth = 0; + boolean inString = false; + char stringChar = 0; + + for (int i = 0; i < content.length(); i++) { + char c = content.charAt(i); + char prev = i > 0 ? content.charAt(i - 1) : ' '; + + // Track string state + if ((c == '"' || c == '\'') && prev != '\\') { + if (!inString) { + inString = true; + stringChar = c; + } else if (c == stringChar) { + inString = false; + } + } + + if (inString) continue; + + // Track bracket depth + if (c == '(') parenDepth++; + else if (c == ')') parenDepth--; + else if (c == '[') bracketDepth++; + else if (c == ']') bracketDepth--; + else if (c == '{') braceDepth++; + else if (c == '}') braceDepth--; + + // Calculate total nesting depth (prefer breaking at lower depth) + int depth = parenDepth + bracketDepth + braceDepth; + + // Find break points + if (settings.wrapAfterComma && c == ',') { + // After comma is a good break point (better at lower depth) + points.add(new BreakPoint(i + 1, 1 + depth)); + } + + if (settings.wrapAfterOperator) { + // After binary operators + if ((c == '+' || c == '-') && i > 0 && i < content.length() - 1) { + char next = content.charAt(i + 1); + if (prev != '(' && prev != '[' && prev != ',' && prev != '=' && + next != '+' && next != '-' && next != '=') { + points.add(new BreakPoint(i + 1, 3 + depth)); + } + } + if (c == '&' && i + 1 < content.length() && content.charAt(i + 1) == '&') { + points.add(new BreakPoint(i + 2, 2 + depth)); + } + if (c == '|' && i + 1 < content.length() && content.charAt(i + 1) == '|') { + points.add(new BreakPoint(i + 2, 2 + depth)); + } + } + + if (settings.wrapBeforeDot && c == '.') { + // Before dot (for method chaining) + points.add(new BreakPoint(i, 2 + depth)); + } + + // After opening brace + if (c == '{' && i + 1 < content.length()) { + points.add(new BreakPoint(i + 1, 1)); + } + } + + // Sort by position + points.sort((a, b) -> Integer.compare(a.position, b.position)); + + return points; + } + + /** + * Create a string of n spaces + */ + private String spaces(int n) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + sb.append(' '); + } + return sb.toString(); + } + + /** + * Format and wrap code in one step + */ + public String formatAndWrap(String text, int maxWidth) { + String formatted = format(text); + if (settings.wrapLongLines) { + return wrapLines(formatted, maxWidth); + } + return formatted; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java new file mode 100644 index 000000000..e3e023dee --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java @@ -0,0 +1,628 @@ +package noppes.npcs.client.gui.util.script; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.util.ChatAllowedCharacters; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; +import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.GL11; + +/** + * Go To Line:Column dialog for the script editor. + * Provides IntelliJ-like "Go to Line" functionality (Ctrl+G). + *

+ * Input format: "line" or "line:column" + * Example: "42" goes to line 42, column 1 + * Example: "42:15" goes to line 42, column 15 + */ +public class GoToLineDialog { + + // ==================== TEXTURE ==================== + public static final ResourceLocation TEXTURE = OverlayKeyPresetViewer.TEXTURE; + + // ==================== DIMENSIONS ==================== + private int x, y, width; + private int dialogWidth = 200; + private int dialogHeight = 51; + private int textFieldWidth = 180; + private int textFieldHeight = 16; + private int padding = 8; + + // ==================== STATE ==================== + private boolean visible = false; + private boolean focused = true; + + // ==================== TEXT FIELD ==================== + private String inputText = ""; + private int cursor = 0; + private int selectionStart = 0; + private int selectionEnd = 0; + private int scrollOffset = 0; + + // ==================== CURSOR BLINK ==================== + private int cursorCounter = 0; + private long lastInputTime = 0; + + // ==================== CALLBACK ==================== + private GoToLineCallback callback; + + private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; + + /** + * Callback interface for go-to-line operations + */ + public interface GoToLineCallback { + /** + * Get total line count + */ + int getLineCount(); + + /** + * Get the column count for a specific line (0-indexed) + */ + int getColumnCount(int lineIndex); + + /** + * Navigate to specific line and column (both 1-indexed for user display) + */ + void goToLineColumn(int line, int column); + + /** + * Called when dialog closes - should restore focus to editor + */ + void onDialogClose(); + + void unfocusMainEditor(); + + void focusMainEditor(); + } + + public void setCallback(GoToLineCallback callback) { + this.callback = callback; + } + + /** + * Initialize/update position for the dialog + */ + public void initGui(int guiX, int guiY, int guiWidth) { + this.width = guiWidth; + // Center the dialog horizontally + this.x = guiX + (guiWidth - dialogWidth) / 2; + // Position near top of editor + this.y = guiY + 40; + } + + // ==================== VISIBILITY ==================== + + public boolean isVisible() { + return visible; + } + + public void show() { + visible = true; + focused = true; + inputText = ""; + cursor = 0; + selectionStart = 0; + selectionEnd = 0; + scrollOffset = 0; + markActivity(); + if (callback != null) callback.unfocusMainEditor(); + } + + public void close() { + visible = false; + focused = false; + if (callback != null) { + callback.onDialogClose(); + callback.focusMainEditor(); + } + } + + public void toggle() { + if (visible && focused) { + close(); + } else { + show(); + } + } + + public boolean hasFocus() { + return visible && focused; + } + + public void unfocus() { + focused = false; + } + // ==================== RENDERING ==================== + + public void draw(int mouseX, int mouseY) { + if (!visible) + return; + + ParseResult result = null; + if (!inputText.isEmpty()) + result = parseInput(inputText); + + GL11.glPushMatrix(); + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + // Draw dialog background with shadow + drawDialogBackground(result != null && !result.valid); + + // Draw title + String title = "Go to Line"; + font.drawString(title, x + padding + 1, y + padding + 4, 0xFFe0e0e0); + + // Draw hint text showing valid range + int lineCount = callback != null ? callback.getLineCount() : 0; + String hint = "[" + lineCount + " lines]"; + int hintWidth = font.getStringWidth(hint); + dialogWidth = 200; + font.drawString(hint, x + dialogWidth - padding - hintWidth, y + padding + 4, 0xFF888888); + + + // Draw text field + int fieldX = x + padding + 0; + int fieldY = y + padding + 14 + 5; + textFieldWidth = dialogWidth - padding * 2; + drawTextField(fieldX, fieldY, textFieldWidth, textFieldHeight, inputText, cursor, + selectionStart, selectionEnd, scrollOffset, focused); + + if (inputText.isEmpty()) + font.drawString("[Line][:Column]", x + padding + 6, y + padding + 23, 0xFF888888); + + // Draw error/validation hint if input is invalid + if (result != null && !result.valid) + font.drawString(result.error, x + padding + 2, y + dialogHeight - 3, 0xFFff6666); + + GL11.glDisable(GL11.GL_BLEND); + GL11.glPopMatrix(); + } + + private void drawDialogBackground(boolean resultInvalid) { + int dialogHeight = this.dialogHeight; + if (resultInvalid) + dialogHeight += 5; // Pad by 6 + + int y = this.y + 5; + // Draw shadow + Gui.drawRect(x + 2, y + 2, x + dialogWidth + 2, y + dialogHeight + 2, 0x80000000); + + // Draw main background + Gui.drawRect(x, y, x + dialogWidth, y + dialogHeight, 0xFF2d2d30); + + // Draw border + Gui.drawRect(x, y, x + dialogWidth, y + 1, 0xFF3c3c3c); + Gui.drawRect(x, y + dialogHeight - 1, x + dialogWidth, y + dialogHeight, 0xFF3c3c3c); + Gui.drawRect(x, y, x + 1, y + dialogHeight, 0xFF3c3c3c); + Gui.drawRect(x + dialogWidth - 1, y, x + dialogWidth, y + dialogHeight, 0xFF3c3c3c); + } + + private void drawTextField(int fx, int fy, int fw, int fh, String text, int cursor, + int selStart, int selEnd, int scrollOff, boolean isFocused) { + // Draw text field background + Gui.drawRect(fx, fy, fx + fw, fy + fh, 0xFF1e1e1e); + Gui.drawRect(fx, fy, fx + fw, fy + 1, isFocused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fx, fy + fh - 1, fx + fw, fy + fh, isFocused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fx, fy, fx + 1, fy + fh, isFocused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fx + fw - 1, fy, fx + fw, fy + fh, isFocused ? 0xFF007acc : 0xFF3c3c3c); + + // Calculate visible area + int textX = fx + 4; + int textY = fy + (fh - 8) / 2; + int visibleWidth = fw - 8; + + // Apply scroll offset + String visibleText = text; + int adjustedCursor = cursor; + int adjustedSelStart = selStart; + int adjustedSelEnd = selEnd; + + if (scrollOff > 0 && scrollOff < text.length()) { + visibleText = text.substring(scrollOff); + adjustedCursor = cursor - scrollOff; + adjustedSelStart = Math.max(0, selStart - scrollOff); + adjustedSelEnd = Math.max(0, selEnd - scrollOff); + } + + // Draw selection highlight + if (adjustedSelStart != adjustedSelEnd) { + int minSel = Math.min(adjustedSelStart, adjustedSelEnd); + int maxSel = Math.max(adjustedSelStart, adjustedSelEnd); + String beforeSel = visibleText.substring(0, Math.min(minSel, visibleText.length())); + String inSel = visibleText.substring(Math.min(minSel, visibleText.length()), + Math.min(maxSel, visibleText.length())); + int selX = textX + font.getStringWidth(beforeSel); + int selW = font.getStringWidth(inSel); + Gui.drawRect(selX, textY - 1, selX + selW, textY + 9, 0xFF264f78); + } + + // Draw text + font.drawString(visibleText, textX, textY, 0xFFe0e0e0); + + // Draw cursor + if (isFocused && shouldShowCursor()) { + String beforeCursor = adjustedCursor > 0 && adjustedCursor <= visibleText.length() + ? visibleText.substring(0, adjustedCursor) : ""; + int cursorX = textX + font.getStringWidth(beforeCursor); + Gui.drawRect(cursorX, textY - 1, cursorX + 1, textY + 9, 0xFFe0e0e0); + } + } + + // ==================== INPUT HANDLING ==================== + + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (!visible) + return false; + + // Check if click is inside dialog + if (mouseX >= x && mouseX < x + dialogWidth && mouseY >= y && mouseY < y + dialogHeight) { + // Check if click is in text field + int fieldX = x + padding; + int fieldY = y + padding + 14; + if (mouseX >= fieldX && mouseX < fieldX + textFieldWidth && + mouseY >= fieldY && mouseY < fieldY + textFieldHeight) { + focused = true; + if (callback != null) callback.unfocusMainEditor(); + // Position cursor based on click + int clickX = mouseX - fieldX - 4; + String visibleText = scrollOffset > 0 && scrollOffset < inputText.length() + ? inputText.substring(scrollOffset) : inputText; + int newCursor = getCharIndexAtX(visibleText, clickX) + scrollOffset; + cursor = Math.max(0, Math.min(newCursor, inputText.length())); + selectionStart = cursor; + selectionEnd = cursor; + markActivity(); + } + return true; + } + + // Click outside dialog - close it + close(); + return false; + } + + private int getCharIndexAtX(String text, int targetX) { + int x = 0; + for (int i = 0; i < text.length(); i++) { + int charWidth = font.getCharWidth(text.charAt(i)); + if (x + charWidth / 2 > targetX) { + return i; + } + x += charWidth; + } + return text.length(); + } + + public boolean keyTyped(char c, int keyCode) { + // Escape - close dialog + if (keyCode == Keyboard.KEY_ESCAPE && visible) { + close(); + return true; + } + + if (!visible || !focused) + return false; + + boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); + boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + + // Enter - execute go to line + if (keyCode == Keyboard.KEY_RETURN || keyCode == Keyboard.KEY_NUMPADENTER) { + executeGoToLine(); + return true; + } + + // Select all (Ctrl+A) + if (ctrl && keyCode == Keyboard.KEY_A) { + selectionStart = 0; + selectionEnd = inputText.length(); + cursor = inputText.length(); + markActivity(); + return true; + } + + // Copy (Ctrl+C) + if (ctrl && keyCode == Keyboard.KEY_C) { + if (selectionStart != selectionEnd) { + int minSel = Math.min(selectionStart, selectionEnd); + int maxSel = Math.max(selectionStart, selectionEnd); + noppes.npcs.NoppesStringUtils.setClipboardContents(inputText.substring(minSel, maxSel)); + } + return true; + } + + // Cut (Ctrl+X) + if (ctrl && keyCode == Keyboard.KEY_X) { + if (selectionStart != selectionEnd) { + int minSel = Math.min(selectionStart, selectionEnd); + int maxSel = Math.max(selectionStart, selectionEnd); + noppes.npcs.NoppesStringUtils.setClipboardContents(inputText.substring(minSel, maxSel)); + inputText = inputText.substring(0, minSel) + inputText.substring(maxSel); + cursor = minSel; + selectionStart = cursor; + selectionEnd = cursor; + updateScrollOffset(); + markActivity(); + } + return true; + } + + // Paste (Ctrl+V) + if (ctrl && keyCode == Keyboard.KEY_V) { + String clipboard = noppes.npcs.NoppesStringUtils.getClipboardContents(); + if (clipboard != null) { + // Only allow digits and colon + StringBuilder filtered = new StringBuilder(); + for (char ch : clipboard.toCharArray()) { + if (Character.isDigit(ch) || ch == ':') { + filtered.append(ch); + } + } + insertText(filtered.toString()); + } + return true; + } + + // Backspace + if (keyCode == Keyboard.KEY_BACK) { + if (selectionStart != selectionEnd) { + deleteSelection(); + } else if (cursor > 0) { + inputText = inputText.substring(0, cursor - 1) + inputText.substring(cursor); + cursor--; + selectionStart = cursor; + selectionEnd = cursor; + updateScrollOffset(); + } + markActivity(); + return true; + } + + // Delete + if (keyCode == Keyboard.KEY_DELETE) { + if (selectionStart != selectionEnd) { + deleteSelection(); + } else if (cursor < inputText.length()) { + inputText = inputText.substring(0, cursor) + inputText.substring(cursor + 1); + } + markActivity(); + return true; + } + + // Left arrow + if (keyCode == Keyboard.KEY_LEFT) { + if (shift) { + if (cursor > 0) { + cursor--; + selectionEnd = cursor; + } + } else { + if (selectionStart != selectionEnd) { + cursor = Math.min(selectionStart, selectionEnd); + } else if (cursor > 0) { + cursor--; + } + selectionStart = cursor; + selectionEnd = cursor; + } + updateScrollOffset(); + markActivity(); + return true; + } + + // Right arrow + if (keyCode == Keyboard.KEY_RIGHT) { + if (shift) { + if (cursor < inputText.length()) { + cursor++; + selectionEnd = cursor; + } + } else { + if (selectionStart != selectionEnd) { + cursor = Math.max(selectionStart, selectionEnd); + } else if (cursor < inputText.length()) { + cursor++; + } + selectionStart = cursor; + selectionEnd = cursor; + } + updateScrollOffset(); + markActivity(); + return true; + } + + // Home + if (keyCode == Keyboard.KEY_HOME) { + if (shift) { + selectionEnd = 0; + cursor = 0; + } else { + cursor = 0; + selectionStart = cursor; + selectionEnd = cursor; + } + updateScrollOffset(); + markActivity(); + return true; + } + + // End + if (keyCode == Keyboard.KEY_END) { + if (shift) { + selectionEnd = inputText.length(); + cursor = inputText.length(); + } else { + cursor = inputText.length(); + selectionStart = cursor; + selectionEnd = cursor; + } + updateScrollOffset(); + markActivity(); + return true; + } + + // Type character - only allow digits and colon + if (ChatAllowedCharacters.isAllowedCharacter(c)) { + if (Character.isDigit(c) || c == ':') { + insertText("" + c); + markActivity(); + return true; + } + } + + return true; // Consume all other keys when dialog is focused + } + + private void insertText(String text) { + if (selectionStart != selectionEnd) { + deleteSelection(); + } + inputText = inputText.substring(0, cursor) + text + inputText.substring(cursor); + cursor += text.length(); + selectionStart = cursor; + selectionEnd = cursor; + updateScrollOffset(); + } + + private void deleteSelection() { + int minSel = Math.min(selectionStart, selectionEnd); + int maxSel = Math.max(selectionStart, selectionEnd); + inputText = inputText.substring(0, minSel) + inputText.substring(maxSel); + cursor = minSel; + selectionStart = cursor; + selectionEnd = cursor; + updateScrollOffset(); + } + + private void updateScrollOffset() { + int fieldWidth = textFieldWidth - 8; + String beforeCursor = cursor > 0 ? inputText.substring(0, cursor) : ""; + int cursorX = font.getStringWidth(beforeCursor); + + // Scroll to keep cursor visible + if (cursorX - scrollOffset * 6 > fieldWidth) { + scrollOffset = Math.max(0, cursor - fieldWidth / 6); + } else if (cursor < scrollOffset) { + scrollOffset = cursor; + } + } + + private void executeGoToLine() { + ParseResult result = parseInput(inputText); + if (result.valid && callback != null) { + callback.goToLineColumn(result.line, result.column); + close(); + } + } + + // ==================== PARSING ==================== + + private static class ParseResult { + boolean valid; + int line; + int column; + String error; + + static ParseResult invalid(String error) { + ParseResult r = new ParseResult(); + r.valid = false; + r.error = error; + return r; + } + + static ParseResult of(int line, int column) { + ParseResult r = new ParseResult(); + r.valid = true; + r.line = line; + r.column = column; + return r; + } + } + + private ParseResult parseInput(String input) { + if (input == null || input.trim().isEmpty()) { + return ParseResult.invalid("Enter line number"); + } + + input = input.trim(); + int line, column = 1; + + int colonIndex = input.indexOf(':'); + if (colonIndex >= 0) { + // Format: line:column + String linePart = input.substring(0, colonIndex); + String colPart = input.substring(colonIndex + 1); + + if (linePart.isEmpty()) { + return ParseResult.invalid("Invalid line number"); + } + + try { + line = Integer.parseInt(linePart); + } catch (NumberFormatException e) { + return ParseResult.invalid("Invalid line number"); + } + + if (!colPart.isEmpty()) { + try { + column = Integer.parseInt(colPart); + } catch (NumberFormatException e) { + return ParseResult.invalid("Invalid column number"); + } + } + } else { + // Format: just line + try { + line = Integer.parseInt(input); + } catch (NumberFormatException e) { + return ParseResult.invalid("Invalid line number"); + } + } + + // Validate ranges + int lineCount = callback != null ? callback.getLineCount() : 0; + if (line < 1) { + return ParseResult.invalid("Line must be >= 1"); + } + if (line > lineCount) { + return ParseResult.invalid("Line must be <= " + lineCount); + } + if (column < 1) { + return ParseResult.invalid("Column must be >= 1"); + } + // Column validation happens at callback - depends on actual line length + + return ParseResult.of(line, column); + } + + // ==================== CURSOR BLINK ==================== + + public void updateCursor() { + cursorCounter++; + } + + private void markActivity() { + lastInputTime = System.currentTimeMillis(); + } + + private boolean shouldShowCursor() { + // Keep cursor visible for 500ms after any input + if (System.currentTimeMillis() - lastInputTime < 500) { + return true; + } + // Then blink every 500ms + return (cursorCounter / 10) % 2 == 0; + } + + // ==================== GETTERS ==================== + + public int getTotalHeight() { + return visible ? dialogHeight + 10 : 0; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java new file mode 100644 index 000000000..12a5567ad --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java @@ -0,0 +1,376 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + +import java.util.Arrays; +import java.util.List; + +/** + * Helper class for indentation-related operations. + */ +public class IndentHelper { + + public static final int TAB_SIZE = 4; + + /** + * Get leading whitespace count of a line + */ + public static int getLineIndent(String lineText) { + int leading = 0; + for (int i = 0; i < lineText.length(); i++) { + char c = lineText.charAt(i); + if (c == ' ' || c == '\t') leading++; + else break; + } + return leading; + } + + /** + * Extract leading whitespace as string + */ + public static String getIndentString(String lineText) { + int indent = getLineIndent(lineText); + return lineText.substring(0, indent); + } + + /** + * Create a string of n spaces + */ + public static String spaces(int n) { + if (n <= 0) return ""; + char[] arr = new char[n]; + Arrays.fill(arr, ' '); + return new String(arr); + } + + /** + * Calculate expected indent for a line based on context + */ + public static int getExpectedIndent(LineData currentLine, List allLines) { + if (currentLine == null || allLines == null) return 0; + + int idx = allLines.indexOf(currentLine); + if (idx <= 0) return 0; + + // Find previous non-empty line + LineData prevLine = null; + for (int i = idx - 1; i >= 0; i--) { + LineData line = allLines.get(i); + if (line.text.trim().length() > 0) { + prevLine = line; + break; + } + } + + if (prevLine == null) return 0; + + int prevIndent = getLineIndent(prevLine.text); + String prevTrimmed = prevLine.text.trim(); + String currTrimmed = currentLine.text.trim(); + + // If current line starts with }, expected is one level back + if (currTrimmed.startsWith("}")) { + return Math.max(0, prevIndent - TAB_SIZE); + } + + // If previous line ends with {, expected is one level forward + if (prevTrimmed.endsWith("{")) { + return prevIndent + TAB_SIZE; + } + + return prevIndent; + } + + /** + * Calculate auto-indent for Enter key + */ + public static String getAutoIndentForEnter(String lineText, int cursorInLine) { + int leading = 0; + while (leading < lineText.length() && + (lineText.charAt(leading) == ' ' || lineText.charAt(leading) == '\t')) { + leading++; + } + + String baseIndent = lineText.substring(0, leading); + int relativeCursor = Math.max(0, Math.min(cursorInLine, lineText.length())); + String beforeCursor = lineText.substring(0, relativeCursor); + + // Find last non-whitespace before cursor + int lastNonWs = -1; + for (int i = beforeCursor.length() - 1; i >= 0; i--) { + if (!Character.isWhitespace(beforeCursor.charAt(i))) { + lastNonWs = i; + break; + } + } + + // Add extra indent if last char is { + // Only add extra indent if cursor is actually after the opening brace (not just in trailing whitespace) + // Check that the last non-whitespace is at or after the line's indent position + boolean opensBlock = lastNonWs >= 0 && lastNonWs >= leading && beforeCursor.charAt(lastNonWs) == '{'; + if (opensBlock) { + return baseIndent + spaces(TAB_SIZE); + } + + return baseIndent; + } + + /** + * Calculate target indent for Tab key + */ + public static int calculateTabTargetIndent(int currentIndent, int cursorColumn, boolean atTextStart) { + if (atTextStart) { + // At text start, move to next tab stop + return ((currentIndent / TAB_SIZE) + 1) * TAB_SIZE; + } else { + // Inside indent, snap to nearest tab stop + int remainder = currentIndent % TAB_SIZE; + if (remainder == 0) { + return currentIndent + TAB_SIZE; + } else { + int down = currentIndent - remainder; + int up = currentIndent + (TAB_SIZE - remainder); + int distDown = remainder; + int distUp = TAB_SIZE - remainder; + if (distUp <= distDown) return up; + else return down; + } + } + } + + /** + * Calculate target indent for Shift+Tab + */ + public static int calculateShiftTabTargetIndent(int currentIndent) { + return Math.max(0, ((currentIndent - 1) / TAB_SIZE) * TAB_SIZE); + } + + /** + * Format entire text with proper indentation and operator spacing + */ + public static FormatResult formatText(String text, int cursorPosition) { + return formatText(text, cursorPosition, 0); + } + + /** + * Format entire text with proper indentation, operator spacing, and optional line wrapping + * + * @param text The text to format + * @param cursorPosition Current cursor position + * @param viewportWidth Viewport width for line wrapping (0 = no wrapping) + */ + public static FormatResult formatText(String text, int cursorPosition, int viewportWidth) { + // First pass: fix indentation + String indented = formatIndentation(text, cursorPosition); + + // Second pass: apply operator spacing and whitespace normalization + FormatHelper helper = new FormatHelper(); + FormatHelper.FormatSettings settings = helper.getSettings(); + + // Enable wrapping if viewport width is provided + if (viewportWidth > 0) { + settings.wrapLongLines = true; + settings.wrapComments = true; + // Use 75% of viewport width as max line length for some margin + int charWidth = 6; // Approximate character width + settings.maxLineLength = Math.max(60, (int) (viewportWidth * 0.8f / charWidth)); + } + + String formatted = helper.format(indented); + + // Apply wrapping if enabled + if (viewportWidth > 0 && settings.wrapLongLines) { + formatted = helper.wrapLines(formatted, settings.maxLineLength); + } + + // Recalculate cursor position (try to maintain relative position) + int newCursorPos = Math.min(cursorPosition, formatted.length()); + + // Try to find cursor on same line with same content context + String[] origLines = text.split("\n", -1); + String[] newLines = formatted.split("\n", -1); + + int lineStartPos = 0; + int cursorLine = -1; + int cursorColInLine = 0; + + for (int li = 0; li < origLines.length; li++) { + int lineEndPos = lineStartPos + origLines[li].length(); + if (cursorPosition >= lineStartPos && cursorPosition <= lineEndPos) { + cursorLine = li; + cursorColInLine = cursorPosition - lineStartPos; + break; + } + lineStartPos = lineEndPos + 1; + } + + if (cursorLine >= 0 && cursorLine < newLines.length) { + int newLineStart = 0; + for (int li = 0; li < cursorLine; li++) { + newLineStart += newLines[li].length() + 1; + } + // Clamp cursor to new line length + newCursorPos = newLineStart + Math.min(cursorColInLine, newLines[cursorLine].length()); + } + + return new FormatResult(formatted, newCursorPos); + } + + /** + * Format indentation only (first pass) + */ + private static String formatIndentation(String text, int cursorPosition) { + String[] linesArr = text.split("\n", -1); + StringBuilder out = new StringBuilder(text.length() + 32); + + boolean inString = false; + boolean escape = false; + boolean inBlockComment = false; + int depth = 0; + + for (int li = 0; li < linesArr.length; li++) { + String line = linesArr[li]; + String trimmedLeading = line.replaceAll("^[ \\t]+", ""); + int originalIndent = getLineIndent(line); + + int opens = 0, closes = 0; + boolean startsWithClose = false; + + // Parse line for braces + for (int idx = 0; idx < line.length(); idx++) { + char c = line.charAt(idx); + char next = idx + 1 < line.length() ? line.charAt(idx + 1) : 0; + + if (inString) { + if (escape) { + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == '"') { + inString = false; + } + continue; + } + + if (inBlockComment) { + if (c == '*' && next == '/') { + inBlockComment = false; + idx++; + } + continue; + } + + if (c == '/' && next == '/') break; + if (c == '/' && next == '*') { + inBlockComment = true; + idx++; + continue; + } + if (c == '"') { + inString = true; + escape = false; + continue; + } + + if (c == '{') { + opens++; + } else if (c == '}') { + closes++; + if (!startsWithClose && line.substring(0, idx).trim().isEmpty()) { + startsWithClose = true; + } + } + } + + int indentLevel = depth; + if (startsWithClose) indentLevel = Math.max(0, indentLevel - 1); + int targetIndent = indentLevel * TAB_SIZE; + + // Check if this is a continuation line: + // 1. Line starts with operator/dot (explicit continuation) + // 2. Previous line ends with operator (implicit continuation - this line continues it) + // If so, preserve extra indentation beyond the base level + boolean isContinuation = isContinuationLine(trimmedLeading); + if (!isContinuation && li > 0) { + // Check if previous line ends with an operator (indicating this line continues it) + String prevLine = linesArr[li - 1].trim(); + isContinuation = lineEndsWithContinuationOperator(prevLine); + } + + if (isContinuation && originalIndent > targetIndent) { + // This is a continuation line - preserve its extra indent + out.append(spaces(originalIndent)).append(trimmedLeading); + } else { + out.append(spaces(targetIndent)).append(trimmedLeading); + } + if (li < linesArr.length - 1) out.append('\n'); + + depth = Math.max(0, depth + opens - closes); + } + + return out.toString(); + } + + /** + * Check if a line appears to be a continuation of a previous line + * (i.e., starts with an operator, dot, or other continuation character) + */ + private static boolean isContinuationLine(String trimmedContent) { + if (trimmedContent.isEmpty()) return false; + char firstChar = trimmedContent.charAt(0); + // Lines starting with these are likely continuations + if (firstChar == '.' || firstChar == '+' || firstChar == '-' || + firstChar == '*' || firstChar == '/' || firstChar == '%' || + firstChar == '&' || firstChar == '|' || firstChar == '^' || + firstChar == '?' || firstChar == ':' || firstChar == ',') { + return true; + } + // Also check for && and || + if (trimmedContent.startsWith("&&") || trimmedContent.startsWith("||")) { + return true; + } + return false; + } + + /** + * Check if a line ends with an operator that indicates the next line is a continuation + */ + private static boolean lineEndsWithContinuationOperator(String trimmedLine) { + if (trimmedLine.isEmpty()) return false; + // Remove trailing comments + int commentIdx = trimmedLine.indexOf("//"); + if (commentIdx >= 0) { + trimmedLine = trimmedLine.substring(0, commentIdx).trim(); + } + if (trimmedLine.isEmpty()) return false; + + // Check if ends with operator + char lastChar = trimmedLine.charAt(trimmedLine.length() - 1); + if (lastChar == '+' || lastChar == '-' || lastChar == '*' || + lastChar == '/' || lastChar == '%' || lastChar == '=' || + lastChar == '&' || lastChar == '|' || lastChar == '^' || + lastChar == '?' || lastChar == ':' || lastChar == ',' || + lastChar == '(' || lastChar == '[') { + // But not if it's ; or } which end statements + return true; + } + // Check for && and || + if (trimmedLine.endsWith("&&") || trimmedLine.endsWith("||")) { + return true; + } + return false; + } + + /** + * Result of text formatting + */ + public static class FormatResult { + public final String text; + public final int cursorPosition; + + public FormatResult(String text, int cursorPosition) { + this.text = text; + this.cursorPosition = cursorPosition; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java new file mode 100644 index 000000000..fe09cb81a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -0,0 +1,1316 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.TextContainer; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class JavaTextContainer extends TextContainer { + + public static final Pattern MODIFIER = Pattern.compile( + "\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b"); + public static final Pattern KEYWORD = Pattern.compile( + "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b"); + + public static final Pattern CLASS_DECL = Pattern.compile("\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + + public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + ; + + public static final Pattern METHOD_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name + ); + public static final Pattern METHOD_CALL = Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); + + // Class-level global fields + public static final Pattern GLOBAL_FIELD_DECL = Pattern.compile( + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + + // Local fields inside methods (simplified) + public static final Pattern LOCAL_FIELD_DECL = Pattern.compile( + "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + + public static final Pattern STRING = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); + public static final Pattern COMMENT = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*|#.*"); + public static final Pattern NUMBER = Pattern.compile( + "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); + + // Import statement: import [static] package.path.ClassName [.*]; (semicolon optional for live typing) + // Allows optional whitespace (including newlines) around the dots so wrapped imports match. + // Match imports while allowing optional whitespace/newlines around dots. + public static final Pattern IMPORT = Pattern.compile( + "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*?)?\\s*(?:;|$)"); + + // ClassPathFinder for resolving imports and determining class types + private final ClassPathFinder classPathFinder = new ClassPathFinder(); + + public List lines = new ArrayList<>(); + public List methodBlocks = new ArrayList<>(); + + public JavaTextContainer(String text) { + super(text); + } + + // Map an offset into the normalized path (tokens joined with '.') back to + // an absolute index in the original substring `orig` (absolute positions start at pathStart). + // The normalization removes whitespace/newlines around dots; this function + // advances a normalized counter when it encounters identifier chars or dots + // and returns the corresponding original index (absolute). + private int mapNormalizedOffsetToOriginal(String orig, int normalizedOffset, int pathStart) { + if (orig == null || orig.isEmpty()) return pathStart; + int norm = 0; + for (int i = 0; i < orig.length(); i++) { + char c = orig.charAt(i); + if (Character.isLetterOrDigit(c) || c == '_' || c == '.') { + norm++; + } + if (norm >= normalizedOffset) { + return pathStart + i; + } + } + return pathStart + orig.length(); + } + + public void init(String text, int width, int height) { + this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n"); + lines.clear(); + String[] split = text.split("\n", -1); + + int totalChars = 0; + for (String l : split) { + StringBuilder line = new StringBuilder(); + // Break the source line `l` into layout-friendly segments using `regexWord`. + // `regexWord` finds word tokens (letters/numbers/underscore/hyphen), newlines + // or the end-of-line. The loop takes substrings from the previous match + // index `i` up to the current `m.start()` so each `word` contains the + // next token plus any following delimiters (spaces, punctuation). + // This lets us measure and wrap at token boundaries rather than mid-word. + Matcher m = regexWord.matcher(l); + int i = 0; + while (m.find()) { + String word = l.substring(i, m.start()); + if (ClientProxy.Font.width(line + word) > width - 10) { + // Note: `end` is an exclusive offset into the full text (start..end). + // For wrapped lines we record the current `totalChars` as the start + // and compute the exclusive end as `start + line.length()`. + lines.add(new LineData(line.toString(), totalChars, totalChars += line.length())); + line = new StringBuilder(); + } + line.append(word); + i = m.start(); + } + lines.add(new LineData(line.toString(), totalChars, totalChars += line.length() + 1)); + } + linesCount = lines.size(); + totalHeight = linesCount * lineHeight; + // Number of fully-visible lines that fit in the given viewport height. + // Use floor division and ensure at least 1 line is visible. + // Don't forget -1, fixes enter auto-scrolling properly + visibleLines = Math.max(height / lineHeight - 1, 1); + } + + private List globalFields = new ArrayList<>(); + private List localFields = new ArrayList<>(); + private java.util.Map importedClasses = new java.util.HashMap<>(); // simpleName -> fullName + private java.util.Set importedPackages = new java.util.HashSet<>(); // wildcard imports + private java.util.Set unresolvedClasses = new java.util.HashSet<>(); // classes that failed to resolve + private java.util.List importEntries = new java.util.ArrayList<>(); + + private static class ImportEntry { + public final String fullPath; // normalized path (no trailing . or *) + public final String matchText; // full matched import text + public final int pathStart; // start of the path group + public final int pathEnd; // end of the path group + public final int importKwStart; // start of the 'import' keyword + public final int importKwEnd; // end of the 'import' keyword + public final int starPos; // absolute pos of '*' in matchText, or -1 + public final boolean isWildcard; + // Cached resolve result (may be null for wildcard imports or if not resolved yet) + public ClassPathFinder.ResolveResult resolveResult; + + public ImportEntry(String fullPath, String matchText, int pathStart, int pathEnd, int importKwStart, int importKwEnd, int starPos, boolean isWildcard) { + this.fullPath = fullPath; + this.matchText = matchText; + this.pathStart = pathStart; + this.pathEnd = pathEnd; + this.importKwStart = importKwStart; + this.importKwEnd = importKwEnd; + this.starPos = starPos; + this.isWildcard = isWildcard; + } + } + + private void collectImports() { + importedClasses.clear(); + importedPackages.clear(); + importEntries.clear(); + unresolvedClasses.clear(); + classPathFinder.clearCache(); + + List excluded = MethodBlock.getExcludedRanges(text); + Matcher m = IMPORT.matcher(text); + while (m.find()) { + if (isInExcludedRange(m.start(), excluded)) + continue; + String fullPath = m.group(1).trim(); + String matchText = m.group(0); + int pathStart = m.start(1); + int pathEnd = m.end(1); + int importKwStart = m.start(); + int importKwEnd = Math.min(importKwStart + 6, text.length()); + + boolean isWildcard = matchText.contains("*"); + int starIndex = matchText.indexOf('*'); + int absStar = starIndex != -1 ? m.start() + starIndex : -1; + + // Record entry for finalization later + importEntries.add(new ImportEntry(fullPath, matchText, pathStart, pathEnd, importKwStart, importKwEnd, absStar, isWildcard)); + + // For wildcard imports we register the package for resolution; final marking deferred + if (isWildcard) { + importedPackages.add(fullPath); + } else { + // Resolve the import to get the proper class name (so simple-name lookups can work now) + ClassPathFinder.ResolveResult result = classPathFinder.resolve(fullPath); + + // Extract simple name (last segment) + int lastDot = fullPath.lastIndexOf('.'); + String simpleName = lastDot >= 0 ? fullPath.substring(lastDot + 1) : fullPath; + + if (!simpleName.isEmpty()) { + if (result.found && result.classInfo != null) { + // Store with resolved name (has $ for inner classes) + importedClasses.put(simpleName, result.classInfo.resolvedName); + } else { + // Store original path for unresolved imports + importedClasses.put(simpleName, fullPath); + // Track this as an unresolved class + unresolvedClasses.add(simpleName); + } + } + // Cache the full ResolveResult on the ImportEntry for reuse in finalizeImports + ImportEntry last = importEntries.get(importEntries.size() - 1); + last.resolveResult = result; + } + } + } + + /** + * Parse and highlight generic type parameters using ClassPathFinder. + * Handles arbitrarily nested generics like "Map>>". + */ + private void highlightGenericTypes(String genericContent, int contentStart, List marks, List excluded) { + if (genericContent == null || genericContent.isEmpty()) return; + + List occurrences = classPathFinder.parseGenericTypes(genericContent, importedClasses, importedPackages); + + for (ClassPathFinder.TypeOccurrence occ : occurrences) { + int absStart = contentStart + occ.startOffset; + int absEnd = contentStart + occ.endOffset; + + if (!isInExcludedRange(absStart, excluded)) { + TokenType tokenType; + switch (occ.type) { + case INTERFACE: + tokenType = TokenType.INTERFACE_DECL; + break; + case ENUM: + tokenType = TokenType.ENUM_DECL; + break; + default: + tokenType = TokenType.IMPORTED_CLASS; + break; + } + marks.add(new Mark(absStart, absEnd, tokenType)); + } + } + } + + private void collectFields() { + globalFields.clear(); + localFields.clear(); + methodBlocks.clear(); + + // Extract method blocks first + methodBlocks = MethodBlock.collectMethodBlocks(text); + + // Get excluded ranges (strings and comments) for the entire text + List excludedRanges = MethodBlock.getExcludedRanges(text); + + // Global fields (excluding those inside methods, strings, and comments) + Matcher mGlobal = GLOBAL_FIELD_DECL.matcher(text); + while (mGlobal.find()) { + String varName = mGlobal.group(2); + int varPosition = mGlobal.start(2); + + // Skip if inside a string or comment + if (isInExcludedRange(varPosition, excludedRanges)) { + continue; + } + + // Check if this variable is inside a method + boolean isInsideMethod = false; + for (MethodBlock block : methodBlocks) { + if (block.containsPosition(varPosition)) { + isInsideMethod = true; + break; + } + } + + // Only add as global if it's not inside any method + if (!isInsideMethod) { + globalFields.add(varName); + } + } + + // Extract local variables from each method block + for (MethodBlock block : methodBlocks) { + // localVariables are already extracted in MethodBlock constructor + for (String var : block.localVariables) { + if (!globalFields.contains(var) && !localFields.contains(var)) { + localFields.add(var); + } + } + } + } + + // Find which method block contains a given position + private MethodBlock findMethodBlockAtPosition(int position) { + for (MethodBlock block : methodBlocks) { + if (block.containsPosition(position)) { + return block; + } + } + return null; + } + + private void highlightVariableReferences(List marks) { + // Known Java keywords and types that shouldn't be flagged as undefined + java.util.Set knownIdentifiers = new java.util.HashSet<>(java.util.Arrays.asList( + // Primitive types + "boolean", "int", "float", "double", "long", "char", "byte", "short", "void", + // Keywords + "null", "true", "false", "if", "else", "switch", "case", "for", "while", "do", + "try", "catch", "finally", "return", "throw", "var", "let", "const", "function", + "continue", "break", "this", "new", "typeof", "instanceof", "class", "interface", + "extends", "implements", "import", "package", "public", "private", "protected", + "static", "final", "abstract", "synchronized", "native", "default", "enum", + "throws", "super", "assert", "volatile", "transient", "strictfp", "goto", + // Common JS/scripting keywords + "undefined", "NaN", "Infinity", "arguments", "prototype", "constructor", + // Common types (first letter uppercase pattern handles most) + "String", "Object", "Array", "Math", "System", "Integer", "Double", "Float", + "Boolean", "Long", "Byte", "Short", "Character", "List", "Map", "Set" + )); + + // First pass: handle field accesses (this.field and obj.field patterns) + Pattern thisFieldPattern = Pattern.compile("\\bthis\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher thisFieldMatcher = thisFieldPattern.matcher(text); + while (thisFieldMatcher.find()) { + String fieldName = thisFieldMatcher.group(1); + int fieldPos = thisFieldMatcher.start(1); + // Highlight as GLOBAL_FIELD if it exists, UNDEFINED_VAR otherwise + if (globalFields.contains(fieldName)) { + marks.add(new Mark(fieldPos, thisFieldMatcher.end(1), TokenType.GLOBAL_FIELD)); + } else { + marks.add(new Mark(fieldPos, thisFieldMatcher.end(1), TokenType.UNDEFINED_VAR)); + } + } + + // Handle identifier.field patterns (e.g., obj.field, global.field) + // The field part should be highlighted as GLOBAL_FIELD (light blue) + // Also, detect chained accesses like `a.b.c` and treat subsequent segments + // as fields when the root `a` is a known variable (parameter/local/global/this). + // Collect function parameters from JS-style `function name(a, b)` declarations + // so script parameters (e.g., `function tick(e)`) are recognized as known roots. + java.util.Set scriptFunctionParams = new java.util.HashSet<>(); + Pattern funcPattern = Pattern.compile("\\bfunction\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s*\\(([^)]*)\\)"); + Matcher funcM = funcPattern.matcher(text); + while (funcM.find()) { + String inside = funcM.group(1); + if (inside != null && !inside.trim().isEmpty()) { + for (String p : inside.split(",")) { + String pn = p.trim(); + if (!pn.isEmpty()) scriptFunctionParams.add(pn); + } + } + } + + Pattern chainPattern = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*(?:\\s*\\.\\s*[a-zA-Z_][a-zA-Z0-9_]*)+)\\b"); + Matcher chainMatcher = chainPattern.matcher(text); + while (chainMatcher.find()) { + if (isInExcludedRange(chainMatcher.start(), MethodBlock.getExcludedRanges(text))) continue; + String chain = chainMatcher.group(1); + int absStart = chainMatcher.start(1); + + java.util.List segs = new java.util.ArrayList<>(); + java.util.List segPos = new java.util.ArrayList<>(); + Matcher segM = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*").matcher(chain); + while (segM.find()) { + segs.add(segM.group()); + segPos.add(absStart + segM.start()); + } + if (segs.size() < 2) continue; + + String root = segs.get(0); + if (Character.isUpperCase(root.charAt(0))) continue; + + boolean rootIsKnown = false; + if (globalFields.contains(root) || localFields.contains(root)) rootIsKnown = true; + MethodBlock mbForRoot = findMethodBlockAtPosition(segPos.get(0)); + if (mbForRoot != null && mbForRoot.parameters.contains(root)) rootIsKnown = true; + if (scriptFunctionParams.contains(root)) rootIsKnown = true; + if ("this".equals(root)) rootIsKnown = true; + + if (rootIsKnown) { + for (int si = 1; si < segs.size(); si++) { + int sAbs = segPos.get(si); + int sEnd = sAbs + segs.get(si).length(); + marks.add(new Mark(sAbs, sEnd, TokenType.GLOBAL_FIELD)); + } + } + } + + // Fallback: simple two-part identifier.field patterns + Pattern objFieldPattern = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher objFieldMatcher = objFieldPattern.matcher(text); + while (objFieldMatcher.find()) { + String fieldName = objFieldMatcher.group(2); + int fieldPos = objFieldMatcher.start(2); + String objName = objFieldMatcher.group(1); + if (!objName.equals("this")) { + marks.add(new Mark(fieldPos, objFieldMatcher.end(2), TokenType.GLOBAL_FIELD)); + } + } + + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); + Matcher m = identifier.matcher(text); + while (m.find()) { + String name = m.group(1); + int position = m.start(1); + + // Skip known keywords + if (knownIdentifiers.contains(name)) continue; + + // Skip field access (identifier preceded by a dot) - these should be gray/default + if (isFieldAccess(position)) continue; + + // Never treat parts of import/package statements as undefined variables + if (isInImportOrPackageStatement(position)) continue; + + // Check if inside a method + MethodBlock methodBlock = findMethodBlockAtPosition(position); + + if (methodBlock != null) { + // Inside a method - first check parameters, locals, and globals (respecting registrations) + if (methodBlock.parameters.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.PARAMETER)); + continue; + } + + if (methodBlock.isLocalDeclaredAtPosition(name, position) || localFields.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD)); + continue; + } + + if (globalFields.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + continue; + } + + // If it's an uppercase identifier that hasn't been registered as a local/param/global, + // treat it as a type reference (skip) rather than an undefined variable. + if (Character.isUpperCase(name.charAt(0))) continue; + + // Unknown variable - mark as undefined but only if it's not a method call or type reference + if (!isMethodCall(position) && !isTypeReference(name, position)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); + } + } else { + // Outside any method - check global fields first + if (globalFields.contains(name) || localFields.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + continue; + } + + // Uppercase unregistered identifiers are likely type names; skip them + if (Character.isUpperCase(name.charAt(0))) continue; + + if (!isMethodCall(position) && !isTypeReference(name, position)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); + } + } + } + } + + private boolean isInImportOrPackageStatement(int position) { + if (position < 0 || position >= text.length()) return false; + + int lineStart = text.lastIndexOf('\n', position); + lineStart = (lineStart < 0) ? 0 : lineStart + 1; + int lineEnd = text.indexOf('\n', position); + lineEnd = (lineEnd < 0) ? text.length() : lineEnd; + + // Quick scan of the line start for "import" or "package" + int i = lineStart; + while (i < lineEnd && Character.isWhitespace(text.charAt(i))) i++; + + if (i + 6 <= lineEnd && text.startsWith("import", i)) { + // Ensure word boundary + int after = i + 6; + return after == lineEnd || Character.isWhitespace(text.charAt(after)); + } + if (i + 7 <= lineEnd && text.startsWith("package", i)) { + int after = i + 7; + return after == lineEnd || Character.isWhitespace(text.charAt(after)); + } + return false; + } + + /** + * Check if the identifier at this position is a field access (preceded by a dot) + * e.g., in "obj.field", the "field" part is a field access + */ + private boolean isFieldAccess(int position) { + if (position <= 0) return false; + + // Look backwards from position, skipping any whitespace + int i = position - 1; + while (i >= 0 && Character.isWhitespace(text.charAt(i))) { + i--; + } + + // Check if preceded by a dot + return i >= 0 && text.charAt(i) == '.'; + } + + /** + * Check if the identifier at this position is a method call (followed by parenthesis) + */ + private boolean isMethodCall(int position) { + // Skip whitespace after identifier + int i = position; + // Note: parentheses needed to avoid evaluating text.charAt(i) when i >= text.length() + while (i < text.length() && (Character.isLetterOrDigit(text.charAt(i)) || text.charAt(i) == '_')) { + i++; + } + // Skip whitespace + while (i < text.length() && Character.isWhitespace(text.charAt(i))) { + i++; + } + // Check for opening paren + return i < text.length() && text.charAt(i) == '('; + } + + /** + * Check if this looks like a type reference (e.g., part of a declaration or generic) + * But NOT a comparison like "i < container" which uses < as less-than operator + */ + private boolean isTypeReference(String name, int position) { + // Check if preceded by 'new ' + if (position > 4) { + String before = text.substring(Math.max(0, position - 5), position); + if (before.endsWith("new ")) { + return true; + } + } + + // Check if preceded by < but make sure it's a generic, not a comparison + // Generic: Type 1) { + int checkPos = position - 1; + // Skip whitespace + while (checkPos > 0 && Character.isWhitespace(text.charAt(checkPos))) { + checkPos--; + } + if (checkPos >= 0 && text.charAt(checkPos) == '<') { + // Check what's before the < + int beforeLt = checkPos - 1; + while (beforeLt >= 0 && Character.isWhitespace(text.charAt(beforeLt))) { + beforeLt--; + } + if (beforeLt >= 0) { + char beforeChar = text.charAt(beforeLt); + // If it's a letter/digit/underscore (identifier) or ) followed by space + <, it's a comparison + // If it's a type name directly followed by <, it's a generic + // Check if there was whitespace between the identifier and < + boolean hasSpaceBeforeLt = (checkPos > 0 && Character.isWhitespace(text.charAt(checkPos - 1))); + if (hasSpaceBeforeLt && (Character.isLetterOrDigit(beforeChar) || beforeChar == '_' || beforeChar == ')')) { + // This is a comparison like "i < container" or "(x + 1) < y" + return false; + } + // Otherwise it's likely a generic like List + if (Character.isLetter(beforeChar) || beforeChar == '>') { + return true; + } + } + } + // Check for comma in generics like Map + if (checkPos >= 0 && text.charAt(checkPos) == ',') { + return true; + } + } + return false; + } + + /** + * Check if position is in an excluded range (string or comment) + */ + private boolean isInExcludedRange(int pos, List ranges) { + for (int[] range : ranges) { + if (pos >= range[0] && pos < range[1]) { + return true; + } + } + return false; + } + + private void finalizeImports(List marks) { + if (importEntries.isEmpty()) return; + + for (ImportEntry ie : importEntries) { + // Highlight the 'import' keyword + marks.add(new Mark(ie.importKwStart, ie.importKwEnd, TokenType.IMPORT_KEYWORD)); + + String fullPath = ie.fullPath; + int pathStart = ie.pathStart; + int pathEnd = ie.pathEnd; + String matchText = ie.matchText; + + if (!ie.isWildcard) { + // Non-wildcard: reuse cached resolve result if available, otherwise resolve now + ClassPathFinder.ResolveResult result = classPathFinder.resolve(fullPath); + + String orig = text.substring(pathStart, pathEnd); + List tokens = new ArrayList<>(); + List tokenStarts = new ArrayList<>(); + List tokenEnds = new ArrayList<>(); + Matcher idm = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*").matcher(orig); + while (idm.find()) { + tokens.add(idm.group()); + tokenStarts.add(idm.start()); + tokenEnds.add(idm.end()); + } + + int pkgSegments = result.packageSegmentCount; + boolean hasPackage = pkgSegments > 0; + + if (result.found && result.invalidStartOffset < 0) { + if (hasPackage && pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); + } + + if (result.classSegments != null && !result.classSegments.isEmpty()) { + int classTokenStart = hasPackage ? pkgSegments : 0; + for (int si = 0; si < result.classSegments.size(); si++) { + int tokIdx = classTokenStart + si; + if (tokIdx >= 0 && tokIdx < tokenStarts.size()) { + int s = pathStart + tokenStarts.get(tokIdx); + int e = pathStart + tokenEnds.get(tokIdx); + ClassPathFinder.ClassSegment seg = result.classSegments.get(si); + TokenType segType; + switch (seg.type) { + case INTERFACE: + segType = TokenType.INTERFACE_DECL; + break; + case ENUM: + segType = TokenType.ENUM_DECL; + break; + default: + segType = TokenType.IMPORTED_CLASS; + break; + } + marks.add(new Mark(s, e, segType)); + } + } + } + } else if (result.classSegments != null && !result.classSegments.isEmpty()) { + if (hasPackage && pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); + } + + int classTokenStart = hasPackage ? pkgSegments : 0; + for (int si = 0; si < result.classSegments.size(); si++) { + int tokIdx = classTokenStart + si; + if (tokIdx >= 0 && tokIdx < tokenStarts.size()) { + int s = pathStart + tokenStarts.get(tokIdx); + int e = pathStart + tokenEnds.get(tokIdx); + ClassPathFinder.ClassSegment seg = result.classSegments.get(si); + TokenType segType; + switch (seg.type) { + case INTERFACE: + segType = TokenType.INTERFACE_DECL; + break; + case ENUM: + segType = TokenType.ENUM_DECL; + break; + default: + segType = TokenType.IMPORTED_CLASS; + break; + } + marks.add(new Mark(s, e, segType)); + } + } + + if (result.invalidStartOffset >= 0) { + int invalidAbs = mapNormalizedOffsetToOriginal(orig, result.invalidStartOffset, pathStart); + if (invalidAbs < pathEnd) { + marks.add(new Mark(invalidAbs, pathEnd, TokenType.UNDEFINED_VAR)); + } + } + } else { + if (result.invalidStartOffset >= 0 && hasPackage) { + if (pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); + } + + int invalidAbs = mapNormalizedOffsetToOriginal(orig, result.invalidStartOffset, pathStart); + if (invalidAbs < pathEnd) { + marks.add(new Mark(invalidAbs, pathEnd, TokenType.UNDEFINED_VAR)); + } + } else if (fullPath.endsWith(".") && hasPackage && result.invalidStartOffset < 0) { + marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + } else if (result.invalidStartOffset < 0 && hasPackage) { + marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + } else { + marks.add(new Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR)); + } + } + + // Highlight .* if present (shouldn't for non-wildcard, but keep behavior) + int starIndex = matchText.indexOf('*'); + if (starIndex != -1) { + int absStar = ie.importKwStart + (starIndex - 0); + marks.add(new Mark(absStar, absStar + 1, TokenType.DEFAULT)); + } + } else { + // Wildcard import: try to determine whether this is a valid package or + // a class whose inner types are being imported (Outer.*) + boolean marked = false; + + // If package is known valid, mark as package + if (classPathFinder.isValidPackage(fullPath)) { + marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + marked = true; + } else { + // Probe uppercase identifiers in file to see if any inner types resolve + java.util.Set uppercaseIds = new java.util.HashSet<>(); + Matcher idm = Pattern.compile("\\b([A-Z][a-zA-Z0-9_]*)\\b").matcher(text); + while (idm.find()) uppercaseIds.add(idm.group(1)); + + for (String ident : uppercaseIds) { + String candInner = fullPath + "$" + ident; + ClassPathFinder.ClassInfo infoInner = classPathFinder.tryResolveClassName(candInner); + if (infoInner != null) { + // outer may be a class - try to get its info + ClassPathFinder.ClassInfo outerInfo = classPathFinder.tryResolveClassName(fullPath); + // mark package tokens (all but last) as TYPE_DECL and last as outer type + String orig = text.substring(pathStart, pathEnd); + List tokens = new ArrayList<>(); + List tokenStarts = new ArrayList<>(); + List tokenEnds = new ArrayList<>(); + Matcher tokm = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*").matcher(orig); + while (tokm.find()) { + tokens.add(tokm.group()); + tokenStarts.add(tokm.start()); + tokenEnds.add(tokm.end()); + } + + int tokenCount = tokenStarts.size(); + if (tokenCount > 0) { + int pkgEnd = pathStart + (tokenEnds.get(Math.max(0, tokenCount - 2))); + if (tokenCount >= 2) { + // mark package (all except last) + marks.add(new Mark(pathStart, pathStart + tokenEnds.get(tokenCount - 2), TokenType.TYPE_DECL)); + } + // mark outer class token (last) + int s = pathStart + tokenStarts.get(tokenCount - 1); + int e = pathStart + tokenEnds.get(tokenCount - 1); + TokenType segType = TokenType.IMPORTED_CLASS; + if (outerInfo != null) { + switch (outerInfo.type) { + case INTERFACE: + segType = TokenType.INTERFACE_DECL; + break; + case ENUM: + segType = TokenType.ENUM_DECL; + break; + default: + segType = TokenType.IMPORTED_CLASS; + break; + } + } + marks.add(new Mark(s, e, segType)); + } + marked = true; + // ensure package listed in importedPackages + importedPackages.add(fullPath); + break; + } + + // Try package-like resolution: fullPath + "." + ident + String candDot = fullPath + "." + ident; + if (classPathFinder.tryResolveClassName(candDot) != null) { + marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + marked = true; + importedPackages.add(fullPath); + break; + } + } + } + + if (!marked) { + // Unknown wildcard import - mark as undefined + marks.add(new Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR)); + } + + // highlight star + if (ie.starPos >= 0) marks.add(new Mark(ie.starPos, ie.starPos + 1, TokenType.DEFAULT)); + } + } + } + + + private void collectImportedClassUsages(List marks) { + List excluded = MethodBlock.getExcludedRanges(text); + // Find identifiers followed by dot (e.g., Math.min, ArrayList.class) + Pattern classUsage = Pattern.compile("\\b([A-Z][a-zA-Z0-9_]*)\\s*\\."); + Matcher m = classUsage.matcher(text); + while (m.find()) { + String className = m.group(1); + int start = m.start(1); + int end = m.end(1); + if (isInExcludedRange(start, excluded)) + continue; + + // Check if this class was imported but failed to resolve + if (unresolvedClasses.contains(className)) { + marks.add(new Mark(start, end, TokenType.UNDEFINED_VAR)); + continue; + } + + // Check if this is an imported class, wildcard match, or java.lang class + boolean isImported = importedClasses.containsKey(className) || ClassPathFinder.JAVA_LANG_CLASSES.contains(className); + if (!isImported && !importedPackages.isEmpty()) { + isImported = true; // Wildcard import present + } + + // Also skip if it's a local/global field or parameter (avoid false positives) + MethodBlock block = findMethodBlockAtPosition(start); + boolean isShadowed = globalFields.contains(className) || localFields.contains(className); + if (block != null) { + isShadowed = isShadowed || block.parameters.contains(className) || block.isLocalDeclaredAtPosition( + className, start); + } + + if (isImported && !isShadowed) { + // Resolve the class to determine its type + ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(className, importedClasses, importedPackages); + TokenType tokenType = TokenType.IMPORTED_CLASS; + if (info != null) { + switch (info.type) { + case INTERFACE: + tokenType = TokenType.INTERFACE_DECL; + break; + case ENUM: + tokenType = TokenType.ENUM_DECL; + break; + } + } + marks.add(new Mark(start, end, tokenType)); + } else if (!isImported && !isShadowed) { + marks.add(new Mark(start, end, TokenType.UNDEFINED_VAR)); + } + } + } + + private void collectClassDeclarations(List marks) { + Matcher m = CLASS_DECL.matcher(text); + List excluded = MethodBlock.getExcludedRanges(text); + while (m.find()) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.CLASS_KEYWORD)); + + int nameStart = m.start(2); + int nameEnd = m.end(2); + if (isInExcludedRange(nameStart, excluded)) + continue; + String kind = m.group(1); + if ("interface".equals(kind)) { + marks.add(new Mark(nameStart, nameEnd, TokenType.INTERFACE_DECL)); + } else if ("enum".equals(kind)) { + marks.add(new Mark(nameStart, nameEnd, TokenType.ENUM_DECL)); + } else { + marks.add(new Mark(nameStart, nameEnd, TokenType.CLASS_DECL)); + } + } + } + + public void formatCodeText() { + // Step 1: Tokenize the full text + List marks = new ArrayList<>(); + + collectImports(); // Parse import statements first + collectFields(); // Extract global and local fields with method scoping + collectPatternMatches(marks, COMMENT, TokenType.COMMENT); + collectPatternMatches(marks, STRING, TokenType.STRING); + + // Import marking is deferred until after type/usages are discovered. + // finalizeImports(marks) will create import marks later. + collectClassDeclarations(marks); // Highlight class/interface/enum declarations + + // Highlight general keywords + collectPatternMatches(marks, KEYWORD, TokenType.KEYWORD); + collectPatternMatches(marks, MODIFIER, TokenType.MODIFIER); + + collectTypeDeclarations(marks); + collectPatternMatches(marks, NEW_TYPE, TokenType.NEW_TYPE, 1); + + collectMethodDeclarations(marks); + collectPatternMatches(marks, METHOD_CALL, TokenType.METHOD_CALL, 1); + + collectPatternMatches(marks, NUMBER, TokenType.NUMBER); + + highlightVariableReferences(marks); + collectImportedClassUsages(marks); // Highlight imported class usages (e.g., Math.min) + + // Finalize import markings after usages/types have been discovered + finalizeImports(marks); + + marks = resolveConflicts(marks); + + // Compute indent guides based on matched braces, ignoring strings/comments + computeIndentGuides(marks); + + // Step 2: Clear existing tokens + for (LineData line : lines) { + line.tokens.clear(); + } + + // Step 3: Assign tokens to the correct lines + for (LineData line : lines) { + int cursor = line.start; + // collectImportStatements used to create marks here; we now defer marking until types/usages are discovered + // so finalizeImports will create the visual marks from recorded import entries. + for (Mark mark : marks) { + if (mark.end <= line.start || mark.start >= line.end) + continue; + + int tokenStart = Math.max(mark.start, line.start); + int tokenEnd = Math.min(mark.end, line.end); + + tokenStart = Math.max(0, Math.min(tokenStart, text.length())); + tokenEnd = Math.max(0, Math.min(tokenEnd, text.length())); + + // plain text before token + if (cursor < tokenStart) { + int end = Math.min(tokenStart, text.length()); + line.tokens.add( + new Token(text.substring(cursor, end), TokenType.DEFAULT, cursor, end)); + } + + // token text + if (tokenStart < tokenEnd) { + line.tokens.add(new Token(text.substring(tokenStart, Math.min(tokenEnd, text.length())), mark.type, tokenStart, Math.min(tokenEnd, text.length()))); + } + + cursor = tokenEnd; + } + + // trailing plain text + if (cursor < line.end) { + int end = Math.min(line.end, text.length()); + line.tokens.add(new Token(text.substring(cursor, end), TokenType.DEFAULT, cursor, end)); + } + } + } + + private void collectMethodDeclarations(List marks) { + Matcher m = METHOD_DECL.matcher(text); + while (m.find()) { // method name + marks.add(new Mark(m.start(2), m.end(2), TokenType.METHOD_DECARE)); + // return type + marks.add(new Mark(m.start(1), m.end(1), TokenType.TYPE_DECL)); + } + } + + private void collectTypeDeclarations(List marks) { + List excluded = MethodBlock.getExcludedRanges(text); + + // Pattern to find type declarations: Type<...> variableName or Type variableName + // We'll manually parse the generic portion to handle arbitrary nesting + Pattern typeStart = Pattern.compile( + "(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + + "([A-Z][a-zA-Z0-9_]*)\\s*" // Group 1 → type name (must start with uppercase) + ); + + for (LineData ld : lines) { + int lineStart = ld.start; + int lineEnd = ld.end; + if (lineStart >= lineEnd) continue; + String s = ld.text; + + Matcher m = typeStart.matcher(s); + int searchFrom = 0; + + while (m.find(searchFrom)) { + int typeNameStart = lineStart + m.start(1); + int typeNameEnd = lineStart + m.end(1); + + // Skip if in excluded range + boolean skip = false; + for (int[] r : excluded) { + if (typeNameStart < r[1] && typeNameEnd > r[0]) { + skip = true; + break; + } + } + if (skip) { + searchFrom = m.end(); + continue; + } + + String typeName = m.group(1); + int posAfterType = m.end(1); + + // Skip whitespace after type name + while (posAfterType < s.length() && Character.isWhitespace(s.charAt(posAfterType))) { + posAfterType++; + } + + // Check for generic parameters + String genericContent = null; + int genericStart = -1; + int genericEnd = -1; + + if (posAfterType < s.length() && s.charAt(posAfterType) == '<') { + genericStart = posAfterType; + int depth = 1; + int i = posAfterType + 1; + while (i < s.length() && depth > 0) { + char c = s.charAt(i); + if (c == '<') depth++; + else if (c == '>') depth--; + i++; + } + if (depth == 0) { + genericEnd = i; + genericContent = s.substring(genericStart + 1, genericEnd - 1); + posAfterType = genericEnd; + } + } + + // Skip whitespace after generics + while (posAfterType < s.length() && Character.isWhitespace(s.charAt(posAfterType))) { + posAfterType++; + } + + // Check if this looks like a type declaration: + // 1. Has generic content (e.g., List) - always a type + // 2. Followed by a variable name (lowercase start or underscore) + // 3. At end of line with generic content (line might be wrapped) + boolean hasGeneric = genericContent != null && !genericContent.isEmpty(); + boolean atEndOfLine = posAfterType >= s.length(); + boolean followedByVarName = false; + + if (!atEndOfLine) { + char nextChar = s.charAt(posAfterType); + followedByVarName = Character.isLetter(nextChar) || nextChar == '_'; + } + + // Accept as type if: has generics OR followed by variable name + // This handles both `List myList` and wrapped lines like `List` + if (hasGeneric || followedByVarName) { + // This looks like a type declaration + + // Resolve the main type + ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(typeName, importedClasses, importedPackages); + TokenType tokenType = TokenType.UNDEFINED_VAR; + if (info != null) { + switch (info.type) { + case INTERFACE: + tokenType = TokenType.INTERFACE_DECL; + break; + case ENUM: + tokenType = TokenType.ENUM_DECL; + break; + default: + tokenType = TokenType.TYPE_DECL; + } + } + marks.add(new Mark(typeNameStart, typeNameEnd, tokenType)); + + // Handle generic content + if (hasGeneric && genericStart >= 0) { + int absGenericStart = lineStart + genericStart; + int absGenericEnd = lineStart + genericEnd; + + // Mark < and > as default + marks.add(new Mark(absGenericStart, absGenericStart + 1, TokenType.DEFAULT)); + marks.add(new Mark(absGenericEnd - 1, absGenericEnd, TokenType.DEFAULT)); + + // Parse and highlight all types in generic content + int contentStart = lineStart + genericStart + 1; + highlightGenericTypes(genericContent, contentStart, marks, excluded); + } + + // If followed by a variable name, detect it and register uppercase-starting + // variable names as locals so subsequent identifier scanning treats them + // as variables (not types). This handles declarations like: + // MyType Capital = new MyType(); + if (followedByVarName) { + int v = posAfterType; + // skip whitespace + while (v < s.length() && Character.isWhitespace(s.charAt(v))) v++; + int varStart = v; + while (v < s.length() && (Character.isLetterOrDigit(s.charAt(v)) || s.charAt(v) == '_')) v++; + int varEnd = v; + if (varEnd > varStart) { + String varName = s.substring(varStart, varEnd); + int absVarStart = lineStart + varStart; + int absVarEnd = lineStart + varEnd; + if (Character.isUpperCase(varName.charAt(0))) { + MethodBlock mb = findMethodBlockAtPosition(absVarStart); + if (mb != null) { + if (!mb.localVariables.contains(varName)) mb.localVariables.add(varName); + if (!localFields.contains(varName)) localFields.add(varName); + marks.add(new Mark(absVarStart, absVarEnd, TokenType.LOCAL_FIELD)); + } + } + } + } + } + + searchFrom = m.end(); + } + } + } + + private void collectPatternMatches(List marks, Pattern pattern, TokenType type, int group) { + Matcher m = pattern.matcher(text); + while (m.find()) { + marks.add(new Mark(m.start(group), m.end(group), type)); + } + } + + private void collectPatternMatches(List marks, Pattern pattern, TokenType type) { + collectPatternMatches(marks, pattern, type, 0); + } + + private List resolveConflicts(List marks) { + // Sort by start index first, then by descending priority so higher-priority marks + // at the same start are considered first. + marks.sort((a, b) -> { + if (a.start != b.start) + return Integer.compare(a.start, b.start); + return Integer.compare(b.type.priority, a.type.priority); + }); + + List result = new ArrayList<>(); + for (Mark m : marks) { + boolean skip = false; + // Remove any existing lower-priority marks that overlap with this higher-priority mark + for (int i = result.size() - 1; i >= 0; i--) { + Mark r = result.get(i); + if (m.start < r.end && m.end > r.start) { + if (r.type.priority < m.type.priority) { + // remove lower-priority overlapping mark + result.remove(i); + } else { + // an existing mark has equal or higher priority and overlaps -> skip adding m + skip = true; + break; + } + } + } + if (!skip) { + result.add(m); + } + } + // Optionally sort the result by start before returning + result.sort((a, b) -> Integer.compare(a.start, b.start)); + return result; + } + + public static class LineData { + public String text; + public int start, end; + public List tokens = new ArrayList<>(); + public List indentCols = new ArrayList<>(); + + public LineData(String text, int startIndex, int end) { + this.text = text; + this.start = startIndex; + this.end = end; + } + + public void drawString(int x, int y, int color) { + StringBuilder builder = new StringBuilder(); + int lastIndex = 0; + + for (Token t : tokens) { + int tokenStart = t.start - this.start; // relative position in line + if (tokenStart > lastIndex) { + // append the text before the token (spaces, punctuation) + builder.append(text, lastIndex, tokenStart); + } + builder.append(colorChar).append(t.type.color).append(t.text).append(colorChar).append('f'); + lastIndex = tokenStart + t.text.length(); + } + + // append any remaining text after the last token + if (lastIndex < text.length()) { + builder.append(text.substring(lastIndex)); + } + + ClientProxy.Font.drawString(builder.toString(), x, y, color); + } + } + + public static class Token { + public String text; + public TokenType type; + public int start, end; // start/end relative to the line + + public Token(String text, TokenType type, int start, int end) { + this.text = text; + this.type = type; + this.start = start; + this.end = end; + } + + @Override + public String toString() { + return "Token{'" + text + '\'' + ", " + type + ", (color=" + type.color + ", priority=" + type.priority + ")" + '}'; + } + } + + private static class Mark { + public int start, end; + public TokenType type; + + public Mark(int start, int end, TokenType type) { + this.start = start; + this.end = end; + this.type = type; + } + + @Override + public String toString() { + return "Mark{" + type + ", (start=" + start + ", end=" + end + ")" + '}'; + } + } + + public enum TokenType { + COMMENT('7', 130), + STRING('5', 120), + CLASS_KEYWORD('c', 115), + IMPORT_KEYWORD('6', 110), + KEYWORD('c', 100), + MODIFIER('6', 90), + NEW_TYPE('d', 80), + IMPORTED_CLASS('3', 75), //imported classes used in code + INTERFACE_DECL('b', 85), + ENUM_DECL('d', 85), + CLASS_DECL('3', 85), + TYPE_DECL('3', 70), + METHOD_DECARE('2', 60), + METHOD_CALL('a', 50), + NUMBER('7', 40), + VARIABLE('f', 30), + GLOBAL_FIELD('b', 35), // aqua - class-level fields + LOCAL_FIELD('e', 25), // yellow - local variables + PARAMETER('9', 36), // blue - method parameters (Bug 10) + UNDEFINED_VAR('4', 105), // dark red - undefined variables (higher priority so it isn't overridden) + DEFAULT('f', 0); + + public final char color; + public final int priority; + + TokenType(char color, int priority) { + this.color = color; + this.priority = priority; + } + } + + // Compute indent guide columns per line based on brace matching. + private void computeIndentGuides(List marks) { + // Clear existing guides + for (LineData ld : lines) { + ld.indentCols.clear(); + } + + // Build ignored ranges from STRING and COMMENT tokens + List ignored = new ArrayList<>(); + for (Mark m : marks) { + if (m.type == TokenType.STRING || m.type == TokenType.COMMENT) { + ignored.add(new int[]{m.start, m.end}); + } + } + + java.util.function.Predicate isIgnored = (pos) -> { + for (int[] r : ignored) { + if (pos >= r[0] && pos < r[1]) + return true; + } + return false; + }; + + class OpenBrace { + int line, col; + + OpenBrace(int l, int c) { + line = l; + col = c; + } + } + java.util.Deque stack = new java.util.ArrayDeque<>(); + + final int tabSize = 4; + + for (int li = 0; li < lines.size(); li++) { + LineData ld = lines.get(li); + String s = ld.text; + for (int i = 0; i < s.length(); i++) { + int absPos = ld.start + i; + if (isIgnored.test(absPos)) + continue; + char c = s.charAt(i); + if (c == '{') { + // compute leading indentation (expanded) up to this char + int leading = 0; + for (int k = 0; k < i; k++) { + char ch = s.charAt(k); + if (ch == '\t') + leading += tabSize; + else + leading += 1; + } + stack.push(new OpenBrace(li, leading)); + } else if (c == '}') { + if (!stack.isEmpty()) { + OpenBrace open = stack.pop(); + int startLine = open.line; + int col = open.col; + if (startLine == li) + continue; // same-line block, skip + // add guide column to lines inside the block + int from = Math.max(0, startLine + 1); + int to = Math.min(lines.size() - 1, li); + for (int l = from; l <= to; l++) { + List list = lines.get(l).indentCols; + if (!list.contains(col)) + list.add(col); + } + } + } + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java new file mode 100644 index 000000000..6158c85ed --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java @@ -0,0 +1,230 @@ +package noppes.npcs.client.gui.util.script; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MethodBlock { + + // pattern matches returnType + methodName + parentheses + public static final Pattern METHOD_PATTERN = Pattern.compile( + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name + ); + + // Pattern for variable declarations (same as in JavaTextContainer) + public static final Pattern LOCAL_VAR_DECL = Pattern.compile( + "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + + // Pattern for method parameters: Type name (with optional ... for varargs) + public static final Pattern PARAMETER_PATTERN = Pattern.compile( + "([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*(?:\\.{3})?)\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); + + public int startOffset; + public int endOffset; + public String text; + public List localVariables = new ArrayList<>(); + public List parameters = new ArrayList<>(); // Method parameters + public Map localVarPositions = new HashMap<>(); // Track declaration positions + + public MethodBlock(int start, int end, String text) { + this.startOffset = start; + this.endOffset = end; + this.text = text; + extractParameters(); + extractLocalVariables(); + } + + // Extract method parameters from the method signature + private void extractParameters() { + parameters.clear(); + + // Find the parameter list between ( and ) + int parenStart = text.indexOf('('); + int parenEnd = text.indexOf(')'); + + if (parenStart < 0 || parenEnd <= parenStart) return; + + String paramList = text.substring(parenStart + 1, parenEnd); + + // Skip if empty + if (paramList.trim().isEmpty()) return; + + // Split by comma (but be careful of generic types like List) + Matcher m = PARAMETER_PATTERN.matcher(paramList); + while (m.find()) { + String paramName = m.group(2); + if (!parameters.contains(paramName)) { + parameters.add(paramName); + } + } + } + + // Extract local variables declared within this method + private void extractLocalVariables() { + localVariables.clear(); + localVarPositions.clear(); + + // Get excluded ranges (strings and comments) for this method's text + List excludedRanges = getExcludedRanges(text); + + Matcher m = LOCAL_VAR_DECL.matcher(text); + while (m.find()) { + String varName = m.group(2); + int declPosition = m.start(); + + // Skip if inside a string or comment + if (isInExcludedRange(declPosition, excludedRanges)) { + continue; + } + + // Check if this is a "this.field" assignment - if so, skip it + // Look backwards from the declaration position to see if preceded by "this." + boolean isThisFieldAssignment = false; + if (declPosition >= 5) { + int checkStart = Math.max(0, declPosition - 10); + String before = text.substring(checkStart, declPosition); + if (before.matches(".*\\bthis\\s*\\.\\s*$")) { + isThisFieldAssignment = true; + } + } + + if (!isThisFieldAssignment && !localVariables.contains(varName)) { + localVariables.add(varName); + // Store the absolute position in the full text (startOffset + relative position) + localVarPositions.put(varName, startOffset + declPosition); + } + } + } + + // Check if a reference position comes before the local variable declaration + // This handles cases like: global.field on line 7, then var global = "x" on line 9 + // At line 7, the local 'global' doesn't exist yet, so it should refer to the global 'global' + public boolean isLocalDeclaredAtPosition(String varName, int absolutePosition) { + if (!localVariables.contains(varName)) { + return false; + } + Integer declPos = localVarPositions.get(varName); + if (declPos == null) { + return false; + } + // The local variable is only "in scope" at or after its declaration position + return absolutePosition >= declPos; + } + + // Check if a position (in the full text) falls within this method + public boolean containsPosition(int position) { + return position >= startOffset && position < endOffset; + } + + public String toString() { + return text; + } + + public static List collectMethodBlocks(String text) { + List methods = new ArrayList<>(); + + // Matches: returnType methodName(...) { (non-greedy capture until first { ) + Pattern methodPattern = Pattern.compile( + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\([^)]*\\)\\s*" + // method name + parameters + "\\{", // opening brace + Pattern.MULTILINE); + + Matcher m = methodPattern.matcher(text); + while (m.find()) { + int start = m.start(); // start of method declaration + int braceStart = text.indexOf("{", m.end() - 1); // first { after declaration + if (braceStart == -1) + continue; + + int braceEnd = findMatchingBrace(text, braceStart); + if (braceEnd == -1) + continue; + + String methodText = text.substring(start, braceEnd + 1); + methods.add(new MethodBlock(start, braceEnd + 1, methodText)); + } + + return methods; + } + + // Find ranges of text to skip (strings and comments). Returns merged, sorted ranges. + public static List getExcludedRanges(String text) { + List ranges = new ArrayList<>(); + + // Find all strings and char literals (simple heuristic) + Pattern stringPattern = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); + Matcher stringMatcher = stringPattern.matcher(text); + while (stringMatcher.find()) { + ranges.add(new int[]{stringMatcher.start(), stringMatcher.end()}); + } + + // Find all comments (block and line comments) + Pattern commentPattern = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*"); + Matcher commentMatcher = commentPattern.matcher(text); + while (commentMatcher.find()) { + ranges.add(new int[]{commentMatcher.start(), commentMatcher.end()}); + } + + if (ranges.isEmpty()) + return ranges; + + // Sort ranges by start position + ranges.sort((a, b) -> Integer.compare(a[0], b[0])); + + // Merge overlapping/adjacent ranges for faster scanning + List merged = new ArrayList<>(); + int[] current = ranges.get(0); + for (int i = 1; i < ranges.size(); i++) { + int[] next = ranges.get(i); + if (next[0] <= current[1]) { + // overlap or adjacent + current[1] = Math.max(current[1], next[1]); + } else { + merged.add(current); + current = next; + } + } + merged.add(current); + return merged; + } + + // Check if a position is within an excluded range + private static boolean isInExcludedRange(int pos, List ranges) { + for (int[] range : ranges) { + if (pos >= range[0] && pos < range[1]) { + return true; + } + } + return false; + } + + // Utility to find the matching closing brace, ignoring braces in strings and + // comments + public static int findMatchingBrace(String text, int openBraceIndex) { + List excludedRanges = getExcludedRanges(text); + + int depth = 0; + for (int i = openBraceIndex; i < text.length(); i++) { + // Skip characters inside strings or comments + if (isInExcludedRange(i, excludedRanges)) { + continue; + } + + char c = text.charAt(i); + if (c == '{') { + depth++; + } else if (c == '}') { + depth--; + if (depth == 0) { + return i; + } + } + } + return -1; // no match + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java new file mode 100644 index 000000000..5dd70e7ce --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java @@ -0,0 +1,199 @@ +package noppes.npcs.client.gui.util.script; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * PackageFinder builds an index of package names found on the provided ClassLoader + * (scanning jar files and directories). After construction queries via {@link #contains} + * are O(1) HashSet lookups. + */ +public class PackageFinder { + private static volatile PackageFinder INSTANCE = null; + private final Set packages = new HashSet<>(); + private final ClassLoader classLoader; + + private PackageFinder(ClassLoader cl) throws IOException { + this.classLoader = cl == null ? Thread.currentThread().getContextClassLoader() : cl; + buildIndex(); + } + + private void buildIndex() throws IOException { + packages.clear(); + + ClassLoader cl = this.classLoader; + + // Try URLClassLoader first (common on most JVM versions used here) + if (cl instanceof URLClassLoader) { + URL[] urls = ((URLClassLoader) cl).getURLs(); + for (URL url : urls) { + try { + String protocol = url.getProtocol(); + if ("file".equals(protocol)) { + File f; + try { + URI u = url.toURI(); + f = new File(u); + } catch (Exception ex) { + f = new File(url.getPath()); + } + if (f.exists()) { + if (f.isDirectory()) + scanDirectory(f); + else if (f.isFile() && f.getName().endsWith(".jar")) + scanJar(f); + } + } else if ("jar".equals(protocol)) { + // jar:file:/path/to/jar.jar!/... + String s = url.getPath(); + int excl = s.indexOf('!'); + String jarPath = (excl >= 0) ? s.substring(0, excl) : s; + if (jarPath.startsWith("file:")) + jarPath = jarPath.substring(5); + File jf = new File(jarPath); + if (jf.exists()) + scanJar(jf); + } + } catch (IOException ignored) { + // ignore individual entries + } + } + } else { + // Fallback: scan system classpath as a best-effort + String cp = System.getProperty("java.class.path", ""); + String[] entries = cp.split(File.pathSeparator); + for (String e : entries) { + if (e == null || e.isEmpty()) + continue; + File f = new File(e); + if (!f.exists()) + continue; + if (f.isDirectory()) + scanDirectory(f); + else if (f.isFile() && f.getName().endsWith(".jar")) + scanJar(f); + } + } + } + + private void scanJar(File jarFile) throws IOException { + try (JarFile jf = new JarFile(jarFile)) { + Enumeration en = jf.entries(); + while (en.hasMoreElements()) { + JarEntry je = en.nextElement(); + String name = je.getName(); + if (name.endsWith(".class")) { + int idx = name.lastIndexOf('/'); + if (idx > 0) { + String pkg = name.substring(0, idx).replace('/', '.'); + addPackageHierarchy(pkg); + } + } + } + } + } + + private void scanDirectory(File dir) throws IOException { + final Path root = dir.toPath(); + if (!Files.exists(root)) + return; + Files.walk(root).forEach(p -> { + try { + if (Files.isRegularFile(p) && p.toString().endsWith(".class")) { + Path parent = p.getParent(); + if (parent == null) + return; + Path rel = root.relativize(parent); + String rp = rel.toString(); + if (rp.isEmpty()) + return; + // Normalize separators to dot + String pkg = rp.replace(File.separatorChar, '.'); + addPackageHierarchy(pkg); + } + } catch (Exception ignored) { + } + }); + } + + private void addPackageHierarchy(String pkg) { + if (pkg == null || pkg.isEmpty()) + return; + String cur = pkg; + while (true) { + if (!cur.isEmpty()) + packages.add(cur); + int idx = cur.lastIndexOf('.'); + if (idx < 0) + break; + cur = cur.substring(0, idx); + } + } + + public boolean contains(String pkg) { + if (pkg == null || pkg.isEmpty()) + return false; + return packages.contains(pkg); + } + + public static boolean find(String pkg) { + PackageFinder pf = getInstance(); + if (pf != null) + return pf.contains(pkg); + return false; + } + + /** + * Initialize the global singleton with the provided ClassLoader (or context CL if null). + * This is idempotent; calling again will re-create the index. + */ + public static synchronized void init(ClassLoader cl) throws IOException { + if (INSTANCE == null) { + INSTANCE = new PackageFinder(cl); + } else { + // rebuild index with new classloader if provided + if (cl != null) { + INSTANCE = new PackageFinder(cl); + } else { + INSTANCE.buildIndex(); + } + } + } + + /** + * Get the initialized singleton. Call {@link #init(ClassLoader)} first. + */ + public static PackageFinder getInstance() { + if (INSTANCE == null) { + try { + INSTANCE = fromContext(); + } catch (IOException e) { + } + } + return INSTANCE; + } + + /** + * Convenience factory using the context classloader and building a standalone finder (not touching singleton). + */ + public static PackageFinder fromContext() throws IOException { + return new PackageFinder(Thread.currentThread().getContextClassLoader()); + } + + /** + * Reload the index using the configured ClassLoader. + */ + public synchronized void reload() throws IOException { + buildIndex(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java new file mode 100644 index 000000000..81c346f8f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -0,0 +1,972 @@ +package noppes.npcs.client.gui.util.script; + +import net.minecraft.client.gui.GuiScreen; +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import org.lwjgl.input.Keyboard; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Handles IntelliJ-like rename refactoring with scope-aware renaming. + *

+ * Features: + * - Scope detection: local variables rename only within their block, global fields rename everywhere + * - Visual feedback: primary occurrence has white border, others just highlighted + * - ESC cancels, Enter confirms + * - Live preview as you type the new name + * - Full word selection by default when starting rename + * - Allows empty word during editing (shows empty box) + * - Click support within rename box + */ +public class RenameRefactorHandler { + + // ==================== STATE ==================== + private boolean active = false; + private String initialWord = ""; // The word when rename started (for undo grouping) + private String originalWord = ""; // Current baseline for live rename + private String currentWord = ""; // Current word being typed + private int primaryOccurrenceStart = -1; + private int primaryOccurrenceEnd = -1; + private List allOccurrences = new ArrayList<>(); + private ScopeInfo scope = null; + + // For global scope, track positions that have local shadowing (to exclude them) + private List localShadowedPositions = new ArrayList<>(); + + // For restoring original text on cancel + private String originalText = ""; + private int originalCursorPos = 0; + + // Callback interface + private RenameCallback callback; + + // Pattern for identifiers + private static final Pattern IDENTIFIER = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); + // Placeholder to show when the rename box is empty + private static final String EMPTY_PLACEHOLDER = "|"; + + /** + * Callback interface for rename operations + */ + public interface RenameCallback { + String getText(); + + void setText(String text); + + void setTextWithoutUndo(String text); // Set text without creating undo entry + + void pushUndoState(String text, int cursor); // Push an undo state + + List getLines(); + + int getCursorPosition(); + + SelectionState getSelectionState(); + + void setCursorPosition(int pos); + + void unfocusMainEditor(); + + void focusMainEditor(); + + int getGutterWidth(); + + int getLineHeight(); + + int getScrolledLine(); + + double getFractionalOffset(); + + void scrollToPosition(int pos); + + JavaTextContainer getContainer(); + + int getViewportWidth(); // For determining if clicks are in rename box + } + + /** + * Scope information for a variable + */ + public static class ScopeInfo { + public final int startOffset; + public final int endOffset; + public final boolean isGlobal; + public final String scopeType; // "global", "method", "block" + + public ScopeInfo(int start, int end, boolean isGlobal, String type) { + this.startOffset = start; + this.endOffset = end; + this.isGlobal = isGlobal; + this.scopeType = type; + } + + public boolean containsPosition(int pos) { + return pos >= startOffset && pos < endOffset; + } + } + + public void setCallback(RenameCallback callback) { + this.callback = callback; + } + + /** + * Start rename refactoring at the current cursor position. + * Selects the word under cursor and finds all occurrences within scope. + * + * @return true if rename mode was successfully started + */ + public boolean startRename() { + if (callback == null) + return false; + + String text = callback.getText(); + int cursor = callback.getCursorPosition(); + + if (text == null || text.isEmpty()) + return false; + + // Find word under cursor + int[] wordBounds = findWordAtPosition(text, cursor); + if (wordBounds == null) + return false; + + // Store original state for cancel AND for undo grouping + originalText = text; + originalCursorPos = cursor; + + initialWord = text.substring(wordBounds[0], wordBounds[1]); // Remember for undo + originalWord = initialWord; + currentWord = initialWord; + + // Determine scope for this identifier - MUST check if local shadows global + scope = determineScope(text, wordBounds[0], originalWord, callback.getContainer()); + + // If global scope, find all positions where local variables shadow this name + localShadowedPositions.clear(); + if (scope != null && scope.isGlobal) { + findLocalShadowedPositions(text, originalWord); + } + + // Find all occurrences within scope + findOccurrences(text, originalWord); + + if (allOccurrences.isEmpty()) + return false; + + active = true; + + // Set selection to full word (uses GuiScriptTextArea's selection) + primaryOccurrenceStart = wordBounds[0]; + primaryOccurrenceEnd = wordBounds[1]; + callback.getSelectionState().setSelection(wordBounds[0], wordBounds[1]); + // callback.setCursorPosition(wordBounds[1]); + callback.getSelectionState().markActivity(); + + return true; + } + + /** + * Cancel rename operation and restore original state + */ + public void cancel() { + if (!active) + return; + + // Restore original text + if (callback != null && !originalText.isEmpty()) { + callback.setText(originalText); + callback.setCursorPosition(originalCursorPos); + } + + resetState(); + + if (callback != null) { + callback.focusMainEditor(); + } + } + + /** + * Confirm rename operation and apply changes + */ + public void confirm() { + if (!active || callback == null) + return; + + // If word is empty, restore original + if (currentWord.isEmpty()) { + cancel(); + return; + } + + // Push the original text (pre-rename) so the first undo will restore the + // document to its state before the refactor. + // During live editing we used setTextWithoutUndo to avoid creating intermediate + // undo entries; pushing the original snapshot here gives a single predictable + // undo step back to the pre-rename state. + if (originalText != null && !originalText.isEmpty()) + callback.pushUndoState(originalText, originalCursorPos); + + // Calculate cursor position relative to primary occurrence + int cursorInWord = callback.getCursorPosition() - primaryOccurrenceStart; + cursorInWord = Math.max(0, Math.min(cursorInWord, currentWord.length())); + int newCursorPos = primaryOccurrenceStart + cursorInWord; + + resetState(); + + callback.focusMainEditor(); + callback.setCursorPosition(Math.max(0, newCursorPos)); + } + + private void resetState() { + active = false; + initialWord = ""; + originalWord = ""; + currentWord = ""; + primaryOccurrenceStart = -1; + primaryOccurrenceEnd = -1; + allOccurrences.clear(); + localShadowedPositions.clear(); + scope = null; + originalText = ""; + originalCursorPos = 0; + } + + /** + * Handle keyboard input during rename + * + * @return true if input was consumed + */ + public boolean keyTyped(char c, int keyCode) { + if (!active || callback == null) + return false; + + // ESC - cancel + if (keyCode == Keyboard.KEY_ESCAPE) { + cancel(); + return true; + } + + // Enter - confirm + if (keyCode == Keyboard.KEY_RETURN) { + confirm(); + return true; + } + + // Tab - confirm and move on + if (keyCode == Keyboard.KEY_TAB) { + confirm(); + return true; + } + + // Get selection state + SelectionState sel = callback.getSelectionState(); + boolean hasSelection = sel.hasSelection(); + + // Calculate cursor position within the word + int cursorInWord = callback.getCursorPosition() - primaryOccurrenceStart; + cursorInWord = Math.max(0, Math.min(cursorInWord, currentWord.length())); + + // Backspace + if (keyCode == Keyboard.KEY_BACK) { + if (hasSelection || GuiScreen.isCtrlKeyDown()) { + // Delete selected text (entire word if fully selected) + currentWord = ""; + applyLiveRename(); + sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart); + callback.setCursorPosition(primaryOccurrenceStart); + } else if (cursorInWord > 0) { + String before = currentWord.substring(0, cursorInWord - 1); + String after = currentWord.substring(cursorInWord); + currentWord = before + after; + applyLiveRename(); + // Update cursor after the text change + callback.setCursorPosition(primaryOccurrenceStart + cursorInWord - 1); + } + sel.markActivity(); + return true; + } + + // Delete + if (keyCode == Keyboard.KEY_DELETE) { + if (hasSelection) { + // Delete selected text (entire word if fully selected) + currentWord = ""; + applyLiveRename(); + sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart); + callback.setCursorPosition(primaryOccurrenceStart); + } else if (cursorInWord < currentWord.length()) { + String before = currentWord.substring(0, cursorInWord); + String after = currentWord.substring(cursorInWord + 1); + currentWord = before + after; + applyLiveRename(); + } + sel.markActivity(); + return true; + } + + // Backspace + if (keyCode == Keyboard.KEY_SPACE && hasSelection) { + // Delete selected text (entire word if fully selected) + currentWord = ""; + applyLiveRename(); + sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart); + callback.setCursorPosition(primaryOccurrenceStart); + } + + // Left arrow + if (keyCode == Keyboard.KEY_LEFT) { + if (cursorInWord > 0) { + callback.setCursorPosition(primaryOccurrenceStart + cursorInWord - 1); + sel.setSelection(0, 0); // Clear selection + } + sel.markActivity(); + return true; + } + + // Right arrow + if (keyCode == Keyboard.KEY_RIGHT) { + if (cursorInWord < currentWord.length()) { + callback.setCursorPosition(primaryOccurrenceStart + cursorInWord + 1); + sel.setSelection(0, 0); // Clear selection + } + sel.markActivity(); + return true; + } + + // Home + if (keyCode == Keyboard.KEY_HOME) { + callback.setCursorPosition(primaryOccurrenceStart); + sel.setSelection(0, 0); // Clear selection + sel.markActivity(); + return true; + } + + // End + if (keyCode == Keyboard.KEY_END) { + callback.setCursorPosition(primaryOccurrenceStart + currentWord.length()); + sel.setSelection(0, 0); // Clear selection + sel.markActivity(); + return true; + } + + // Ctrl+A - select all (select the whole word) + if (keyCode == Keyboard.KEY_A && + Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) { + sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart + currentWord.length()); + callback.setCursorPosition(primaryOccurrenceStart + currentWord.length()); + sel.markActivity(); + return true; + } + + // Character input - only valid identifier characters + boolean isFirstChar = hasSelection || (currentWord.isEmpty() && cursorInWord == 0); + if (isValidIdentifierChar(c, isFirstChar)) { + if (hasSelection) { + // Replace entire selection with this character + currentWord = "" + c; + applyLiveRename(); + callback.setCursorPosition(primaryOccurrenceStart + 1); + sel.setSelection(0, 0); // Clear selection + } else { + String before = currentWord.substring(0, cursorInWord); + String after = currentWord.substring(cursorInWord); + currentWord = before + c + after; + applyLiveRename(); + callback.setCursorPosition(primaryOccurrenceStart + cursorInWord + 1); + } + sel.markActivity(); + return true; + } + + // Block other input while in rename mode + return true; + } + + /** + * Check if string is a valid identifier + */ + private boolean isValidIdentifier(String s) { + if (s == null || s.isEmpty()) + return false; + return IDENTIFIER.matcher(s).matches(); + } + + /** + * Check if character is valid for an identifier + */ + private boolean isValidIdentifierChar(char c, boolean isFirst) { + if (isFirst) { + return Character.isLetter(c) || c == '_'; + } + return Character.isLetterOrDigit(c) || c == '_'; + } + + /** + * Apply rename changes live as user types (without creating undo entries) + */ + private void applyLiveRename() { + if (callback == null) + return; + + String text = callback.getText(); + + // Calculate new primary position after rename + int occurrencesBefore = 0; + for (int[] occ : allOccurrences) { + if (occ[0] < primaryOccurrenceStart) { + occurrencesBefore++; + } + } + int lengthDiff = currentWord.length() - originalWord.length(); + int newPrimaryStart = primaryOccurrenceStart + (occurrencesBefore * lengthDiff); + + // If the current typed word is empty, do NOT modify the document's other occurrences. + // We treat empty as a temporary state: preserve the editor text and the previously + // discovered occurrences so they will re-appear when the user types again. + if (currentWord.isEmpty()) { + // Keep the text unchanged and preserve allOccurrences (do not clear). + callback.setTextWithoutUndo(text); + // Keep originalWord as the last non-empty word so length calculations remain correct + // and place the caret at the primary position for editing. + setSelection(newPrimaryStart, newPrimaryStart); + return; + } + + // Apply rename for non-empty currentWord + String newText = applyRename(text, currentWord); + callback.setTextWithoutUndo(newText); // Don't create undo entry for each keystroke + + // Update tracking for current word (only when non-empty) + originalWord = currentWord; + + // Recalculate occurrences based on the new word + findOccurrences(newText, currentWord); + + // Update primary occurrence tracking + if (!allOccurrences.isEmpty()) { + for (int[] occ : allOccurrences) { + if (Math.abs(occ[0] - newPrimaryStart) < 2) { + setSelection(occ[0], occ[1]); + break; + } + } + } else if (currentWord.isEmpty()) { + // Keep tracking the position even when empty + setSelection(newPrimaryStart, newPrimaryStart); + } + } + + private void setSelection(int start, int end) { + primaryOccurrenceStart = start; + primaryOccurrenceEnd = end; + if (callback != null) + callback.getSelectionState().setSelection(start, end); + } + + /** + * Update occurrence positions when word is empty (for visual tracking) + */ + private void updateOccurrencePositionsForEmpty(int newPrimaryStart) { + // No-op: we preserve the previously discovered occurrences while the user + // temporarily clears the rename box. Clearing occurrences here caused them + // to be removed from the document during live-editing. Rendering will use + // primary occurrence position set by setSelection(). + // Intentionally left blank. + } + + /** + * Apply rename to all occurrences in text + */ + private String applyRename(String text, String newName) { + if (allOccurrences.isEmpty()) + return text; + + StringBuilder result = new StringBuilder(); + int lastEnd = 0; + + // Sort occurrences by position + List sorted = new ArrayList<>(allOccurrences); + sorted.sort((a, b) -> Integer.compare(a[0], b[0])); + + for (int[] occ : sorted) { + if (occ[0] >= lastEnd && occ[1] <= text.length()) { + result.append(text, lastEnd, occ[0]); + result.append(newName); // Append newName, even if empty (which deletes the occurrence) + lastEnd = occ[1]; + } + } + result.append(text.substring(lastEnd)); + + return result.toString(); + } + + /** + * Find all occurrences of the identifier within scope + */ + private void findOccurrences(String text, String word) { + allOccurrences.clear(); + + if (word.isEmpty() || scope == null) + return; + + // Get excluded ranges (strings and comments) + List excluded = MethodBlock.getExcludedRanges(text); + + Pattern pattern = Pattern.compile("\\b" + Pattern.quote(word) + "\\b"); + Matcher m = pattern.matcher(text); + + while (m.find()) { + int start = m.start(); + int end = m.end(); + + // Skip if in string or comment + if (isInExcludedRange(start, excluded)) + continue; + + // Check if within scope + if (scope.isGlobal) { + // For global scope, skip positions where local variables shadow the global + if (!isInLocalShadowedRange(start)) { + allOccurrences.add(new int[]{start, end}); + } + } else if (scope.containsPosition(start)) { + allOccurrences.add(new int[]{start, end}); + } + } + } + + /** + * Find all positions where local variables with the same name shadow the global variable. + * This populates localShadowedPositions with ranges where local declarations exist. + */ + private void findLocalShadowedPositions(String text, String varName) { + localShadowedPositions.clear(); + + List methods = MethodBlock.collectMethodBlocks(text); + List excluded = MethodBlock.getExcludedRanges(text); + + for (MethodBlock method : methods) { + // Check if this method has a local variable or parameter with the same name + boolean hasLocalDecl = method.localVariables.contains(varName); + boolean hasParamDecl = isParameterInMethod(text, varName, method); + + if (hasLocalDecl || hasParamDecl) { + // Find where the local/param scope starts + int scopeStart; + if (hasParamDecl) { + // Parameter scope starts at method start + scopeStart = method.startOffset; + } else { + // Local variable scope starts at declaration + scopeStart = findLocalDeclarationPosition(text, varName, method); + if (scopeStart < 0) scopeStart = method.startOffset; + } + + // Find all occurrences of varName within this method's shadowed range + Pattern pattern = Pattern.compile("\\b" + Pattern.quote(varName) + "\\b"); + Matcher m = pattern.matcher(text); + + while (m.find()) { + int pos = m.start(); + + // Skip if in string or comment + if (isInExcludedRange(pos, excluded)) + continue; + + // Check if within the shadowed scope of this method + if (pos >= scopeStart && pos < method.endOffset) { + localShadowedPositions.add(new int[]{m.start(), m.end()}); + } + } + } + } + } + + /** + * Check if a position is within a locally shadowed range + */ + private boolean isInLocalShadowedRange(int pos) { + for (int[] range : localShadowedPositions) { + if (pos >= range[0] && pos < range[1]) + return true; + } + return false; + } + + /** + * Check if varName is a parameter in the method + */ + private boolean isParameterInMethod(String text, String varName, MethodBlock method) { + String methodHeader = text.substring(method.startOffset, + Math.min(method.startOffset + 500, method.endOffset)); + int parenStart = methodHeader.indexOf('('); + int parenEnd = methodHeader.indexOf(')'); + if (parenStart >= 0 && parenEnd > parenStart) { + String params = methodHeader.substring(parenStart + 1, parenEnd); + Pattern paramPattern = Pattern.compile("\\b" + Pattern.quote(varName) + "\\b"); + return paramPattern.matcher(params).find(); + } + return false; + } + + /** + * Check if position is in an excluded range + */ + private boolean isInExcludedRange(int pos, List ranges) { + for (int[] range : ranges) { + if (pos >= range[0] && pos < range[1]) + return true; + } + return false; + } + + /** + * Find word boundaries at a given position + */ + private int[] findWordAtPosition(String text, int pos) { + if (pos < 0 || pos > text.length()) + return null; + + // Find start of word + int start = pos; + while (start > 0 && isIdentifierChar(text.charAt(start - 1))) { + start--; + } + + // Find end of word + int end = pos; + while (end < text.length() && isIdentifierChar(text.charAt(end))) { + end++; + } + + if (start == end) + return null; + + // Validate it's a proper identifier + String word = text.substring(start, end); + if (!IDENTIFIER.matcher(word).matches()) + return null; + + return new int[]{start, end}; + } + + private boolean isIdentifierChar(char c) { + return Character.isLetterOrDigit(c) || c == '_'; + } + + /** + * Determine the scope for a variable at the given position. + * Key insight: Local variables shadow global variables within their scope. + * We need to check if the cursor is within the scope of a local declaration. + */ + private ScopeInfo determineScope(String text, int position, String varName, JavaTextContainer container) { + List methods = MethodBlock.collectMethodBlocks(text); + + // First, check if this is a global field declaration (outside any method) + boolean isGlobalField = isGlobalFieldDeclaration(text, varName, methods); + + // Check if cursor is outside all methods - definitely global scope + boolean insideMethod = false; + MethodBlock containingMethod = null; + for (MethodBlock method : methods) { + if (method.containsPosition(position)) { + insideMethod = true; + containingMethod = method; + break; + } + } + + if (!insideMethod || containingMethod == null) { + // Cursor is outside methods - must be global scope + return new ScopeInfo(0, text.length(), true, "global"); + } + + // Cursor is inside a method + // Check if it's a method parameter first + String methodHeader = text.substring(containingMethod.startOffset, + Math.min(containingMethod.startOffset + 500, containingMethod.endOffset)); + int parenStart = methodHeader.indexOf('('); + int parenEnd = methodHeader.indexOf(')'); + if (parenStart >= 0 && parenEnd > parenStart) { + String params = methodHeader.substring(parenStart + 1, parenEnd); + Pattern paramPattern = Pattern.compile("\\b" + Pattern.quote(varName) + "\\b"); + if (paramPattern.matcher(params).find()) { + // It's a parameter - scope to method + return new ScopeInfo(containingMethod.startOffset, containingMethod.endOffset, false, "parameter"); + } + } + + // Check if there's a local variable with this name in the method + if (containingMethod.localVariables.contains(varName)) { + // Find where the local variable is declared + int localDeclPosition = findLocalDeclarationPosition(text, varName, containingMethod); + + if (localDeclPosition >= 0 && position >= localDeclPosition) { + // Cursor is at or after the local declaration - this is the local variable + int[] blockBounds = findInnermostBlockWithDeclaration(text, position, varName, containingMethod); + if (blockBounds != null) { + return new ScopeInfo(blockBounds[0], blockBounds[1], false, "block"); + } + return new ScopeInfo(containingMethod.startOffset, containingMethod.endOffset, false, "local"); + } else if (isGlobalField) { + // Cursor is BEFORE the local declaration - must be referring to global + // This is the shadowing case + return new ScopeInfo(0, text.length(), true, "global"); + } + } + + // No local variable with this name - check if it's a global field + if (isGlobalField) { + return new ScopeInfo(0, text.length(), true, "global"); + } + + // Unknown variable - default to method scope as fallback + return new ScopeInfo(containingMethod.startOffset, containingMethod.endOffset, false, "method"); + } + + /** + * Check if a variable is declared as a global field (outside any method) + */ + private boolean isGlobalFieldDeclaration(String text, String varName, List methods) { + Matcher m = JavaTextContainer.GLOBAL_FIELD_DECL.matcher(text); + while (m.find()) { + if (m.group(2).equals(varName)) { + int pos = m.start(2); + // Verify it's not inside a method + boolean insideMethod = false; + for (MethodBlock method : methods) { + if (method.containsPosition(pos)) { + insideMethod = true; + break; + } + } + if (!insideMethod) { + return true; + } + } + } + return false; + } + + /** + * Find the position of the first local variable declaration with the given name + */ + private int findLocalDeclarationPosition(String text, String varName, MethodBlock method) { + // Pattern for local variable declaration: Type varName = or Type varName; + Pattern localDecl = Pattern.compile("\\b\\w+\\s+" + Pattern.quote(varName) + "\\s*[=;,)]"); + String methodText = text.substring(method.startOffset, Math.min(method.endOffset, text.length())); + Matcher m = localDecl.matcher(methodText); + + // Skip past the method header (parameters) to find true local declarations + int bodyStart = methodText.indexOf('{'); + if (bodyStart < 0) + bodyStart = 0; + + while (m.find()) { + if (m.start() >= bodyStart) { + return method.startOffset + m.start(); + } + } + return -1; + } + + /** + * Find the innermost brace block that contains both the position and a declaration of varName + */ + private int[] findInnermostBlockWithDeclaration(String text, int position, String varName, MethodBlock method) { + List excluded = MethodBlock.getExcludedRanges(text); + + // Find all brace blocks within the method + List blocks = new ArrayList<>(); + List braceStack = new ArrayList<>(); + + for (int i = method.startOffset; i < method.endOffset && i < text.length(); i++) { + if (isInExcludedRange(i, excluded)) + continue; + + char c = text.charAt(i); + if (c == '{') { + braceStack.add(i); + } else if (c == '}') { + if (!braceStack.isEmpty()) { + int openPos = braceStack.remove(braceStack.size() - 1); + blocks.add(new int[]{openPos, i + 1}); + } + } + } + + // Find smallest block containing position that has a declaration of varName + int[] best = null; + int bestSize = Integer.MAX_VALUE; + + Pattern declPattern = Pattern.compile("\\b\\w+\\s+" + Pattern.quote(varName) + "\\s*[=;,)]"); + + for (int[] block : blocks) { + if (block[0] <= position && block[1] >= position) { + int size = block[1] - block[0]; + if (size < bestSize) { + // Check if this block contains a declaration of varName + String blockText = text.substring(block[0], Math.min(block[1], text.length())); + if (declPattern.matcher(blockText).find()) { + best = block; + bestSize = size; + } + } + } + } + + return best; + } + + // ==================== RENDERING ==================== + + /** + * Get all occurrences for rendering highlights + */ + public List getOccurrences() { + return allOccurrences; + } + + /** + * Get primary occurrence (the one under cursor when rename started) + */ + public int[] getPrimaryOccurrence() { + if (!active) + return null; + return new int[]{primaryOccurrenceStart, primaryOccurrenceEnd}; + } + + /** + * Check if a given occurrence is the primary one + */ + public boolean isPrimaryOccurrence(int start) { + return start == primaryOccurrenceStart; + } + + /** + * Get cursor position within the rename word for rendering + */ + public int getCursorInWord() { + if (callback == null) return 0; + int cursorPos = callback.getCursorPosition(); + return Math.max(0, Math.min(cursorPos - primaryOccurrenceStart, currentWord.length())); + } + + /** + * Check if word is fully selected (for visual rendering) + */ + public boolean isWordFullySelected() { + if (callback == null) return false; + SelectionState selection = callback.getSelectionState(); + return selection.hasSelection() && + selection.getStartSelection() == primaryOccurrenceStart && + selection.getEndSelection() == primaryOccurrenceEnd; + } + + /** + * Get the current word being renamed + */ + public String getCurrentWord() { + return currentWord; + } + + // ==================== MOUSE HANDLING ==================== + + /** + * Handle mouse click within the rename box area. + * + * @param clickPosInText The text position where the click occurred + * @return true if the click was within the primary occurrence box, false otherwise + */ + public boolean handleClick(int clickPosInText) { + if (!active || callback == null) + return false; + + return false; + } + + /** + * Check if a text position is within the primary rename occurrence + */ + public boolean isPositionInPrimaryOccurrence(int pos) { + if (!active) + return false; + return pos >= primaryOccurrenceStart && pos <= primaryOccurrenceEnd; + } + + // ==================== STATUS ==================== + + public boolean isActive() { + return active; + } + + /** + * @deprecated Use GuiScriptTextArea's cursor rendering instead + */ + public void updateCursor() { + // No-op - cursor is now handled by GuiScriptTextArea + } + + /** + * Check if cursor should be visible based on blink timing. + * Uses SelectionState's recent input check. + */ + public boolean shouldShowCursor() { + if (callback != null) { + return callback.getSelectionState().hadRecentInput(); + } + return true; + } + + /** + * Get scope type description for status display + */ + public String getScopeDescription() { + if (scope == null) + return ""; + switch (scope.scopeType) { + case "global": + return "Renaming in file (" + allOccurrences.size() + " occurrences)"; + case "parameter": + return "Renaming parameter (" + allOccurrences.size() + " occurrences)"; + case "local": + return "Renaming local variable (" + allOccurrences.size() + " occurrences)"; + case "method": + return "Renaming in method (" + allOccurrences.size() + " occurrences)"; + case "block": + return "Renaming in block (" + allOccurrences.size() + " occurrences)"; + default: + return "Renaming (" + allOccurrences.size() + " occurrences)"; + } + } + + /** + * Get number of occurrences that will be renamed + */ + public int getOccurrenceCount() { + return allOccurrences.size(); + } + + public void drawStatusBox() { + // String status = renameHandler.getScopeDescription(); + // String hint = "Enter \u2713 | Esc \u2715"; // Enter to confirm, Esc to cancel + // int statusWidth = Math.max(ClientProxy.Font.width(status), ClientProxy.Font.width(hint)) + 8; + // int statusX = x + width - statusWidth - 8; + // int statusY = y + height - 30; + // // Semi-transparent background + // drawRect(statusX - 2, statusY - 2, statusX + statusWidth + 2, statusY + 24, 0xDD1a1a2e); + // // Border (purple/blue for rename mode) + // int borderColor = 0xFF6677dd; + // drawRect(statusX - 2, statusY - 2, statusX + statusWidth + 2, statusY - 1, borderColor); + // drawRect(statusX - 2, statusY + 23, statusX + statusWidth + 2, statusY + 24, borderColor); + // drawRect(statusX - 2, statusY - 2, statusX - 1, statusY + 24, borderColor); + // drawRect(statusX + statusWidth + 1, statusY - 2, statusX + statusWidth + 2, statusY + 24, borderColor); + // // Status text (top line) + // ClientProxy.Font.drawString(status, statusX + 4, statusY + 2, 0xFFccddff); + // // Hint text (bottom line - dimmer) + // ClientProxy.Font.drawString(hint, statusX + 4, statusY + 13, 0xAA88aacc); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java new file mode 100644 index 000000000..4d71a6b62 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java @@ -0,0 +1,241 @@ +package noppes.npcs.client.gui.util.script; + +import org.lwjgl.input.Mouse; + +/** + * Manages smooth scrolling state and animation for the script text area. + * Uses exponential smoothing for fluid scroll animations. + */ +public class ScrollState { + // Current fractional scroll position (in lines) + private double scrollPos = -1.0; + // Target scroll position to animate towards + private double targetScroll = 0.0; + // Current scroll velocity for momentum calculations + private double scrollVelocity = 0.0; + // Last timestamp for delta time calculation + private long lastScrollTime = 0L; + // First visible line + private int scrolledLine = 0; + // Scrollbar drag offset when dragging thumb + private int scrollbarDragOffset = 0; + // Whether currently dragging the scrollbar + private boolean clickScrolling = false; + + // Animation parameters + private static final double TAU = 0.1; // Time constant (~55ms feels snappy) + private static final double SNAP_THRESHOLD = 0.01; // Snap to target when this close + private static final double MAX_DT = 0.05; // Max delta time for stability + + /** + * Reset scroll state to initial values + */ + public void reset() { + scrollPos = 0.0; + targetScroll = 0.0; + scrollVelocity = 0.0; + scrolledLine = 0; + lastScrollTime = System.currentTimeMillis(); + } + + /** + * Initialize scroll position if not already initialized + */ + public void initializeIfNeeded(int currentLine) { + if (scrollPos < 0) { + scrollPos = currentLine; + lastScrollTime = System.currentTimeMillis(); + } + } + + /** + * Update scroll animation. Call once per frame. + * + * @param maxScroll Maximum valid scroll position + */ + public void update(int maxScroll) { + long nowMs = System.currentTimeMillis(); + double dt = Math.min(MAX_DT, (nowMs - lastScrollTime) / 1000.0); + lastScrollTime = nowMs; + + double dist = targetScroll - scrollPos; + + // Snap to target if close enough + if (Math.abs(dist) < SNAP_THRESHOLD) { + scrollPos = targetScroll; + scrollVelocity = 0.0; + } else { + // Exponential smoothing + double alpha = 1.0 - Math.exp(-dt / Math.max(1e-6, TAU)); + double prev = scrollPos; + scrollPos += dist * alpha; + scrollVelocity = (scrollPos - prev) / (dt > 0 ? dt : 1e-6); + + // Clamp overshoot + if ((dist > 0 && scrollPos > targetScroll) || (dist < 0 && scrollPos < targetScroll)) { + scrollPos = targetScroll; + scrollVelocity = 0.0; + } + } + + // Keep in bounds + clampToBounds(maxScroll); + + // Update integer line for rendering + scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); + } + + /** + * Clamp scroll position to valid bounds + */ + public void clampToBounds(int maxScroll) { + if (scrollPos < 0) scrollPos = 0; + if (scrollPos > maxScroll) scrollPos = maxScroll; + if (targetScroll < 0) targetScroll = 0; + if (targetScroll > maxScroll) targetScroll = maxScroll; + scrolledLine = Math.max(0, Math.min(scrolledLine, maxScroll)); + } + + /** + * Apply mouse wheel scroll + * + * @param wheelDelta Positive = scroll up, negative = scroll down + * @param maxScroll Maximum valid scroll position + */ + public void applyWheelScroll(int wheelDelta, int maxScroll) { + double sign = Math.copySign(1, wheelDelta); + targetScroll -= sign * 2.0; // 2 lines per wheel tick + clampToBounds(maxScroll); + } + + /** + * Set target scroll directly (for scrollbar dragging) + */ + public void setTargetScroll(double target, int maxScroll) { + targetScroll = Math.max(0, Math.min(target, maxScroll)); + } + + /** + * Scroll to make a specific line visible + * + * @param lineIdx Line index to make visible + * @param visibleLines Number of visible lines in viewport + * @param maxScroll Maximum scroll position + */ + public void scrollToLine(int lineIdx, int visibleLines, int maxScroll) { + int firstVisible = scrolledLine; + int lastFullyVisible = scrolledLine + visibleLines; + + if (lineIdx < firstVisible) { + // Line is above viewport - scroll up + targetScroll = lineIdx; + } else if (lineIdx > lastFullyVisible) { + // Line is below viewport - scroll down + targetScroll = Math.min(lineIdx - visibleLines, maxScroll); + } + // If line is visible, don't change scroll + } + + // Getters + public double getScrollPos() { + return scrollPos; + } + + public double getTargetScroll() { + return targetScroll; + } + + /** + * @return First visible line + **/ + public int getScrolledLine() { + return scrolledLine; + } + + public double getScrollVelocity() { + return scrollVelocity; + } + + public boolean isClickScrolling() { + return clickScrolling; + } + + public int getScrollbarDragOffset() { + return scrollbarDragOffset; + } + + /** + * Get fractional offset for sub-pixel rendering + */ + public double getFractionalOffset() { + return scrollPos - scrolledLine; + } + + // Setters for scrollbar interaction + public void setClickScrolling(boolean scrolling) { + this.clickScrolling = scrolling; + } + + public void setScrollbarDragOffset(int offset) { + this.scrollbarDragOffset = offset; + } + + public void setScrolledLine(int line) { + this.scrolledLine = line; + } + + /** + * Handle scrollbar dragging interaction. This encapsulates the logic that was + * previously in the GUI class for handling thumb clicks/drags. + * + * @param yMouse current mouse Y coordinate + * @param areaX GUI area X (unused but kept for parity) + * @param areaY GUI area Y (top of text area) + * @param areaHeight height of the scroll track area + * @param visibleLines number of visible lines in viewport + * @param linesCount total number of lines + * @param maxScroll maximum allowed scroll value + */ + public void handleClickScrolling(int yMouse, int areaX, int areaY, int areaHeight, int visibleLines, int linesCount, int maxScroll) { + // Keep dragging while mouse button is down + setClickScrolling(Mouse.isButtonDown(0)); + int diff = Math.max(0, linesCount - visibleLines); + if (diff > 0) { + int sbSize = Math.max((int) (1f * visibleLines / Math.max(1, linesCount) * areaHeight), 2); + int trackTop = areaY + 1; + int trackHeight = Math.max(1, areaHeight - 4); + int thumbRange = Math.max(1, trackHeight - sbSize); + double linesCountD = Math.max(1, (double) linesCount); + int thumbTop = (int) (areaY + 1f * getScrollPos() / linesCountD * (areaHeight - 4)) + 1; + + if (yMouse < thumbTop || yMouse > thumbTop + sbSize) { + double centerRatio = (double) (yMouse - trackTop) / (double) trackHeight; + centerRatio = Math.max(0.0, Math.min(1.0, centerRatio)); + setTargetScroll(centerRatio * diff, maxScroll); + setScrollbarDragOffset(sbSize / 2); + } else { + int desiredTop = yMouse - getScrollbarDragOffset(); + desiredTop = Math.max(trackTop, Math.min(trackTop + thumbRange, desiredTop)); + double ratio = (double) (desiredTop - trackTop) / (double) thumbRange; + ratio = Math.max(0.0, Math.min(1.0, ratio)); + setTargetScroll(ratio * diff, maxScroll); + } + } + if (!isClickScrolling()) { + setScrollbarDragOffset(0); + } + } + + /** + * Attempt to start a scrollbar drag if the mouse down occurred on the scrollbar thumb area. + * Returns true if drag was initiated (caller should cancel normal text-click drag behavior). + */ + public void startScrollbarDrag(int yMouse, int areaY, int areaHeight, int linesCount) { + // Thumb width in GuiScriptTextArea is 6px (posX..posX+5), so check clicks near the right edge + // Start click-scrolling mode and set initial drag offset relative to thumb top. + setClickScrolling(true); + double linesCountD = Math.max(1, (double) linesCount); + int thumbTop = (int) (areaY + 1f * getScrollPos() / linesCountD * (areaHeight - 4)) + 1; + setScrollbarDragOffset(yMouse - thumbTop); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java new file mode 100644 index 000000000..32e835aaf --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -0,0 +1,1380 @@ +package noppes.npcs.client.gui.util.script; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.util.ChatAllowedCharacters; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.gui.util.GuiScriptTextArea.UndoData; +import noppes.npcs.client.gui.util.GuiUtil; +import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; +import noppes.npcs.util.ValueUtil; +import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Search and Replace bar component for the script editor. + * Provides IntelliJ-like search functionality with: + * - Search text field with rounded edges + * - Match case (Cc) toggle + * - Whole word (W) toggle + * - Previous/Next match navigation + * - Replace functionality with Replace/Replace All/Exclude buttons + * - Auto-scroll to matches + * - All matches highlighting + */ +public class SearchReplaceBar { + + // ==================== TEXTURE ==================== + public static final ResourceLocation TEXTURE = OverlayKeyPresetViewer.TEXTURE; + + // ==================== DIMENSIONS ==================== + private int x, y, width; + private int barHeight = 24; + private int replaceBarHeight = 22; + private int textFieldWidth = 150; + private int textFieldHeight = 16; + private int buttonSize = 16; + private int buttonSpacing = 2; + private int padding = 4; + + // ==================== STATE ==================== + private boolean visible = false; + private boolean showReplace = false; + private boolean searchFieldFocused = true; + private boolean replaceFieldFocused = false; + + // ==================== SEARCH OPTIONS ==================== + private boolean matchCase = false; + private boolean wholeWord = false; + + // ==================== TEXT FIELDS ==================== + private String searchText = ""; + private String replaceText = ""; + private int searchCursor = 0; + private int replaceCursor = 0; + private int searchSelectionStart = 0; + private int searchSelectionEnd = 0; + private int replaceSelectionStart = 0; + private int replaceSelectionEnd = 0; + // Text scroll offsets for when text exceeds field width + private int searchScrollOffset = 0; + private int replaceScrollOffset = 0; + + // ==================== UNDO/REDO ==================== + private List searchUndoList = new ArrayList<>(); + private List searchRedoList = new ArrayList<>(); + private List replaceUndoList = new ArrayList<>(); + private List replaceRedoList = new ArrayList<>(); + + // ==================== MATCHES ==================== + private List matches = new ArrayList<>(); + private int currentMatchIndex = -1; + private List excludedMatches = new ArrayList<>(); + + // ==================== HOVER STATE ==================== + private boolean hoverClose = false; + private boolean hoverSearchUnwind = false; + private boolean hoverMatchCase = false; + private boolean hoverWholeWord = false; + private boolean hoverPrev = false; + private boolean hoverNext = false; + private boolean hoverReplace = false; + private boolean hoverReplaceAll = false; + private boolean hoverExclude = false; + + // ==================== CURSOR BLINK ==================== + private int cursorCounter = 0; + private long lastInputTime = 0; // For cursor blink pause on activity + + // ==================== DOUBLE/TRIPLE CLICK ==================== + private long lastClickTime = 0; + private int clickCount = 0; + private boolean lastClickWasSearch = true; + + // ==================== CALLBACK ==================== + public SearchCallback callback; + + private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; + + /** + * Callback interface for search/replace operations + */ + public interface SearchCallback { + String getText(); + + String getHighlightedWord(); + + // Return current selection bounds in the main editor (start inclusive, end exclusive) + int getSelectionStart(); + + int getSelectionEnd(); + + void setText(String text); + + void scrollToPosition(int position); + + void setSelection(int start, int end); + + int getGutterWidth(); + + void unfocusMainEditor(); + + void focusMainEditor(); + + void onMatchesUpdated(); // Called when matches change (for undo/redo sync) + + void resizeEditor(boolean open, int barHeight); + } + + public SearchReplaceBar() { + } + + public void setCallback(SearchCallback callback) { + this.callback = callback; + } + + /** + * Initialize bar position based on text area bounds. + * Preserves existing state (search text, matches, etc.) across initGui calls. + */ + public void initGui(int textAreaX, int textAreaY, int textAreaWidth) { + this.x = textAreaX; + this.y = textAreaY; + this.width = textAreaWidth; + // State is preserved - don't reset searchText, matches, visible, etc. + } + + public void open() { + boolean oldVisible = this.visible; + boolean matchHighlight = false; + visible = true; + if (callback != null) { + // Always notify callback that the search UI is open and provide its current height. + callback.resizeEditor(true, getTotalHeight()); + callback.unfocusMainEditor(); + + String highlight = callback.getHighlightedWord(); + if (highlight != null) { + searchText = highlight; + matchHighlight = true; + } + } + + searchSelectionStart = 0; + searchSelectionEnd = searchText.length(); + searchCursor = searchText.length(); + markActivity(); + updateMatches(); + if (matchHighlight) { + setCurrentMatchToHighlight(); + navigateToCurrentMatch(); + } + } + + /** + * Open search bar (Ctrl+R) + */ + public void openSearch() { + showReplace = false; + searchFieldFocused = true; + replaceFieldFocused = false; + open(); + + } + + /** + * Open search+replace bar (Ctrl+Shift+R) + */ + public void openSearchReplace() { + showReplace = true; //both for window resize properly + searchFieldFocused = false; + replaceFieldFocused = true; + open(); + } + + /** + * Close the search bar and restore focus to main editor + */ + public void close() { + if (callback != null) { + // When closing, signal closed and height 0 + callback.resizeEditor(false, 0); + callback.focusMainEditor(); + } + visible = false; + searchFieldFocused = false; + replaceFieldFocused = false; + } + + public void focus(boolean replace) { + if (replace) + replaceFieldFocused = true; + else + searchFieldFocused = true; + } + + /** + * Unfocus the search bar (when clicking main editor) + */ + public void unfocus() { + searchFieldFocused = false; + replaceFieldFocused = false; + } + + /** + * Save current state to undo list before making changes + */ + private void saveState(boolean isSearchField) { + if (isSearchField) { + searchUndoList.add(new UndoData(searchText, searchCursor)); + searchRedoList.clear(); // Clear redo on new change + } else { + replaceUndoList.add(new UndoData(replaceText, replaceCursor)); + replaceRedoList.clear(); + } + } + + /** + * Undo last change in focused field + */ + public void undo() { + if (searchFieldFocused && !searchUndoList.isEmpty()) { + UndoData data = searchUndoList.remove(searchUndoList.size() - 1); + searchRedoList.add(new UndoData(searchText, searchCursor)); + searchText = data.text; + searchCursor = data.cursorPosition; + searchSelectionStart = searchCursor; + searchSelectionEnd = searchCursor; + updateScrollOffset(true); + updateMatches(); + } else if (replaceFieldFocused && !replaceUndoList.isEmpty()) { + UndoData data = replaceUndoList.remove(replaceUndoList.size() - 1); + replaceRedoList.add(new UndoData(replaceText, replaceCursor)); + replaceText = data.text; + replaceCursor = data.cursorPosition; + replaceSelectionStart = replaceCursor; + replaceSelectionEnd = replaceCursor; + updateScrollOffset(false); + } + } + + /** + * Redo last undone change in focused field + */ + public void redo() { + if (searchFieldFocused && !searchRedoList.isEmpty()) { + UndoData data = searchRedoList.remove(searchRedoList.size() - 1); + searchUndoList.add(new UndoData(searchText, searchCursor)); + searchText = data.text; + searchCursor = data.cursorPosition; + searchSelectionStart = searchCursor; + searchSelectionEnd = searchCursor; + updateScrollOffset(true); + updateMatches(); + } else if (replaceFieldFocused && !replaceRedoList.isEmpty()) { + UndoData data = replaceRedoList.remove(replaceRedoList.size() - 1); + replaceUndoList.add(new UndoData(replaceText, replaceCursor)); + replaceText = data.text; + replaceCursor = data.cursorPosition; + replaceSelectionStart = replaceCursor; + replaceSelectionEnd = replaceCursor; + updateScrollOffset(false); + } + } + + /** + * Update scroll offset for text field to keep cursor visible + */ + private void updateScrollOffset(boolean isSearchField) { + int fieldWidth = textFieldWidth - 8; + String text = isSearchField ? searchText : replaceText; + int cursor = isSearchField ? searchCursor : replaceCursor; + String beforeCursor = cursor > 0 ? text.substring(0, cursor) : ""; + int cursorX = font.getStringWidth(beforeCursor); + int scrollOffset = isSearchField ? searchScrollOffset : replaceScrollOffset; + + // Scroll to keep cursor visible + if (cursorX - scrollOffset * 6 > fieldWidth) { + scrollOffset = Math.max(0, cursor - fieldWidth / 6); + } else if (cursor < scrollOffset) { + scrollOffset = cursor; + } + + if (isSearchField) { + searchScrollOffset = scrollOffset; + } else { + replaceScrollOffset = scrollOffset; + } + } + + /** + * Toggle visibility + */ + public void toggle() { + if (visible && !showReplace) { + close(); + } else { + openSearch(); + } + } + + /** + * Toggle replace mode + */ + public void toggleReplace() { + if (visible && showReplace) { + close(); + } else { + openSearchReplace(); + } + } + + public boolean isVisible() { + return visible; + } + + public boolean isSearchFocused() { + return visible && searchFieldFocused; + } + + public boolean isReplaceFocused() { + return visible && replaceFieldFocused; + } + + public boolean hasFocus() { + return visible && (searchFieldFocused || replaceFieldFocused); + } + + /** + * Get total height of the bar (for viewport offset) + */ + public int getTotalHeight() { + if (!visible) return 0; + return showReplace ? barHeight + replaceBarHeight : barHeight; + } + + /** + * Get all current matches for highlighting + */ + public List getMatches() { + return matches; + } + + /** + * Get current match index + */ + public int getCurrentMatchIndex() { + return currentMatchIndex; + } + + /** + * Check if a match is excluded + */ + public boolean isMatchExcluded(int index) { + return excludedMatches.contains(index); + } + + /** + * Update cursor blink counter + */ + public void updateCursor() { + cursorCounter++; + } + + /** + * Mark activity to pause cursor blinking + */ + private void markActivity() { + lastInputTime = System.currentTimeMillis(); + } + + /** + * Check if there was recent input (cursor stays visible during typing) + */ + private boolean hadRecentInput() { + return System.currentTimeMillis() - lastInputTime < 500; + } + + /** + * Check if cursor should be visible (solid during typing, blinking otherwise) + */ + private boolean shouldShowCursor() { + return hadRecentInput() || (cursorCounter / 10) % 2 == 0; + } + + // ==================== DRAWING ==================== + + /** + * Draw the search/replace bar + */ + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + updateHoverStates(mouseX, mouseY); + + int totalHeight = getTotalHeight(); + Gui.drawRect(x, y, x + width, y + totalHeight, 0xFF2d2d30); + Gui.drawRect(x, y + totalHeight - 1, x + width, y + totalHeight, 0xFF414141); + + drawSearchRow(mouseX, mouseY); + + if (showReplace) { + drawReplaceRow(mouseX, mouseY); + } + } + + private void drawSearchRow(int mouseX, int mouseY) { + int rowY = y + padding; + int currentX = x + padding; + textFieldWidth = 100; + drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, + searchText, searchCursor, searchSelectionStart, searchSelectionEnd, + searchFieldFocused, "Search...", searchScrollOffset, true); + currentX += textFieldWidth + buttonSpacing + 4; + + // "0 results" in red, otherwise "X/Y" in normal color + String matchInfo = matches.isEmpty() ? "0 results" : + (currentMatchIndex + 1) + "/" + matches.size(); + int matchInfoWidth = font.getStringWidth(matchInfo); + font.drawString(matchInfo, currentX, rowY + 4, + matches.isEmpty() && !searchText.isEmpty() ? 0xFFff6666 : 0xFFcccccc); + currentX += matchInfoWidth + 8; + drawToggleButton(currentX, rowY, "Cc", matchCase, hoverMatchCase, "Match Case"); + currentX += buttonSize + buttonSpacing; + + drawToggleButton(currentX, rowY, "W", wholeWord, hoverWholeWord, "Whole Word"); + currentX += buttonSize + buttonSpacing + 4; + + boolean prevEnabled = !matches.isEmpty(); + drawNavButton(currentX, rowY, "▲", prevEnabled, hoverPrev); + currentX += buttonSize + buttonSpacing; + + boolean nextEnabled = !matches.isEmpty(); + drawNavButton(currentX, rowY, "▼", nextEnabled, hoverNext); + currentX += buttonSize + buttonSpacing; + + int closeX = x + width - buttonSize - padding; + drawCloseButton(closeX, rowY, hoverClose); + + int replaceUnwindButton = closeX - buttonSize - buttonSpacing; + drawActionButton(replaceUnwindButton, rowY, showReplace ? "▼" : "◀", hoverSearchUnwind, true); + } + + private void drawReplaceRow(int mouseX, int mouseY) { + int rowY = y + barHeight + 2; + int currentX = x + padding; + + drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, + replaceText, replaceCursor, replaceSelectionStart, replaceSelectionEnd, + replaceFieldFocused, "Replace...", replaceScrollOffset, false); + currentX += textFieldWidth + buttonSpacing + 4; + + drawActionButton(currentX, rowY, "Replace", hoverReplace, !matches.isEmpty()); + currentX += font.getStringWidth("Replace") + 12 + buttonSpacing; + + drawActionButton(currentX, rowY, "Replace All", hoverReplaceAll, !matches.isEmpty()); + currentX += font.getStringWidth("Replace All") + 12 + buttonSpacing; + + drawActionButton(currentX, rowY, "Exclude", hoverExclude, currentMatchIndex >= 0); + } + + private void drawTextField(int fieldX, int fieldY, int fieldWidth, int fieldHeight, + String text, int cursor, int selStart, int selEnd, + boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { + // Draw text field background + Gui.drawRect(fieldX, fieldY, fieldX + fieldWidth, fieldY + fieldHeight, 0xFF1e1e1e); + Gui.drawRect(fieldX, fieldY, fieldX + fieldWidth, fieldY + 1, focused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fieldX, fieldY + fieldHeight - 1, fieldX + fieldWidth, fieldY + fieldHeight, + focused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fieldX, fieldY, fieldX + 1, fieldY + fieldHeight, focused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fieldX + fieldWidth - 1, fieldY, fieldX + fieldWidth, fieldY + fieldHeight, + focused ? 0xFF007acc : 0xFF3c3c3c); + + // Calculate visible area + int textX = fieldX + 4; + int textY = fieldY + (fieldHeight - 8) / 2; + int visibleWidth = fieldWidth - 8; + + // Apply scroll offset + String visibleText = text; + int adjustedSelStart = selStart; + int adjustedSelEnd = selEnd; + + if (scrollOffset > 0 && scrollOffset < text.length()) { + visibleText = text.substring(scrollOffset); + adjustedSelStart = Math.max(0, selStart - scrollOffset); + adjustedSelEnd = Math.max(0, selEnd - scrollOffset); + } + + // Draw selection highlight + if (adjustedSelStart != adjustedSelEnd) { + int minSel = Math.min(adjustedSelStart, adjustedSelEnd); + int maxSel = Math.max(adjustedSelStart, adjustedSelEnd); + String beforeSel = visibleText.substring(0, Math.min(minSel, visibleText.length())); + String inSel = visibleText.substring(Math.min(minSel, visibleText.length()), + Math.min(maxSel, visibleText.length())); + int selX = textX + font.getStringWidth(beforeSel); + int selW = font.getStringWidth(inSel); + Gui.drawRect(selX, textY - 1, selX + selW, textY + 9, 0xFF264f78); + } + + int cursorPosX = font.getStringWidth(text.substring(0, Math.min(cursor, text.length()))); + if (cursorPosX - scrollOffset > visibleWidth - 2) { + scrollOffset = cursorPosX - visibleWidth + 2; + } else if (cursorPosX - scrollOffset < 0) { + scrollOffset = Math.max(0, cursorPosX); + } + // Update stored scroll offset + if (isSearchField) { + searchScrollOffset = scrollOffset; + } else { + replaceScrollOffset = scrollOffset; + } + + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight); + + + // Draw selection highlight + if (selStart != selEnd && focused) { + int startX = textX + font.getStringWidth( + text.substring(0, Math.min(selStart, text.length()))) - scrollOffset; + int endX = textX + font.getStringWidth(text.substring(0, Math.min(selEnd, text.length()))) - scrollOffset; + Gui.drawRect(startX, textY - 1, endX, textY + font.FONT_HEIGHT, 0xFF2d5ca6); + } + + // Draw text with scroll offset + boolean noMatches = !text.isEmpty() && matches.isEmpty(); + int col = text.isEmpty() ? 0xFFa0a0a0 : 0xFFe0e0e0; + if (isSearchField && noMatches) + col = 0xFFff6666; // Red + + font.drawString(text.isEmpty() ? placeholder : text, textX - scrollOffset, textY, col); + + // Draw cursor (stays visible during typing) + if (focused && shouldShowCursor()) { + int cursorDrawX = textX + cursorPosX - scrollOffset; + Gui.drawRect(cursorDrawX, textY - 1, cursorDrawX + 1, textY + font.FONT_HEIGHT, 0xFFffffff); + } + + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + } + + private void oldDrawTextField(int fieldX, int fieldY, int fieldWidth, int fieldHeight, + String text, int cursor, int selStart, int selEnd, + boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + GL11.glPushMatrix(); + GL11.glColor4f(1, 1, 1, 1); + + float boxScaleX = fieldWidth / 32f, boxScaleY = 1.5f; + GL11.glScalef(boxScaleX, boxScaleY, 1); + + if (focused) { + GL11.glColor4f(0.28f, 0.45f, 0.7f, 1f); + } else { + GL11.glColor4f(0.33f, 0.33f, 0.33f, 1f); + } + GuiUtil.drawTexturedModalRect(fieldX / boxScaleX, (fieldY - 16) / boxScaleY, 32, 20, 0, 492); + GL11.glPopMatrix(); + + int textX = fieldX + 4; + int textY = fieldY + (fieldHeight - font.FONT_HEIGHT) / 2; + int visibleWidth = fieldWidth - 8; + + // Calculate scroll offset to keep cursor visible + int cursorPosX = font.getStringWidth(text.substring(0, Math.min(cursor, text.length()))); + if (cursorPosX - scrollOffset > visibleWidth - 2) { + scrollOffset = cursorPosX - visibleWidth + 2; + } else if (cursorPosX - scrollOffset < 0) { + scrollOffset = Math.max(0, cursorPosX); + } + // Update stored scroll offset + if (isSearchField) { + searchScrollOffset = scrollOffset; + } else { + replaceScrollOffset = scrollOffset; + } + + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight); + + { + // Draw selection highlight + if (selStart != selEnd && focused) { + int startX = textX + font.getStringWidth(text.substring(0, Math.min(selStart, text.length()))) - scrollOffset; + int endX = textX + font.getStringWidth(text.substring(0, Math.min(selEnd, text.length()))) - scrollOffset; + Gui.drawRect(startX, textY - 1, endX, textY + font.FONT_HEIGHT, 0xFF2d5ca6); + } + + // Draw text with scroll offset + boolean noMatches = !text.isEmpty() && matches.isEmpty(); + int col = text.isEmpty() ? 0xFFa0a0a0 : 0xFFe0e0e0; + if (isSearchField && noMatches) + col = 0xFFff6666; // Red + + font.drawString(text.isEmpty() ? placeholder : text, textX - scrollOffset, textY, col); + + // Draw cursor (stays visible during typing) + if (focused && shouldShowCursor()) { + int cursorDrawX = textX + cursorPosX - scrollOffset; + Gui.drawRect(cursorDrawX, textY - 1, cursorDrawX + 1, textY + font.FONT_HEIGHT, 0xFFffffff); + } + } + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + } + + private void drawToggleButton(int btnX, int btnY, String label, boolean active, boolean hovered, String tooltip) { + int bgColor = active ? 0xFF4a6da7 : (hovered ? 0xFF505050 : 0xFF3c3c3c); + Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); + Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + 1, 0xFF606060); + Gui.drawRect(btnX, btnY + buttonSize - 1, btnX + buttonSize, btnY + buttonSize, 0xFF2a2a2a); + + int textColor = active ? 0xFFffffff : (hovered ? 0xFFcccccc : 0xFF999999); + int labelWidth = font.getStringWidth(label); + font.drawString(label, btnX + (buttonSize - labelWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2 + 1, textColor); + } + + private void drawNavButton(int btnX, int btnY, String arrow, boolean enabled, boolean hovered) { + int bgColor = !enabled ? 0xFF393939 : (hovered ? 0xFF505050 : 0xFF3c3c3c); + Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); + Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + 1, 0xFF606060); + + + int textColor = !enabled ? 0xFF555555 : (hovered ? 0xFFffffff : 0xFFaaaaaa); + int arrowWidth = font.getStringWidth(arrow); + font.drawString(arrow, btnX + (buttonSize - arrowWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2, textColor); + } + + private void drawCloseButton(int btnX, int btnY, boolean hovered) { + int bgColor = hovered ? 0xFFc75050 : 0xFF3c3c3c; + Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); + + int textColor = hovered ? 0xFFffffff : 0xFFaaaaaa; + String xChar = "X"; + int xWidth = font.getStringWidth(xChar); + font.drawString(xChar, btnX + (buttonSize - xWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2 + 1, textColor); + } + + private void drawActionButton(int btnX, int btnY, String label, boolean hovered, boolean enabled) { + int btnWidth = font.getStringWidth(label) + 10; + int btnHeight = 14; + + int bgColor = !enabled ? 0xFF393939 : (hovered ? 0xFF505050 : 0xFF3c3c3c); + Gui.drawRect(btnX, btnY + 1, btnX + btnWidth, btnY + btnHeight + 1, bgColor); + Gui.drawRect(btnX, btnY + 1, btnX + btnWidth, btnY + 2, 0xFF555555); + Gui.drawRect(btnX, btnY + btnHeight, btnX + btnWidth, btnY + btnHeight + 1, 0xFF2a2a2a); + + int textColor = !enabled ? 0xFF555555 : (hovered ? 0xFFffffff : 0xFFaaaaaa); + font.drawString(label, btnX + 5, btnY + (btnHeight - font.FONT_HEIGHT) / 2 + 2, textColor); + } + + // ==================== HOVER DETECTION ==================== + + private void updateHoverStates(int mouseX, int mouseY) { + int rowY = y + padding; + int currentX = x + padding; + + int searchFieldEndX = currentX + textFieldWidth; + currentX = searchFieldEndX + buttonSpacing + 4; + + String matchInfo = matches.isEmpty() ? "0 results" : (currentMatchIndex + 1) + "/" + matches.size(); + currentX += font.getStringWidth(matchInfo) + 8; + + int matchCaseX = currentX; + hoverMatchCase = isMouseOver(mouseX, mouseY, matchCaseX, rowY, buttonSize, buttonSize); + currentX += buttonSize + buttonSpacing; + + int wholeWordX = currentX; + hoverWholeWord = isMouseOver(mouseX, mouseY, wholeWordX, rowY, buttonSize, buttonSize); + currentX += buttonSize + buttonSpacing + 4; + + int prevX = currentX; + hoverPrev = isMouseOver(mouseX, mouseY, prevX, rowY, buttonSize, buttonSize); + currentX += buttonSize + buttonSpacing; + + int nextX = currentX; + hoverNext = isMouseOver(mouseX, mouseY, nextX, rowY, buttonSize, buttonSize); + + int closeX = x + width - buttonSize - padding; + hoverClose = isMouseOver(mouseX, mouseY, closeX, rowY, buttonSize, buttonSize); + + int replaceUnwindButton = closeX - buttonSize - buttonSpacing; + hoverSearchUnwind = isMouseOver(mouseX, mouseY, replaceUnwindButton, rowY, buttonSize, buttonSize); + + if (showReplace) { + int replaceRowY = y + barHeight + 2; + currentX = x + padding + textFieldWidth + buttonSpacing + 4; + + int replaceWidth = font.getStringWidth("Replace") + 12; + hoverReplace = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, replaceWidth, 14); + currentX += replaceWidth + buttonSpacing; + + int replaceAllWidth = font.getStringWidth("Replace All") + 12; + hoverReplaceAll = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, replaceAllWidth, 14); + currentX += replaceAllWidth + buttonSpacing; + + int excludeWidth = font.getStringWidth("Exclude") + 12; + hoverExclude = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, excludeWidth, 14); + } else { + hoverReplace = hoverReplaceAll = hoverExclude = false; + } + } + + private boolean isMouseOver(int mouseX, int mouseY, int bx, int by, int bw, int bh) { + return mouseX >= bx && mouseX < bx + bw && mouseY >= by && mouseY < by + bh; + } + + public void resetSelection() { + searchSelectionStart = searchSelectionEnd = searchCursor; + replaceSelectionStart = replaceSelectionEnd = replaceCursor; + } + // ==================== MOUSE INPUT ==================== + + /** + * Handle mouse click - supports double-click for word selection, triple-click for all + * + * @return true if click was consumed + */ + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (!visible) return false; + if (button != 0) return false; // Only handle left clicks + + int totalHeight = getTotalHeight(); + // If click is outside the bar, don't consume it (let main editor handle) + if (mouseX < x || mouseX > x + width || mouseY < y || mouseY > y + totalHeight) { + resetSelection(); + return false; + } + + int rowY = y + padding; + int currentX = x + padding; + + // Handle click timing for double/triple click + long now = System.currentTimeMillis(); + boolean isSearchFieldClick = isMouseOver(mouseX, mouseY, currentX, rowY - 2, textFieldWidth, textFieldHeight + 4); + boolean isReplaceFieldClick = showReplace && isMouseOver(mouseX, mouseY, x + padding, y + barHeight, textFieldWidth, textFieldHeight + 4); + + if ((isSearchFieldClick || isReplaceFieldClick) && callback != null) callback.unfocusMainEditor(); + + if (isSearchFieldClick) { + boolean sameField = lastClickWasSearch; + if (sameField && now - lastClickTime < 300) { + clickCount++; + } else { + clickCount = 1; + } + lastClickTime = now; + lastClickWasSearch = true; + + searchFieldFocused = true; + replaceFieldFocused = false; + markActivity(); + + if (clickCount == 1) { + // Single click - position cursor + int clickX = mouseX - currentX - 4 + searchScrollOffset; + searchCursor = getTextCursorPosition(searchText, clickX); + searchSelectionStart = searchSelectionEnd = searchCursor; + } else if (clickCount == 2) { + // Double click - select word + selectWordAt(searchText, getTextCursorPosition(searchText, mouseX - currentX - 4 + searchScrollOffset), true); + } else if (clickCount >= 3) { + // Triple click - select all + searchSelectionStart = 0; + searchSelectionEnd = searchText.length(); + searchCursor = searchText.length(); + clickCount = 0; + } + return true; + } + currentX += textFieldWidth + buttonSpacing + 4; + + String matchInfo = matches.isEmpty() ? "0 results" : (currentMatchIndex + 1) + "/" + matches.size(); + currentX += font.getStringWidth(matchInfo) + 8; + + if (hoverMatchCase) { + matchCase = !matchCase; + updateMatches(); + return true; + } + currentX += buttonSize + buttonSpacing; + + if (hoverWholeWord) { + wholeWord = !wholeWord; + updateMatches(); + return true; + } + currentX += buttonSize + buttonSpacing + 4; + + if (hoverPrev && !matches.isEmpty()) { + goToPreviousMatch(); + return true; + } + currentX += buttonSize + buttonSpacing; + + if (hoverNext && !matches.isEmpty()) { + goToNextMatch(); + return true; + } + + if (hoverClose) { + close(); + return true; + } + + if (hoverSearchUnwind) { + showReplace = !showReplace; + return true; + } + + if (showReplace) { + int replaceRowY = y + barHeight + 2; + + if (isReplaceFieldClick || isMouseOver(mouseX, mouseY, x + padding, replaceRowY - 2, textFieldWidth, textFieldHeight + 4)) { + // Handle click timing for replace field + boolean sameField = !lastClickWasSearch; + if (sameField && now - lastClickTime < 300) { + clickCount++; + } else { + clickCount = 1; + } + lastClickTime = now; + lastClickWasSearch = false; + + searchFieldFocused = false; + replaceFieldFocused = true; + markActivity(); + + if (clickCount == 1) { + int clickX = mouseX - x - padding - 4 + replaceScrollOffset; + replaceCursor = getTextCursorPosition(replaceText, clickX); + replaceSelectionStart = replaceSelectionEnd = replaceCursor; + } else if (clickCount == 2) { + selectWordAt(replaceText, getTextCursorPosition(replaceText, mouseX - x - padding - 4 + replaceScrollOffset), false); + } else if (clickCount >= 3) { + replaceSelectionStart = 0; + replaceSelectionEnd = replaceText.length(); + replaceCursor = replaceText.length(); + clickCount = 0; + } + return true; + } + + if (hoverReplace && !matches.isEmpty()) { + replaceCurrent(); + return true; + } + + if (hoverReplaceAll && !matches.isEmpty()) { + replaceAll(); + return true; + } + + if (hoverExclude && currentMatchIndex >= 0) { + excludeCurrentMatch(); + return true; + } + } + + // Click was within bar bounds but not on any control - consume it to prevent MTA from getting focus + return true; + } + + private int getTextCursorPosition(String text, int clickX) { + if (text.isEmpty() || clickX <= 0) return 0; + + for (int i = 1; i <= text.length(); i++) { + int w = font.getStringWidth(text.substring(0, i)); + if (clickX < w) { + int prevW = font.getStringWidth(text.substring(0, i - 1)); + return clickX < (prevW + w) / 2 ? i - 1 : i; + } + } + return text.length(); + } + + /** + * Select word at cursor position (for double-click) + */ + private void selectWordAt(String text, int pos, boolean isSearchField) { + if (text.isEmpty()) return; + + // Find word boundaries + int start = pos; + int end = pos; + + // Expand left + while (start > 0 && isWordChar(text.charAt(start - 1))) { + start--; + } + // Expand right + while (end < text.length() && isWordChar(text.charAt(end))) { + end++; + } + + if (isSearchField) { + searchSelectionStart = start; + searchSelectionEnd = end; + searchCursor = end; + } else { + replaceSelectionStart = start; + replaceSelectionEnd = end; + replaceCursor = end; + } + } + + private boolean isWordChar(char c) { + return Character.isLetterOrDigit(c) || c == '_'; + } + + // ==================== KEYBOARD INPUT ==================== + + /** + * Handle keyboard input + * + * @return true if input was consumed + */ + public boolean keyTyped(char c, int keyCode) { + if (keyCode == Keyboard.KEY_ESCAPE && visible) { + close(); + return true; + } + + if (!visible || (!searchFieldFocused && !replaceFieldFocused)) return false; + + // Check modifier keys - don't let them trigger first-match issue + boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); + boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + boolean alt = Keyboard.isKeyDown(Keyboard.KEY_LMENU) || Keyboard.isKeyDown(Keyboard.KEY_RMENU); + + // Ignore lone modifier key presses + if (keyCode == Keyboard.KEY_LCONTROL || keyCode == Keyboard.KEY_RCONTROL || + keyCode == Keyboard.KEY_LSHIFT || keyCode == Keyboard.KEY_RSHIFT || + keyCode == Keyboard.KEY_LMENU || keyCode == Keyboard.KEY_RMENU) { + return true; // Consume but don't process + } + + if (keyCode == Keyboard.KEY_TAB) { + if (showReplace) { + searchFieldFocused = !searchFieldFocused; + replaceFieldFocused = !replaceFieldFocused; + } else if (searchFieldFocused && callback != null) { + searchFieldFocused = false; + callback.focusMainEditor(); + } + markActivity(); + return true; + } + + // Enter/Shift+Enter for next/previous match + if (keyCode == Keyboard.KEY_RETURN) { + if (shift) { + // Shift+Enter = previous match + if (!matches.isEmpty()) { + goToPreviousMatch(); + } + } else if (replaceFieldFocused && !matches.isEmpty()) { + replaceCurrent(); + } else if (!matches.isEmpty()) { + goToNextMatch(); + } + return true; + } + + // Up/Down arrows cycle through matches when SRB is focused + if (keyCode == Keyboard.KEY_UP && !matches.isEmpty()) { + goToPreviousMatch(); + return true; + } + if (keyCode == Keyboard.KEY_DOWN && !matches.isEmpty()) { + goToNextMatch(); + return true; + } + + if (searchFieldFocused) { + handleTextFieldInput(c, keyCode, true); + } else if (replaceFieldFocused) { + handleTextFieldInput(c, keyCode, false); + } + + return true; + } + + private void handleTextFieldInput(char c, int keyCode, boolean isSearchField) { + String text = isSearchField ? searchText : replaceText; + int cursor = isSearchField ? searchCursor : replaceCursor; + int selStart = isSearchField ? searchSelectionStart : replaceSelectionStart; + int selEnd = isSearchField ? searchSelectionEnd : replaceSelectionEnd; + + boolean hasSelection = selStart != selEnd; + boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); + boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + + if (ctrl && keyCode == Keyboard.KEY_A) { + selStart = 0; + selEnd = text.length(); + cursor = text.length(); + } else if (ctrl && keyCode == Keyboard.KEY_C) { + if (hasSelection) { + String selected = text.substring(Math.min(selStart, selEnd), Math.max(selStart, selEnd)); + setClipboard(selected); + } + } else if (ctrl && keyCode == Keyboard.KEY_X) { + if (hasSelection) { + String selected = text.substring(Math.min(selStart, selEnd), Math.max(selStart, selEnd)); + setClipboard(selected); + saveState(isSearchField); + text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); + cursor = Math.min(selStart, selEnd); + selStart = selEnd = cursor; + } + } else if (ctrl && keyCode == Keyboard.KEY_V) { + String clipboard = getClipboard(); + if (clipboard != null) { + clipboard = clipboard.replace("\n", "").replace("\r", ""); + saveState(isSearchField); + if (hasSelection) { + text = text.substring(0, Math.min(selStart, selEnd)) + clipboard + text.substring(Math.max(selStart, selEnd)); + cursor = Math.min(selStart, selEnd) + clipboard.length(); + } else { + text = text.substring(0, cursor) + clipboard + text.substring(cursor); + cursor += clipboard.length(); + } + selStart = selEnd = cursor; + } + } else if (keyCode == Keyboard.KEY_BACK) { + saveState(isSearchField); + if (hasSelection) { + text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); + cursor = Math.min(selStart, selEnd); + } else if (cursor > 0) { + text = text.substring(0, cursor - 1) + text.substring(cursor); + cursor--; + } + selStart = selEnd = cursor; + } else if (keyCode == Keyboard.KEY_DELETE) { + saveState(isSearchField); + if (hasSelection) { + text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); + cursor = Math.min(selStart, selEnd); + } else if (cursor < text.length()) { + text = text.substring(0, cursor) + text.substring(cursor + 1); + } + selStart = selEnd = cursor; + } else if (keyCode == Keyboard.KEY_LEFT) { + if (shift) { + if (cursor > 0) { + cursor--; + if (selStart == selEnd) { + selEnd = cursor + 1; + selStart = cursor; + } else if (cursor < selStart) { + selStart = cursor; + } else { + selEnd = cursor; + } + } + } else { + if (hasSelection) { + cursor = Math.min(selStart, selEnd); + } else if (cursor > 0) { + cursor--; + } + selStart = selEnd = cursor; + } + } else if (keyCode == Keyboard.KEY_RIGHT) { + if (shift) { + if (cursor < text.length()) { + cursor++; + if (selStart == selEnd) { + selStart = cursor - 1; + selEnd = cursor; + } else if (cursor > selEnd) { + selEnd = cursor; + } else { + selStart = cursor; + } + } + } else { + if (hasSelection) { + cursor = Math.max(selStart, selEnd); + } else if (cursor < text.length()) { + cursor++; + } + selStart = selEnd = cursor; + } + } else if (keyCode == Keyboard.KEY_HOME) { + if (shift) { + selEnd = selStart == selEnd ? cursor : selEnd; + selStart = 0; + } else { + selStart = selEnd = 0; + } + cursor = 0; + } else if (keyCode == Keyboard.KEY_END) { + if (shift) { + selStart = selStart == selEnd ? cursor : selStart; + selEnd = text.length(); + } else { + selStart = selEnd = text.length(); + } + cursor = text.length(); + } else if (ChatAllowedCharacters.isAllowedCharacter(c)) { + saveState(isSearchField); + if (hasSelection) { + text = text.substring(0, Math.min(selStart, selEnd)) + c + text.substring(Math.max(selStart, selEnd)); + cursor = Math.min(selStart, selEnd) + 1; + } else { + text = text.substring(0, cursor) + c + text.substring(cursor); + cursor++; + } + selStart = selEnd = cursor; + } + + if (selStart > selEnd) { + int temp = selStart; + selStart = selEnd; + selEnd = temp; + } + + // Mark activity on any input to keep cursor visible + markActivity(); + + if (isSearchField) { + searchText = text; + searchCursor = cursor; + searchSelectionStart = selStart; + searchSelectionEnd = selEnd; + updateMatches(); + } else { + replaceText = text; + replaceCursor = cursor; + replaceSelectionStart = selStart; + replaceSelectionEnd = selEnd; + } + } + + private void setClipboard(String text) { + try { + java.awt.datatransfer.StringSelection selection = new java.awt.datatransfer.StringSelection(text); + java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null); + } catch (Exception ignored) { + } + } + + private String getClipboard() { + try { + return (String) java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().getData(java.awt.datatransfer.DataFlavor.stringFlavor); + } catch (Exception e) { + return null; + } + } + + // ==================== SEARCH LOGIC ==================== + + /** + * Update matches based on current search text and options + */ + public void updateMatches() { + matches.clear(); + excludedMatches.clear(); + + if (callback == null || searchText.isEmpty()) { + if (callback != null) callback.onMatchesUpdated(); + return; + } + + String sourceText = callback.getText(); + if (sourceText == null || sourceText.isEmpty()) { + callback.onMatchesUpdated(); + return; + } + + String searchPattern = searchText; + + if (wholeWord) { + searchPattern = "\\b" + Pattern.quote(searchPattern) + "\\b"; + } else { + searchPattern = Pattern.quote(searchPattern); + } + + int flags = matchCase ? 0 : Pattern.CASE_INSENSITIVE; + + try { + Pattern pattern = Pattern.compile(searchPattern, flags); + Matcher matcher = pattern.matcher(sourceText); + + while (matcher.find()) { + matches.add(new int[]{matcher.start(), matcher.end()}); + } + } catch (Exception ignored) { + } + + // No need to navigate right now + if (!matches.isEmpty()) { + // currentMatchIndex = 0; + // navigateToCurrentMatch(); + } + + currentMatchIndex = ValueUtil.clamp(currentMatchIndex, 0, matches.size() - 1); + callback.onMatchesUpdated(); + } + + /** + * If the editor currently has a selection, prefer the match that contains that selection + * and navigate to it. + */ + private void setCurrentMatchToHighlight() { + if (callback == null || matches.isEmpty()) + return; + try { + int selStart = callback.getSelectionStart(); + int selEnd = callback.getSelectionEnd(); + if (selStart >= 0 && selEnd > selStart) { + for (int i = 0; i < matches.size(); i++) { + int[] m = matches.get(i); + if (m[0] <= selStart && m[1] >= selEnd) { + currentMatchIndex = i; + return; + } + } + } + } catch (Exception ignored) { + } + } + + /** + * Navigate to next match + */ + public void goToNextMatch() { + if (matches.isEmpty()) return; + + if (currentMatchIndex < 0) currentMatchIndex = 0; + else currentMatchIndex = (currentMatchIndex + 1) % matches.size(); + + navigateToCurrentMatch(); + } + + /** + * Navigate to previous match + */ + public void goToPreviousMatch() { + if (matches.isEmpty()) return; + + if (currentMatchIndex < 0) currentMatchIndex = matches.size() - 1; + else currentMatchIndex = (currentMatchIndex - 1 + matches.size()) % matches.size(); + + navigateToCurrentMatch(); + } + + /** + * Navigate viewport to current match and select it + */ + private void navigateToCurrentMatch() { + if (callback == null || currentMatchIndex < 0 || currentMatchIndex >= matches.size()) return; + + int[] match = matches.get(currentMatchIndex); + callback.scrollToPosition(match[0]); + callback.setSelection(match[0], match[1]); + } + + // ==================== REPLACE LOGIC ==================== + + /** + * Replace current match + */ + public void replaceCurrent() { + if (callback == null || currentMatchIndex < 0 || matches.isEmpty()) return; + if (excludedMatches.contains(currentMatchIndex)) { + goToNextMatch(); + return; + } + + String sourceText = callback.getText(); + int[] match = matches.get(currentMatchIndex); + + String newText = sourceText.substring(0, match[0]) + replaceText + sourceText.substring(match[1]); + callback.setText(newText); + + updateMatches(); + + if (!matches.isEmpty()) { + int replacementEnd = match[0] + replaceText.length(); + for (int i = 0; i < matches.size(); i++) { + if (matches.get(i)[0] >= replacementEnd) { + currentMatchIndex = i; + break; + } + } + navigateToCurrentMatch(); + } + } + + /** + * Replace all matches + */ + public void replaceAll() { + if (callback == null || matches.isEmpty()) return; + + String sourceText = callback.getText(); + StringBuilder result = new StringBuilder(); + int lastEnd = 0; + + for (int i = 0; i < matches.size(); i++) { + if (excludedMatches.contains(i)) { + continue; + } + int[] match = matches.get(i); + result.append(sourceText, lastEnd, match[0]); + result.append(replaceText); + lastEnd = match[1]; + } + result.append(sourceText.substring(lastEnd)); + + callback.setText(result.toString()); + updateMatches(); + } + + /** + * Exclude current match from replacement + */ + public void excludeCurrentMatch() { + if (currentMatchIndex < 0) return; + + if (excludedMatches.contains(currentMatchIndex)) { + // If already excluded, remove the exclusion and keep selection on this match + excludedMatches.remove(Integer.valueOf(currentMatchIndex)); + } else { + // Otherwise exclude and move to the next match + excludedMatches.add(currentMatchIndex); + goToNextMatch(); + } + } + + // ==================== UTILITY ==================== + + /** + * Get search text for external highlighting + */ + public String getSearchText() { + return searchText; + } + + /** + * Check if bar consumes click at position + */ + public boolean isMouseOverBar(int mouseX, int mouseY) { + if (!visible) return false; + int totalHeight = getTotalHeight(); + return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + totalHeight; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java new file mode 100644 index 000000000..89efa34cd --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java @@ -0,0 +1,242 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * Manages cursor position, text selection, and related operations. + */ +public class SelectionState { + private int startSelection = 0; + private int endSelection = 0; + private int cursorPosition = 0; + private long lastInputTime = 0L; + + /** + * Reset all cursor selections to a single position + */ + public void reset(int position) { + startSelection = endSelection = cursorPosition = position; + } + + /** + * Clamp all selection bounds to valid text range + */ + public void clamp(int textLength) { + if (textLength <= 0) { + startSelection = endSelection = cursorPosition = 0; + return; + } + startSelection = Math.max(0, Math.min(startSelection, textLength)); + endSelection = Math.max(0, Math.min(endSelection, textLength)); + cursorPosition = Math.max(0, Math.min(cursorPosition, textLength)); + } + + /** + * Set cursor position, optionally extending selection + */ + public void setCursor(int position, int textLength, boolean extendSelection) { + position = Math.max(0, Math.min(position, textLength)); + + if (position != cursorPosition) { + if (!extendSelection) { + endSelection = startSelection = cursorPosition = position; + } else { + int diff = cursorPosition - position; + if (cursorPosition == startSelection) { + startSelection -= diff; + } else if (cursorPosition == endSelection) { + endSelection -= diff; + } + + // Ensure start <= end + if (startSelection > endSelection) { + int temp = endSelection; + endSelection = startSelection; + startSelection = temp; + } + cursorPosition = position; + } + clamp(textLength); + markActivity(); + } + } + + /** + * Select a range of text + */ + public void setSelection(int start, int end) { + startSelection = Math.min(start, end); + endSelection = Math.max(start, end); + } + + /** + * Select all text + */ + public void selectAll(int textLength) { + startSelection = cursorPosition = 0; + endSelection = textLength; + } + + /** + * Check if there's an active selection (not just cursor) + */ + public boolean hasSelection() { + return startSelection != endSelection; + } + + /** + * Get selected text + */ + public String getSelectedText(String text) { + if (!hasSelection() || text == null) return ""; + int start = Math.max(0, Math.min(startSelection, text.length())); + int end = Math.max(0, Math.min(endSelection, text.length())); + return text.substring(start, end); + } + + /** + * Get text before selection/cursor + */ + public String getTextBefore(String text) { + if (text == null || startSelection <= 0) return ""; + return text.substring(0, Math.min(startSelection, text.length())); + } + + /** + * Get text after selection/cursor + */ + public String getTextAfter(String text) { + if (text == null) return ""; + int pos = Math.min(endSelection, text.length()); + return text.substring(pos); + } + + /** + * Find which line the cursor is on (0-indexed) + */ + public int getCursorLineIndex(List lines, int textLength) { + if (lines == null || lines.isEmpty()) return 0; + + for (int i = 0; i < lines.size(); i++) { + LineData ld = lines.get(i); + boolean isLastLine = (i == lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= ld.start && cursorPosition <= ld.end) + : (cursorPosition >= ld.start && cursorPosition < ld.end); + if (isOnLine) { + return i; + } + } + return lines.size() - 1; + } + + /** + * Check if cursor is on a specific line, handling the last line specially + * (last line doesn't have trailing newline, so use <= for end boundary) + */ + public boolean isCursorOnLine(int lineIndex, LineData line, int totalLines) { + boolean isLastLine = (lineIndex == totalLines - 1); + return isLastLine + ? (cursorPosition >= line.start && cursorPosition <= line.end) + : (cursorPosition >= line.start && cursorPosition < line.end); + } + + /** + * Find line containing the cursor + */ + public LineData findCurrentLine(List lines) { + if (lines == null) return null; + for (int i = 0; i < lines.size(); i++) { + LineData line = lines.get(i); + if (isCursorOnLine(i, line, lines.size())) { + return line; + } + } + return null; + } + + /** + * Mark user activity (for cursor blink timing) + */ + public void markActivity() { + lastInputTime = System.currentTimeMillis(); + } + + /** + * Check if there was recent input (for cursor blink pause) + */ + public boolean hadRecentInput() { + return System.currentTimeMillis() - lastInputTime < 500; + } + + // Getters + public int getStartSelection() { + return startSelection; + } + + public int getEndSelection() { + return endSelection; + } + + public int getCursorPosition() { + return cursorPosition; + } + + public long getLastInputTime() { + return lastInputTime; + } + + // Direct setters (use with caution, prefer higher-level methods) + public void setStartSelection(int pos) { + this.startSelection = pos; + } + + public void setEndSelection(int pos) { + this.endSelection = pos; + } + + public void setCursorPositionDirect(int pos) { + this.cursorPosition = pos; + } + + /** + * After text modification, update selection to new position + */ + public void afterTextInsert(int newPosition) { + endSelection = startSelection = cursorPosition = newPosition; + markActivity(); + } + + /** + * Select the word surrounding the current cursor position using the provided regex. + * The regex should match word tokens; the implementation finds the token which + * contains the cursor and selects its start..end. + */ + public void selectWordAtCursor(String text, Pattern wordPattern) { + if (text == null || wordPattern == null) return; + java.util.regex.Matcher m = wordPattern.matcher(text); + while (m.find()) { + if (cursorPosition >= m.start() && cursorPosition <= m.end()) { + setSelection(m.start(), m.end()); + return; + } + } + } + + /** + * Select the entire line containing the cursor. `lines` is the parsed + * line data from `JavaTextContainer` where each LineData has start/end indices. + */ + public void selectLineAtCursor(List lines) { + if (lines == null) return; + for (LineData line : lines) { + if (isCursorOnLine(lines.indexOf(line), line, lines.size())) { + setSelection(line.start, line.end); + return; + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java b/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java new file mode 100644 index 000000000..c5cfd9f77 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java @@ -0,0 +1,126 @@ +package noppes.npcs.client.gui.util.script; + +import java.util.ArrayList; +import java.util.List; + +/** + * Manages undo/redo stacks for the text editor. + */ +public class UndoManager { + + /** + * Stores a snapshot of text state for undo/redo + */ + public static class UndoState { + public final String text; + public final int cursorPosition; + + public UndoState(String text, int cursorPosition) { + this.text = text; + this.cursorPosition = cursorPosition; + } + } + + private final List undoStack = new ArrayList<>(); + private final List redoStack = new ArrayList<>(); + private boolean isUndoing = false; + + // Optional: limit stack size to prevent memory issues + private static final int MAX_STACK_SIZE = 100; + + /** + * Record current state before making a change + */ + public void recordState(String text, int cursorPosition) { + if (isUndoing) return; + + undoStack.add(new UndoState(text, cursorPosition)); + redoStack.clear(); + + // Trim stack if too large + while (undoStack.size() > MAX_STACK_SIZE) { + undoStack.remove(0); + } + } + + /** + * Undo to previous state + * + * @param currentText Current text before undo + * @param currentCursor Current cursor position + * @return Previous state, or null if nothing to undo + */ + public UndoState undo(String currentText, int currentCursor) { + if (undoStack.isEmpty()) return null; + + isUndoing = true; + redoStack.add(new UndoState(currentText, currentCursor)); + UndoState state = undoStack.remove(undoStack.size() - 1); + isUndoing = false; + + return state; + } + + /** + * Redo to next state + * + * @param currentText Current text before redo + * @param currentCursor Current cursor position + * @return Next state, or null if nothing to redo + */ + public UndoState redo(String currentText, int currentCursor) { + if (redoStack.isEmpty()) return null; + + isUndoing = true; + undoStack.add(new UndoState(currentText, currentCursor)); + UndoState state = redoStack.remove(redoStack.size() - 1); + isUndoing = false; + + return state; + } + + /** + * Check if undo is available + */ + public boolean canUndo() { + return !undoStack.isEmpty(); + } + + /** + * Check if redo is available + */ + public boolean canRedo() { + return !redoStack.isEmpty(); + } + + /** + * Clear all history + */ + public void clear() { + undoStack.clear(); + redoStack.clear(); + } + + /** + * Check if currently performing an undo/redo operation + */ + public boolean isUndoing() { + return isUndoing; + } + + /** + * Set undoing flag (for external control during setText) + */ + public void setUndoing(boolean undoing) { + this.isUndoing = undoing; + } + + // For backward compatibility with existing code + public List getUndoList() { + return undoStack; + } + + public List getRedoList() { + return redoStack; + } +} diff --git a/src/main/java/noppes/npcs/client/key/KeyEvent.java b/src/main/java/noppes/npcs/client/key/KeyEvent.java new file mode 100644 index 000000000..1948e2f78 --- /dev/null +++ b/src/main/java/noppes/npcs/client/key/KeyEvent.java @@ -0,0 +1,41 @@ +package noppes.npcs.client.key; + +public class KeyEvent { + public final KeyPreset.PressType type; + public final KeyPreset preset; + public final int keyCode; + public final boolean isMouse; + public final long timestamp; + + public KeyEvent(KeyPreset.PressType type, KeyPreset preset) { + this.type = type; + this.preset = preset; + this.keyCode = preset.keyCode(); + this.isMouse = preset.isMouseKey(); + this.timestamp = System.currentTimeMillis(); + } + + public boolean is(KeyPreset.PressType t) { + return this.type == t; + } + + public boolean isPress() { + return is(KeyPreset.PressType.PRESS); + } + + public boolean isHold() { + return is(KeyPreset.PressType.HOLD); + } + + public boolean isRelease() { + return is(KeyPreset.PressType.RELEASE); + } + + public boolean isSinglePress() { + return is(KeyPreset.PressType.SINGLE_PRESS); + } + + public boolean isPressRelease() { + return is(KeyPreset.PressType.PRESS_RELEASE); + } +} diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java new file mode 100644 index 000000000..e76cf14ff --- /dev/null +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -0,0 +1,288 @@ +package noppes.npcs.client.key; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.settings.GameSettings; +import net.minecraft.nbt.NBTTagCompound; +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; + +import java.util.function.Consumer; + +public class KeyPreset { + /** + * @field SINGLE_PRESS => on releasing before 5 ticks press time + * @field PRESS_RELEASE => on pressing and releasing + */ + /** + * Press types for key events. + */ + public enum PressType {PRESS, HOLD, RELEASE, SINGLE_PRESS, PRESS_RELEASE} + + public KeyState defaultState = new KeyState(); + public KeyState currentState = new KeyState(); + + public String name, description; + public boolean shouldConflict = true; + public Consumer task; + + // Throttling for hold-down behavior + private long lastActionTime = 0; + private long throttleInterval = 25; + private long firstThrottleInterval = 500; + private boolean isFirstAction = true; + + private long pressStartMs = 0; + public boolean isDown; + + + public KeyPreset(String name) { + this.name = name; + } + + public KeyPreset setDefaultState(int keyCode, boolean hasCtrl, boolean hasAlt, boolean hasShift) { + defaultState.setState(keyCode, hasCtrl, hasAlt, hasShift); + currentState.setState(keyCode, hasCtrl, hasAlt, hasShift); + return this; + } + + public KeyPreset setDescription(String description) { + this.description = description; + return this; + } + + public KeyPreset setTask(Consumer task) { + this.task = task; + return this; + } + + public KeyPreset shouldConflict(boolean shouldConflict) { + this.shouldConflict = shouldConflict; + return this; + } + + /** + * Fire every X ms when holding + */ + public KeyPreset setThrottleInterval(long ms) { + this.throttleInterval = ms; + return this; + } + + /** + * Set throttle interval for the first action (initial press) + */ + public KeyPreset setFirstThrottleInterval(long ms) { + this.firstThrottleInterval = ms; + return this; + } + + /** + * Determine whether the given press type should be throttled. Only HOLD events are throttled. + */ + public boolean shouldThrottle(PressType pressType) { + if (throttleInterval <= 0) return false; + long currentTime = System.currentTimeMillis(); + long intervalToUse = pressType == PressType.HOLD && isFirstAction && firstThrottleInterval > 0 ? + firstThrottleInterval : throttleInterval; + if (currentTime - lastActionTime < intervalToUse) return true; + lastActionTime = currentTime; + isFirstAction = false; + return false; + } + + public void tick() { + int keyCode = keyCode(); + if (keyCode == -1 || keyCode == 0) + return; + + boolean isDown = isMouseKey() ? Mouse.isButtonDown(keyCode + 100) : Keyboard.isKeyDown(keyCode); + isDown = isDown && (isCtrlKeyDown() == hasCtrl()) && (isAltKeyDown() == hasAlt()) && (isShiftKeyDown() == hasShift()); + setDown(isDown); + } + + private static final long SHORT_PRESS_MS = 250L; + + public void setDown(boolean down) { + if (down && !isDown) { // just pressed + pressStartMs = System.currentTimeMillis(); + onAction(PressType.PRESS, PressType.PRESS_RELEASE); + isFirstAction = true; // Reset for new press sequence + } + + if (!down && isDown) { // just released + if (getPressTime() <= SHORT_PRESS_MS) + onAction(PressType.SINGLE_PRESS); + onAction(PressType.RELEASE, PressType.PRESS_RELEASE); + pressStartMs = 0; + } + + if (down) // while held + onAction(PressType.HOLD); + + this.isDown = down; + } + + public long getPressTime() { + return pressStartMs > 0 ? (System.currentTimeMillis() - pressStartMs) : 0L; + } + + public KeyPreset onAction(PressType... pressTypes) { + if (task == null) + return this; + + for (PressType pressType : pressTypes) + if (!shouldThrottle(pressType)) + this.task.accept(new KeyEvent(pressType, this)); + + return this; + } + + public boolean isDefault() { + return currentState.equals(defaultState); + } + + public boolean isMouseKey() { + return keyCode() < -1; + } + + public int keyCode() { + return currentState.keyCode; + } + + public boolean hasCtrl() { + return currentState.hasCtrl; + } + + public boolean hasAlt() { + return currentState.hasAlt; + } + + public boolean hasShift() { + return currentState.hasShift; + } + + public String getKeyName() { + return currentState.getName(); + } + + public void clear() { + currentState.clear(); + } + + public void writeToNbt(NBTTagCompound c) { + c.setTag(name, currentState.writeToNbt()); + } + + public void readFromNbt(NBTTagCompound compound) { + currentState.readFromNbt(compound.getCompoundTag(name)); + } + + public boolean equals(Object preset) { + if (preset == this) + return true; + + if (preset instanceof KeyPreset) { + KeyPreset key = (KeyPreset) preset; + return key.currentState.equals(currentState); + } + + return false; + } + + public static class KeyState { + public int keyCode = -1; + public boolean hasCtrl, hasAlt, hasShift; + + public void setState(int keyCode, boolean hasCtrl, boolean hasAlt, boolean hasShift) { + this.keyCode = keyCode; + this.hasCtrl = hasCtrl; + this.hasAlt = hasAlt; + this.hasShift = hasShift; + } + + public void readFrom(KeyState state) { + keyCode = state.keyCode; + hasCtrl = state.hasCtrl; + hasShift = state.hasShift; + hasAlt = state.hasAlt; + } + + public void writeTo(KeyState state) { + state.setState(keyCode, hasCtrl, hasAlt, hasShift); + } + + public boolean hasState() { + return keyCode != -1; + } + + public void clear() { + this.keyCode = -1; + hasCtrl = hasAlt = hasShift = false; + } + + public boolean equals(Object preset) { + if (preset == this) + return true; + + if (preset instanceof KeyState) { + KeyState state = (KeyState) preset; + return state.keyCode == keyCode && state.hasCtrl == hasCtrl && state.hasAlt == hasAlt && state.hasShift == hasShift; + } + + return false; + } + + public NBTTagCompound writeToNbt() { + NBTTagCompound compound = new NBTTagCompound(); + compound.setInteger("keyCode", keyCode); + compound.setBoolean("hasCtrl", hasCtrl); + compound.setBoolean("hasShift", hasShift); + compound.setBoolean("hasAlt", hasAlt); + return compound; + } + + public void readFromNbt(NBTTagCompound compound) { + this.keyCode = compound.getInteger("keyCode"); + this.hasCtrl = compound.getBoolean("hasCtrl"); + this.hasShift = compound.getBoolean("hasShift"); + this.hasAlt = compound.getBoolean("hasAlt"); + } + + public String getName() { + int code = keyCode; + String name = ""; + + if (code == LEFT_MOUSE) + name = "Left Mouse"; + else if (code == RIGHT_MOUSE) + name = "Right Mouse"; + else if (code == MIDDLE_MOUSE) + name = "Middle Mouse"; + else { + name = code == -1 ? "" : GameSettings.getKeyDisplayString(code); + if (name.contains("Button")) + name = name.replace("Button", "Mouse"); + } + + return (hasCtrl ? "CTRL " : "") + (hasAlt ? "ALT " : "") + (hasShift ? "SHIFT " : "") + name; + } + } + + public static final int LEFT_MOUSE = -100, RIGHT_MOUSE = -99, MIDDLE_MOUSE = -98, MOUSE_4 = -97, MOUSE_5 = -96; + + public static boolean isCtrlKeyDown() { + return Minecraft.isRunningOnMac ? Keyboard.isKeyDown(219) || Keyboard.isKeyDown(220) : Keyboard.isKeyDown(29) || Keyboard.isKeyDown(157); + } + + public static boolean isAltKeyDown() { + return Keyboard.isKeyDown(56) || Keyboard.isKeyDown(184); + } + + public static boolean isShiftKeyDown() { + return Keyboard.isKeyDown(42) || Keyboard.isKeyDown(54); + } + + public static boolean isNotCtrlAltShift(int key) { + return key != 219 && key != 220 && key != 29 && key != 157 && key != 42 && key != 54 && key != 56 && key != 184; + } +} diff --git a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java new file mode 100644 index 000000000..1a978cdc7 --- /dev/null +++ b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java @@ -0,0 +1,78 @@ +package noppes.npcs.client.key; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class KeyPresetManager { + public List keys = new ArrayList<>(); + public String fileName; + + + public KeyPresetManager(String fileName) { + this.fileName = fileName; + } + + public KeyPreset add(String name) { + KeyPreset preset = new KeyPreset(name); + keys.add(preset); + return preset; + } + + public void tick() { + for (KeyPreset key : keys) + key.tick(); + } + + + public void load() { + try { + File dir = getDir(); + File file = new File(dir, fileName + ".json"); + if (!file.exists()) + return; + + NBTTagCompound compound = NBTJsonUtil.LoadFile(file); + for (KeyPreset key : keys) + key.readFromNbt(compound); + + } catch (Exception e) { + LogWriter.except(e); + } + } + + public void save() { + try { + File dir = getDir(); + String filename = fileName + ".json"; + + File newFile = new File(dir, filename + "_new"); + File originalFile = new File(dir, filename); + + NBTTagCompound compound = new NBTTagCompound(); + for (KeyPreset key : keys) + key.writeToNbt(compound); + + NBTJsonUtil.SaveFile(newFile, compound); + + if (originalFile.exists()) + originalFile.delete(); + + newFile.renameTo(originalFile); + } catch (Exception e) { + LogWriter.except(e); + } + } + + public File getDir() { + File dir = new File(CustomNpcs.Dir, "keypresets"); + if (!dir.exists()) + dir.mkdir(); + return dir; + } +} diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java new file mode 100644 index 000000000..4a9e294d0 --- /dev/null +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -0,0 +1,39 @@ +package noppes.npcs.client.key.impl; + +import noppes.npcs.client.key.KeyPreset; +import noppes.npcs.client.key.KeyPresetManager; +import org.lwjgl.input.Keyboard; + +public class ScriptEditorKeys extends KeyPresetManager { + + /// ///////////////// + // Editor shortcuts (mapped from GuiScriptTextArea.handleShortcutKeys) + public final KeyPreset COPY = add("Copy").setDefaultState(Keyboard.KEY_C, true, false, false); + public final KeyPreset PASTE = add("Paste").setDefaultState(Keyboard.KEY_V, true, false, false); + public final KeyPreset CUT = add("Cut").setDefaultState(Keyboard.KEY_X, true, false, false); + public final KeyPreset DUPLICATE = add("Duplicate").setDefaultState(Keyboard.KEY_D, true, false, false); + public final KeyPreset UNDO = add("Undo").setDefaultState(Keyboard.KEY_Z, true, false, false); + public final KeyPreset REDO = add("Redo").setDefaultState(Keyboard.KEY_Y, true, false, false); + + public final KeyPreset FORMAT = add("Format Code").setDefaultState(Keyboard.KEY_F, false, true, false); + public final KeyPreset TOGGLE_COMMENT = add("Toggle Comment").setDefaultState(Keyboard.KEY_SLASH, true, false, false); + + // Search/Replace + public final KeyPreset SEARCH = add("Search").setDefaultState(Keyboard.KEY_F, true, false, false); + public final KeyPreset SEARCH_REPLACE = add("Replace").setDefaultState(Keyboard.KEY_F, true, false, true); + + // Navigation + public final KeyPreset GO_TO_LINE = add("Go to Line").setDefaultState(Keyboard.KEY_G, true, false, false); + + // Refactoring + public final KeyPreset RENAME = add("Rename").setDefaultState(Keyboard.KEY_R, true, false, false); + + // View + public final KeyPreset FULLSCREEN = add("Toggle Fullscreen").setDefaultState(Keyboard.KEY_F11, false, false, false); + + public ScriptEditorKeys() { + super("script_editor"); + load(); + } +} + diff --git a/src/main/java/noppes/npcs/client/model/ModelPony.java b/src/main/java/noppes/npcs/client/model/ModelPony.java index b0d23b40a..203a1d12b 100644 --- a/src/main/java/noppes/npcs/client/model/ModelPony.java +++ b/src/main/java/noppes/npcs/client/model/ModelPony.java @@ -17,7 +17,7 @@ import noppes.npcs.entity.EntityNpcPony; import org.lwjgl.opengl.GL11; import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Color; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java b/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java index 252f0af65..90172ce6f 100644 --- a/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java +++ b/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java @@ -2,7 +2,7 @@ package noppes.npcs.client.renderer; import net.minecraft.client.renderer.ImageBufferDownload; -import java.awt.*; +import java.awt.Graphics; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; diff --git a/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java b/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java index a82ba93a8..b018adf60 100644 --- a/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java +++ b/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java @@ -25,7 +25,7 @@ import noppes.npcs.entity.EntityNpcPony; import org.lwjgl.opengl.GL11; import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Color; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java b/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java index 64b37340f..7f9fdd19b 100644 --- a/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java +++ b/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java @@ -26,8 +26,8 @@ public class BlockBeamRenderer extends BlockRendererInterface { public void renderTileEntityAt(TileEntity var1, double var2, double var4, double var6, float var8) { TileVariant tile = (TileVariant) var1; - GL11.glDisable(GL12.GL_RESCALE_NORMAL); GL11.glPushMatrix(); + GL11.glDisable(GL12.GL_RESCALE_NORMAL); GL11.glTranslatef((float) var2 + 0.5f, (float) var4 + 1.5f, (float) var6 + 0.5f); //GL11.glScalef(1.2f, 1.1f, 1.2f); GL11.glRotatef(180, 0, 0, 1); diff --git a/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java b/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java index 9aa8cdcaa..5be3a9492 100644 --- a/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java +++ b/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java @@ -23,7 +23,7 @@ import noppes.npcs.blocks.tiles.TileScripted.TextPlane; import noppes.npcs.client.TextBlockClient; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.Color; import java.util.Random; public class BlockScriptedRenderer extends BlockRendererInterface { diff --git a/src/main/java/noppes/npcs/client/util/Color.java b/src/main/java/noppes/npcs/client/util/Color.java new file mode 100644 index 000000000..75450aeee --- /dev/null +++ b/src/main/java/noppes/npcs/client/util/Color.java @@ -0,0 +1,114 @@ +package noppes.npcs.client.util; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.nbt.NBTTagCompound; +import org.lwjgl.opengl.GL11; + +public class Color { + public int color; + public float alpha; + + public Color(int color) { + setColor(color, 1); + } + + public Color(int color, float alpha) { + setColor(color, alpha); + } + + public Color(int red, int green, int blue, float alpha) { + setColor(red, green, blue, alpha); + } + + public void setColor(int color, float alpha) { + this.color = color; + this.alpha = alpha; + } + + public void setColor(int red, int green, int blue, float alpha) { + this.color = (red << 16) + (green << 8) + blue; + this.alpha = alpha; + } + + public static Color lerpRGBA(Color color1, Color color2, float fraction) { + fraction = Math.min(fraction, 1f); + int red = (int) (color1.getRed() + ((color2.getRed() - color1.getRed()) * fraction)); + int green = (int) (color1.getGreen() + ((color2.getGreen() - color1.getGreen()) * fraction)); + int blue = (int) (color1.getBlue() + ((color2.getBlue() - color1.getBlue()) * fraction)); + float newAlpha = color1.alpha + ((color2.alpha - color1.alpha) * fraction); + int newColor = (red << 16) + (green << 8) + blue; + return new Color(newColor, newAlpha); + } + + public Color lerpRGBA(Color color2, float fraction) { + return Color.lerpRGBA(this, color2, fraction); + } + + public Color multiply(float multi) { + if (multi == 1) + return this.clone(); + + int r = (int) ((getRed() * multi)); + int g = (int) ((getGreen() * multi)); + int b = (int) ((getBlue() * multi)); + float a = alpha * multi; + return new Color((r << 16) + (g << 8) + b, a); + } + + @SideOnly(Side.CLIENT) + public void glColor() { + GL11.glColor4f(getRedF(), getGreenF(), getBlueF(), alpha); + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound, String name) { + compound.setInteger(name + "Color", color); + compound.setFloat(name + "Alpha", alpha); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound, String name) { + setColor(compound.getInteger(name + "Color"), compound.getFloat(name + "Alpha")); + } + + public int getRed() { + return (color >> 16 & 0xFF); + } + + public float getRedF() { + return (float) (color >> 16 & 0xFF) / 255f; + } + + public int getGreen() { + return (color >> 8 & 0xFF); + } + + public float getGreenF() { + return (float) (color >> 8 & 0xFF) / 255f; + } + + public int getBlue() { + return (color & 0xFF); + } + + public float getBlueF() { + return (float) (color & 0xFF) / 255f; + } + + public static String getColor(int color) { + String str; + for (str = Integer.toHexString(color); str.length() < 6; str = "0" + str) { + } + return str; + } + + public static String getColor(int color, float alpha) { + String str = getColor(color); + return str + " | " + (int) (alpha * 255); + } + + public Color clone() { + return new Color(color, alpha); + } +} diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index c84ec3604..e14861033 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -11,14 +11,14 @@ import java.util.Arrays; public class ConfigMarket { public static Configuration config; - public final static String MARKET = "Market"; public final static String CURRENCY = "Market.Currency"; - public final static String TRADER = "Market.Trader"; + public final static String AUCTION = "Market.Auction"; + public final static String AUCTION_BLACKLIST = "Market.Auction.Blacklist"; + public final static String AUCTION_LOGGING = "Market.Auction.Logging"; - // ========================================= - // Market General - // ========================================= - public static boolean MarketEnabled = true; + // Max trades grid = 9x5 = 45 slots + public static final int MAX_TRADE_SLOTS = 45; + public static final int MIN_TRADE_SLOTS = 1; // ========================================= // Currency Settings @@ -29,10 +29,38 @@ public class ConfigMarket { public static long MaxBalance = Long.MAX_VALUE; // ========================================= - // Trader Settings + // Auction Settings // ========================================= - public static boolean EnableStockByDefault = false; - public static String DefaultResetType = "NONE"; + public static boolean AuctionEnabled = true; + public static int AuctionDurationHours = 24; + public static long ListingFee = 10; + public static long MinimumListingPrice = 1; + public static double SalesTaxPercent = 0.05; + public static int DefaultMaxTrades = 8; + public static int SnipeProtectionMinutes = 2; + public static int ClaimExpirationDays = 20; + public static double MinBidIncrementPercent = 0.05; + public static double CancellationPenaltyPercent = 0.10; + + // ========================================= + // Auction Blacklist Settings + // ========================================= + public static boolean BlacklistEnabled = true; + public static String[] BlacklistedItems = new String[]{}; + public static String[] BlacklistedMods = new String[]{}; + public static String[] BlacklistedNBTTags = new String[]{}; + + // ========================================= + // Auction Logging Settings + // ========================================= + 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; public static void init(File configFile) { config = new Configuration(configFile); @@ -40,12 +68,6 @@ public class ConfigMarket { try { config.load(); - // ========================================= - // Market General - // ========================================= - MarketEnabled = config.get(MARKET, "Enable Market System", true, - "Enable the market/economy system").getBoolean(true); - // ========================================= // Currency Settings // ========================================= @@ -64,21 +86,111 @@ public class ConfigMarket { "Maximum currency balance a player can have").getInt(Integer.MAX_VALUE); // ========================================= - // Trader Settings + // Auction Settings // ========================================= - config.setCategoryComment(TRADER, "Trader stock system settings"); + config.setCategoryComment(AUCTION, "Auction system settings. The Auction allows players to list items for sale with bidding and buyout options."); - EnableStockByDefault = config.get(TRADER, "Enable Stock By Default", false, - "Enable stock system by default for new traders").getBoolean(false); + AuctionEnabled = config.get(AUCTION, "Enable Auction", true, + "Enable the Auction system").getBoolean(true); - DefaultResetType = config.get(TRADER, "Default Reset Type", "NONE", - "Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY)").getString(); + AuctionDurationHours = config.get(AUCTION, "Auction Duration Hours", 24, + "Default duration for auctions in hours").getInt(24); + + ListingFee = config.get(AUCTION, "Listing Fee", 10, + "Flat fee charged when creating a listing").getInt(10); + + MinimumListingPrice = config.get(AUCTION, "Minimum Listing Price", 1, + "Minimum starting price for auction listings").getInt(1); + if (MinimumListingPrice < 1) MinimumListingPrice = 1; + + SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, + "Percentage of sale price taken as tax (0.05 = 5%). Tax is deleted as a currency sink.").getDouble(0.05); + + int maxTrades = config.get(AUCTION, "Default Max Trades", 8, + "Default maximum number of trade slots per player (listings + bids + claims). Min: 1, Max: 45. Players can have more via customnpcs.auction.trades.X permissions.").getInt(8); + DefaultMaxTrades = Math.max(MIN_TRADE_SLOTS, Math.min(MAX_TRADE_SLOTS, maxTrades)); + + SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 2, + "When a bid is placed with less than this many minutes remaining, the auction is extended to this duration").getInt(2); + + ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 20, + "Number of days before unclaimed items/currency are deleted").getInt(20); + + MinBidIncrementPercent = config.get(AUCTION, "Min Bid Increment Percent", 0.05, + "Minimum bid increment as a percentage of current bid (0.05 = 5%)").getDouble(0.05); + + CancellationPenaltyPercent = config.get(AUCTION, "Cancellation Penalty Percent", 0.10, + "Percentage of current bid taken as penalty when seller cancels an auction with bids (0.10 = 10%)").getDouble(0.10); + + // ========================================= + // Auction Blacklist Settings + // ========================================= + config.setCategoryComment(AUCTION_BLACKLIST, + "Item Blacklist settings for the Auction House.\n" + + "Prevents specific items, mods, or items with certain NBT tags from being listed.\n\n" + + "ITEM FORMAT: Use 'modid:itemname' format (e.g., 'minecraft:bedrock', 'customnpcs:npcWand')\n" + + "WILDCARDS: Use * for wildcards (e.g., 'customnpcs:npc*' blocks all items starting with 'npc')\n" + + "MOD FORMAT: Use just the mod ID (e.g., 'projecte' blocks all items from that mod)\n" + + "NBT FORMAT: Use the NBT tag key name (e.g., 'AdminOnly' blocks items with that tag)"); + + BlacklistEnabled = config.get(AUCTION_BLACKLIST, "Enable Blacklist", true, + "Enable item blacklist checking when creating listings").getBoolean(true); + + BlacklistedItems = config.get(AUCTION_BLACKLIST, "Blacklisted Items", new String[]{ + "minecraft:bedrock", + "minecraft:command_block", + "customnpcs:npcWand", + "customnpcs:npcMobCloner", + "customnpcs:npcScripter", + "customnpcs:npcMovingPath", + "customnpcs:npcMounter", + "customnpcs:npcTeleporter", + "customnpcs:npcTool", + "customnpcs:npcSoulstoneFilled" + }, + "Items that cannot be listed on the Auction House.\n" + + "Format: modid:itemname (supports * wildcards)\n" + + "Example: 'minecraft:diamond_sword' or 'customnpcs:npc*'").getStringList(); + + BlacklistedMods = config.get(AUCTION_BLACKLIST, "Blacklisted Mods", new String[]{}, + "All items from these mods are blocked from the Auction House.\n" + + "Format: modid (e.g., 'projecte', 'thaumcraft')").getStringList(); + + BlacklistedNBTTags = config.get(AUCTION_BLACKLIST, "Blacklisted NBT Tags", new String[]{}, + "Items containing any of these NBT tag keys are blocked.\n" + + "Checks the root level of the item's NBT compound.\n" + + "Example: 'AdminOnly', 'CreativeMode'").getStringList(); + + // ========================================= + // Auction Logging Settings + // ========================================= + config.setCategoryComment(AUCTION_LOGGING, "Auction logging settings. Enable specific log types to track auction activity."); + + AuctionLoggingEnabled = config.get(AUCTION_LOGGING, "Enable Auction Logging", false, + "Master switch for auction logging. If false, no auction events are logged.").getBoolean(false); + + LogAuctionCreated = config.get(AUCTION_LOGGING, "Log Created", true, + "Log when auctions are created").getBoolean(true); + + LogAuctionBid = config.get(AUCTION_LOGGING, "Log Bid", true, + "Log when bids are placed").getBoolean(true); + + LogAuctionBuyout = config.get(AUCTION_LOGGING, "Log Buyout", true, + "Log when auctions are bought out").getBoolean(true); + + LogAuctionSold = config.get(AUCTION_LOGGING, "Log Sold", true, + "Log when auctions end with a winner").getBoolean(true); + + LogAuctionExpired = config.get(AUCTION_LOGGING, "Log Expired", true, + "Log when auctions expire with no bids").getBoolean(true); + + LogAuctionCancelled = config.get(AUCTION_LOGGING, "Log Cancelled", true, + "Log when auctions are cancelled").getBoolean(true); + + LogAuctionClaimed = config.get(AUCTION_LOGGING, "Log Claimed", true, + "Log when claims are collected").getBoolean(true); // Set category order - config.setCategoryPropertyOrder(MARKET, new ArrayList<>(Arrays.asList( - "Enable Market System" - ))); - config.setCategoryPropertyOrder(CURRENCY, new ArrayList<>(Arrays.asList( "Use Vault", "Currency Name", @@ -86,9 +198,35 @@ public class ConfigMarket { "Max Balance" ))); - config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList( - "Enable Stock By Default", - "Default Reset Type" + config.setCategoryPropertyOrder(AUCTION, new ArrayList<>(Arrays.asList( + "Enable Auction", + "Auction Duration Hours", + "Listing Fee", + "Minimum Listing Price", + "Sales Tax Percent", + "Default Max Trades", + "Snipe Protection Minutes", + "Claim Expiration Days", + "Min Bid Increment Percent", + "Cancellation Penalty Percent" + ))); + + config.setCategoryPropertyOrder(AUCTION_BLACKLIST, new ArrayList<>(Arrays.asList( + "Enable Blacklist", + "Blacklisted Items", + "Blacklisted Mods", + "Blacklisted NBT Tags" + ))); + + config.setCategoryPropertyOrder(AUCTION_LOGGING, new ArrayList<>(Arrays.asList( + "Enable Auction Logging", + "Log Created", + "Log Bid", + "Log Buyout", + "Log Sold", + "Log Expired", + "Log Cancelled", + "Log Claimed" ))); } catch (Exception e) { diff --git a/src/main/java/noppes/npcs/config/GlyphCache.java b/src/main/java/noppes/npcs/config/GlyphCache.java index 98474c493..86f34d433 100644 --- a/src/main/java/noppes/npcs/config/GlyphCache.java +++ b/src/main/java/noppes/npcs/config/GlyphCache.java @@ -5,7 +5,13 @@ import net.minecraft.client.renderer.GLAllocation; import net.minecraft.util.ResourceLocation; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.GraphicsEnvironment; +import java.awt.Rectangle; +import java.awt.RenderingHints; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.geom.Point2D; diff --git a/src/main/java/noppes/npcs/config/StringCache.java b/src/main/java/noppes/npcs/config/StringCache.java index 263b480b9..c40d59f1c 100644 --- a/src/main/java/noppes/npcs/config/StringCache.java +++ b/src/main/java/noppes/npcs/config/StringCache.java @@ -5,7 +5,8 @@ import net.minecraft.client.renderer.Tessellator; import net.minecraft.util.ResourceLocation; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.Font; +import java.awt.Point; import java.awt.font.GlyphVector; import java.lang.ref.WeakReference; import java.text.Bidi; diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java new file mode 100644 index 000000000..115f13dc9 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java @@ -0,0 +1,40 @@ +package noppes.npcs.constants; + +import net.minecraft.util.StatCollector; + +public enum EnumAuctionSort { + NEWEST("auction.sort.newest"), + ENDING_SOON("auction.sort.endingSoon"), + PRICE_LOW("auction.sort.priceLow"), + PRICE_HIGH("auction.sort.priceHigh"), + MOST_BIDS("auction.sort.mostBids"); + + private final String langKey; + + EnumAuctionSort(String langKey) { + this.langKey = langKey; + } + + public String getLangKey() { + return langKey; + } + + public String getDisplayName() { + return StatCollector.translateToLocal(langKey); + } + + public static String[] getDisplayNames() { + String[] names = new String[values().length]; + for (int i = 0; i < values().length; i++) { + names[i] = values()[i].getDisplayName(); + } + return names; + } + + public static EnumAuctionSort fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return NEWEST; + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java b/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java new file mode 100644 index 000000000..14fc0f4ff --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java @@ -0,0 +1,47 @@ +package noppes.npcs.constants; + +import net.minecraft.util.StatCollector; + +public enum EnumAuctionStatus { + ACTIVE("auction.status.active"), + ENDED("auction.status.ended"), + CANCELLED("auction.status.cancelled"), + CLAIMED("auction.status.claimed"); + + private final String langKey; + + EnumAuctionStatus(String langKey) { + this.langKey = langKey; + } + + public String getLangKey() { + return langKey; + } + + public String getDisplayName() { + return StatCollector.translateToLocal(langKey); + } + + public boolean isActive() { + return this == ACTIVE; + } + + public boolean isEnded() { + return this == ENDED || this == CANCELLED || this == CLAIMED; + } + + public boolean canBid() { + return this == ACTIVE; + } + + public boolean canCancel() { + return this == ACTIVE; + } + + public static EnumAuctionStatus fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return ACTIVE; + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumClaimType.java b/src/main/java/noppes/npcs/constants/EnumClaimType.java new file mode 100644 index 000000000..35fbf2003 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumClaimType.java @@ -0,0 +1,38 @@ +package noppes.npcs.constants; + +import net.minecraft.util.StatCollector; + +public enum EnumClaimType { + ITEM("auction.claim.item"), + CURRENCY("auction.claim.currency"), + REFUND("auction.claim.refund"); + + private final String langKey; + + EnumClaimType(String langKey) { + this.langKey = langKey; + } + + public String getLangKey() { + return langKey; + } + + public String getDisplayName() { + return StatCollector.translateToLocal(langKey); + } + + public boolean isItem() { + return this == ITEM; + } + + public boolean isCurrency() { + return this == CURRENCY || this == REFUND; + } + + public static EnumClaimType fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return ITEM; + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumGuiType.java b/src/main/java/noppes/npcs/constants/EnumGuiType.java index f2d911561..1859b2616 100644 --- a/src/main/java/noppes/npcs/constants/EnumGuiType.java +++ b/src/main/java/noppes/npcs/constants/EnumGuiType.java @@ -64,5 +64,10 @@ public enum EnumGuiType { ScriptBlock, Paintbrush, MagicBook, - ManageAbilities; + ManageAbilities, + PlayerAuction, + PlayerAuctionSell, + PlayerAuctionTrades, + PlayerAuctionBidding, + ManageAuction; } diff --git a/src/main/java/noppes/npcs/constants/EnumNotificationType.java b/src/main/java/noppes/npcs/constants/EnumNotificationType.java new file mode 100644 index 000000000..02130e710 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumNotificationType.java @@ -0,0 +1,32 @@ +package noppes.npcs.constants; + +import net.minecraft.util.StatCollector; + +public enum EnumNotificationType { + AUCTION_WON("auction.notify.won"), + AUCTION_OUTBID("auction.notify.outbid"), + AUCTION_SOLD("auction.notify.sold"), + AUCTION_EXPIRED("auction.notify.expired"), + CLAIM_READY("auction.notify.claim"); + + private final String langKey; + + EnumNotificationType(String langKey) { + this.langKey = langKey; + } + + public String getLangKey() { + return langKey; + } + + public String getDisplayName() { + return StatCollector.translateToLocal(langKey); + } + + public static EnumNotificationType fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return CLAIM_READY; + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumRoleType.java b/src/main/java/noppes/npcs/constants/EnumRoleType.java index 624658fb3..33f36941a 100644 --- a/src/main/java/noppes/npcs/constants/EnumRoleType.java +++ b/src/main/java/noppes/npcs/constants/EnumRoleType.java @@ -2,5 +2,5 @@ package noppes.npcs.constants; public enum EnumRoleType { - None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount + None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount, Auctioneer } diff --git a/src/main/java/noppes/npcs/constants/EnumScriptType.java b/src/main/java/noppes/npcs/constants/EnumScriptType.java index b1052c86c..99a8b4091 100644 --- a/src/main/java/noppes/npcs/constants/EnumScriptType.java +++ b/src/main/java/noppes/npcs/constants/EnumScriptType.java @@ -116,6 +116,10 @@ public enum EnumScriptType { PROFILE_REMOVE("profileRemove"), PROFILE_CREATE("profileCreate"), + ON_EFFECT_ADD("onEffectAdd"), + ON_EFFECT_TICK("onEffectTick"), + ON_EFFECT_REMOVE("onEffectRemove"), + ABILITY_START("abilityStart"), ABILITY_EXECUTE("abilityExecute"), ABILITY_INTERRUPT("abilityInterrupt"), diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index abf55a84e..06d5a6ec3 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -6,5 +6,8 @@ public enum EnumScrollData { MAGIC_CYCLES, DIALOG_GROUP, QUEST_GROUP, - ABILITY_TYPES + ABILITY_TYPES, + CUSTOM_ABILITIES, + ANIMATIONS, + BUILTIN_ANIMATIONS } diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java new file mode 100644 index 000000000..dcc5b5d95 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -0,0 +1,280 @@ +package noppes.npcs.constants; + +import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.IBlockEvent; +import noppes.npcs.api.event.ICustomGuiEvent; +import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IFactionEvent; +import noppes.npcs.api.event.IForgeEvent; +import noppes.npcs.api.event.IItemEvent; +import noppes.npcs.api.event.INpcEvent; +import noppes.npcs.api.event.IPartyEvent; +import noppes.npcs.api.event.IPlayerEvent; +import noppes.npcs.api.event.IProjectileEvent; +import noppes.npcs.api.event.IQuestEvent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Represents a script context that maps to one or more event interface namespaces. + *

+ * A script context can have multiple namespaces because script editors often + * support hooks from multiple event types. For example, Player scripts support: + * - IPlayerEvent (core player hooks) + * - IAnimationEvent (animation hooks) + * - IPartyEvent (party hooks) + * - ICustomGuiEvent (custom GUI hooks) + * - etc. + *

+ * Each context also has a hookContext string used by ScriptHookController for + * hook registration and lookup. + *

+ * This is a registerable system - any mod can register their own contexts: + *

+ * // Register a new context with multiple namespaces: + * ScriptContext.register("DBC", "dbc", IDBCEvent.class, IDBCPlayerEvent.class); + *

+ * // Or add namespaces to an existing context: + * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent"); + */ +public class ScriptContext { + + // Registry of all script contexts + private static final Map REGISTRY = new ConcurrentHashMap<>(); + + // ==================== BUILT-IN CONTEXTS ==================== + + public static final ScriptContext NPC = register("NPC", "npc", + INpcEvent.class, + IProjectileEvent.class + ); + + public static final ScriptContext PLAYER = register("PLAYER", "player", + IPlayerEvent.class, + IAnimationEvent.class, + IPartyEvent.class, + IDialogEvent.class, + IQuestEvent.class, + IFactionEvent.class, + ICustomGuiEvent.class + ); + + public static final ScriptContext BLOCK = register("BLOCK", "block", + IBlockEvent.class + ); + + public static final ScriptContext ITEM = register("ITEM", "item", + IItemEvent.class + ); + + public static final ScriptContext FORGE = register("FORGE", "forge", + IForgeEvent.class + ); + + public static final ScriptContext LINKED_ITEM = register("LINKED_ITEM", "linked_item", + IItemEvent.class + ); + + public static final ScriptContext RECIPE = register("RECIPE", "recipe", + "Recipe" + ); + + public static final ScriptContext EFFECT = register("EFFECT", "effect", + "Effect" + ); + + public static final ScriptContext GLOBAL = register("GLOBAL", "", + "Global" // Special case: Global namespace doesn't have a corresponding event class + ); + + // ==================== INSTANCE FIELDS ==================== + + /** + * Unique identifier for this context (e.g., "NPC", "PLAYER", "DBC") + */ + public final String id; + + /** + * Hook context identifier used by ScriptHookController (e.g., "npc", "player") + */ + public final String hookContext; + + /** + * The event interface namespaces this context supports + */ + private final List namespaces; + + // ==================== CONSTRUCTOR ==================== + + private ScriptContext(String id, String hookContext, String... namespaces) { + this.id = id; + this.hookContext = hookContext != null ? hookContext : ""; + this.namespaces = new ArrayList<>(Arrays.asList(namespaces)); + } + + // ==================== NAMESPACE ACCESS ==================== + + /** + * Get all namespaces this context supports. + * + * @return Unmodifiable list of namespace strings + */ + public List getNamespaces() { + return Collections.unmodifiableList(namespaces); + } + + /** + * Check if this context includes a specific namespace. + * + * @param namespace The namespace to check (e.g., "IPlayerEvent") + * @return true if this context supports hooks from that namespace + */ + public boolean hasNamespace(String namespace) { + return namespaces.contains(namespace); + } + + /** + * Add a namespace to this context. + * Mods can use this to extend built-in contexts with their own event types. + * + * @param namespace The namespace to add (e.g., "IDBCPlayerEvent") + */ + public void addNamespace(String namespace) { + if (!namespaces.contains(namespace)) { + namespaces.add(namespace); + } + } + + /** + * Get the primary namespace (first one registered). + * This is mainly for backward compatibility. + * + * @return The first namespace, or "Global" if none + */ + public String getPrimaryNamespace() { + return namespaces.isEmpty() ? "Global" : namespaces.get(0); + } + + // ==================== REGISTRATION API ==================== + + /** + * Register a new script context with a hook context and one or more namespaces. + * + * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param hookContext The hook context string for ScriptHookController (e.g., "dbc") + * @param namespaces The event interface names (e.g., "IDBCEvent", "IDBCPlayerEvent") + * @return The registered ScriptContext + */ + public static ScriptContext register(String id, String hookContext, String... namespaces) { + ScriptContext context = new ScriptContext(id, hookContext, namespaces); + REGISTRY.put(id, context); + return context; + } + + /** + * Register a new script context using event interface classes. + * Automatically extracts the simple name from each class. + *

+ * Example: + * ScriptContext.register("NPC", "npc", INpcEvent.class, IProjectileEvent.class); + * + * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param hookContext The hook context string for ScriptHookController (e.g., "npc") + * @param eventClasses The event interface classes (simple names will be extracted) + * @return The registered ScriptContext + */ + public static ScriptContext register(String id, String hookContext, Class... eventClasses) { + String[] namespaces = new String[eventClasses.length]; + for (int i = 0; i < eventClasses.length; i++) { + namespaces[i] = eventClasses[i].getSimpleName(); + } + return register(id, hookContext, namespaces); + } + + /** + * Get a script context by its ID. + * + * @param id The context ID (e.g., "NPC", "PLAYER", "DBC") + * @return The ScriptContext, or GLOBAL if not found + */ + public static ScriptContext byId(String id) { + return REGISTRY.getOrDefault(id, GLOBAL); + } + + /** + * Find a script context that contains a specific namespace. + * + * @param namespace The event interface namespace (e.g., "INpcEvent") + * @return The first ScriptContext that contains this namespace, or GLOBAL if not found + */ + public static ScriptContext byNamespace(String namespace) { + if (namespace == null) return GLOBAL; + for (ScriptContext ctx : REGISTRY.values()) { + if (ctx.hasNamespace(namespace)) { + return ctx; + } + } + return GLOBAL; + } + + /** + * Find a script context by its hook context string. + * + * @param hookContext The hook context string (e.g., "npc", "player") + * @return The ScriptContext with that hook context, or GLOBAL if not found + */ + public static ScriptContext byHookContext(String hookContext) { + if (hookContext == null || hookContext.isEmpty()) return GLOBAL; + for (ScriptContext ctx : REGISTRY.values()) { + if (hookContext.equals(ctx.hookContext)) { + return ctx; + } + } + return GLOBAL; + } + + /** + * Get all registered script contexts. + * + * @return Unmodifiable collection of all registered contexts + */ + public static Collection values() { + return Collections.unmodifiableCollection(REGISTRY.values()); + } + + /** + * Check if a context with the given ID exists. + * + * @param id The context ID to check + * @return true if registered + */ + public static boolean exists(String id) { + return REGISTRY.containsKey(id); + } + + // ==================== OBJECT METHODS ==================== + + @Override + public String toString() { + return id + " -> " + namespaces; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ScriptContext)) return false; + ScriptContext other = (ScriptContext) obj; + return id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerAuction.java b/src/main/java/noppes/npcs/containers/ContainerAuction.java new file mode 100644 index 000000000..2ccaa2e52 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuction.java @@ -0,0 +1,81 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Base container for all auction GUIs. + * Provides player inventory at the correct position for the 256x256 auction texture. + * Player inventory is display-only - no modifications allowed. + */ +public class ContainerAuction extends ContainerNpcInterface { + public final EntityNPCInterface npc; + + // Player inventory layout for 256x256 auction texture + // Top-left slot background at (47, 163) + // Item renders at +1 offset inside the 18x18 slot background + public static final int PLAYER_INV_X = 48; // 47 + 1 for item offset + public static final int PLAYER_INV_Y = 164; // 163 + 1 for item offset + public static final int HOTBAR_Y = 222; // PLAYER_INV_Y + (3 * 18) + 4 gap + + // Player inventory slots are added first (36 slots: 0-35) + public static final int PLAYER_INV_SLOT_COUNT = 36; + + public ContainerAuction(EntityNPCInterface npc, EntityPlayer player) { + super(player); + this.npc = npc; + this.player = player; + + addPlayerInventory(); + } + + protected void addPlayerInventory() { + // Main inventory (3 rows of 9) + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + int slotIndex = col + row * 9 + 9; // Slots 9-35 + int x = PLAYER_INV_X + col * 18; + int y = PLAYER_INV_Y + row * 18; + addSlotToContainer(new Slot(player.inventory, slotIndex, x, y)); + } + } + + // Hotbar (1 row of 9) + for (int col = 0; col < 9; col++) { + int x = PLAYER_INV_X + col * 18; + addSlotToContainer(new Slot(player.inventory, col, x, HOTBAR_Y)); + } + } + + @Override + public ItemStack transferStackInSlot(EntityPlayer player, int slotIndex) { + return null; // Disable shift-clicking + } + + @Override + public ItemStack slotClick(int slotIndex, int mouseButton, int mode, EntityPlayer player) { + // Block all slot interactions by default in auction containers + // This prevents picking up, placing, and swapping items + // Subclasses can override to allow specific interactions + return null; + } + + /** + * Check if the slot index is a player inventory slot. + */ + public boolean isPlayerInventorySlot(int slotIndex) { + return slotIndex >= 0 && slotIndex < PLAYER_INV_SLOT_COUNT; + } + + /** + * Get the ItemStack in a player inventory slot. + * @param containerSlotIndex The container slot index (0-35 for player inv) + */ + public ItemStack getPlayerInventoryStack(int containerSlotIndex) { + if (!isPlayerInventorySlot(containerSlotIndex)) return null; + Slot slot = (Slot) inventorySlots.get(containerSlotIndex); + return slot != null ? slot.getStack() : null; + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java new file mode 100644 index 000000000..2b40000ac --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java @@ -0,0 +1,63 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.InventoryBasic; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Container for the auction bidding/purchase view. + * Displays a single item slot showing the listing's item. + */ +public class ContainerAuctionBidding extends ContainerAuction { + // Display slot layout + private int itemSlotX = 71; + private int itemSlotY = 76; + private int itemSlotIndex = PLAYER_INV_SLOT_COUNT; + + private final IInventory displayInventory; + private AuctionListing listing; + + public ContainerAuctionBidding(EntityNPCInterface npc, EntityPlayer player) { + super(npc, player); + displayInventory = new InventoryBasic("Display", false, 1); + + // Display slot (read-only) + addSlotToContainer(new Slot(displayInventory, 0, itemSlotX, itemSlotY) { + @Override + public boolean isItemValid(ItemStack stack) { return false; } + @Override + public boolean canTakeStack(EntityPlayer player) { return false; } + }); + } + + /** + * Set the listing to display. + */ + public void setListing(AuctionListing listing) { + this.listing = listing; + if (listing != null && listing.item != null) { + displayInventory.setInventorySlotContents(0, listing.item.copy()); + } else { + displayInventory.setInventorySlotContents(0, null); + } + } + + public AuctionListing getListing() { + return listing; + } + + public ItemStack getDisplayItem() { + return displayInventory.getStackInSlot(0); + } + + /** + * Check if this is the display slot. + */ + public boolean isDisplaySlot(int slotIndex) { + return slotIndex == itemSlotIndex; + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionListing.java b/src/main/java/noppes/npcs/containers/ContainerAuctionListing.java new file mode 100644 index 000000000..9691935be --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionListing.java @@ -0,0 +1,133 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.List; + +/** + * Container for the auction listing browse view. + * Displays a 9x5 grid of auction listings (45 slots). + */ +public class ContainerAuctionListing extends ContainerAuction { + // Listing grid layout + public static final int LISTING_START_X = 57; // 56 + 1 for item offset + public static final int LISTING_START_Y = 47; // 46 + 1 for item offset + public static final int LISTING_COLS = 9; + public static final int LISTING_ROWS = 5; + public static final int LISTING_SLOT_COUNT = LISTING_COLS * LISTING_ROWS; + + public final InventoryAuctionDisplay displayInventory; + private int currentPage = 0; + private AuctionFilter filter = new AuctionFilter(); + + public ContainerAuctionListing(EntityNPCInterface npc, EntityPlayer player) { + super(npc, player); + + // Create display inventory for listings + displayInventory = new InventoryAuctionDisplay(LISTING_SLOT_COUNT); + + // Add listing display slots (9x5 grid) + for (int row = 0; row < LISTING_ROWS; row++) { + for (int col = 0; col < LISTING_COLS; col++) { + int slotIndex = col + row * LISTING_COLS; + int x = LISTING_START_X + col * 18; + int y = LISTING_START_Y + row * 18; + addSlotToContainer(new SlotAuctionDisplay(displayInventory, slotIndex, x, y)); + } + } + + // Load initial listings + refreshListings(); + } + + public void refreshListings() { + if (AuctionController.Instance == null) return; + + List listings = AuctionController.Instance.getActiveListings( + filter, currentPage, LISTING_SLOT_COUNT); + + displayInventory.clear(); + for (int i = 0; i < listings.size() && i < LISTING_SLOT_COUNT; i++) { + displayInventory.setListing(i, listings.get(i)); + } + } + + public AuctionListing getListingAt(int slotIndex) { + return displayInventory.getListing(slotIndex); + } + + /** + * Find a listing by matching ItemStack. + */ + public AuctionListing getListingForItem(ItemStack stack) { + if (stack == null) return null; + + for (int i = 0; i < LISTING_SLOT_COUNT; i++) { + AuctionListing listing = displayInventory.getListing(i); + if (listing != null && listing.item != null && ItemStack.areItemStacksEqual(listing.item, stack)) { + return listing; + } + } + return null; + } + + public int getCurrentPage() { + return currentPage; + } + + public void setPage(int page) { + this.currentPage = Math.max(0, page); + refreshListings(); + } + + public void nextPage() { + setPage(currentPage + 1); + } + + public void prevPage() { + if (currentPage > 0) { + setPage(currentPage - 1); + } + } + + public int getTotalPages() { + if (AuctionController.Instance == null) return 1; + int totalListings = AuctionController.Instance.getTotalActiveListings(filter); + return Math.max(1, (int) Math.ceil((double) totalListings / LISTING_SLOT_COUNT)); + } + + public int getTotalListings() { + if (AuctionController.Instance == null) return 0; + return AuctionController.Instance.getTotalActiveListings(filter); + } + + public AuctionFilter getFilter() { + return filter; + } + + public void setFilter(AuctionFilter filter) { + this.filter = filter; + this.currentPage = 0; + refreshListings(); + } + + /** + * Check if a slot index is a display slot. + */ + public boolean isDisplaySlot(int slotIndex) { + int start = getDisplaySlotStart(); + return slotIndex >= start && slotIndex < start + LISTING_SLOT_COUNT; + } + + /** + * Get the starting slot index for display slots. + */ + public int getDisplaySlotStart() { + return PLAYER_INV_SLOT_COUNT; + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java new file mode 100644 index 000000000..14b381f40 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java @@ -0,0 +1,125 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.InventoryBasic; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Container for creating auction listings with a single sell slot. + * Items are cloned from inventory, not moved. Validation happens on submit. + */ +public class ContainerAuctionSell extends ContainerAuction { + private int sellSlotX = 59; + private int sellSlotY = 101; + private int sellSlotIndex = PLAYER_INV_SLOT_COUNT; + private int maxStack = 64; + + private final IInventory sellInventory; + + public ContainerAuctionSell(EntityNPCInterface npc, EntityPlayer player) { + super(npc, player); + sellInventory = new InventoryBasic("Sell", false, 1); + + // Sell slot blocks direct interaction + addSlotToContainer(new Slot(sellInventory, 0, sellSlotX, sellSlotY) { + @Override + public boolean isItemValid(ItemStack stack) { return false; } + @Override + public boolean canTakeStack(EntityPlayer player) { return false; } + }); + } + + public ItemStack getItemToSell() { + return sellInventory.getStackInSlot(0); + } + + public void clearSellSlot() { + sellInventory.setInventorySlotContents(0, null); + } + + /** + * Add items from inventory to sell slot. + * Not limited by inventory - just stages items for sale. Validation on submit. + * @param sourceSlot Inventory slot index + * @param fullStack True=full stack, False=1 item + */ + public void addToSellSlot(int sourceSlot, boolean fullStack) { + if (!isPlayerInventorySlot(sourceSlot)) return; + + ItemStack source = getPlayerInventoryStack(sourceSlot); + if (source == null) return; + + ItemStack sellStack = sellInventory.getStackInSlot(0); + int toAdd = fullStack ? source.stackSize : 1; + + if (sellStack == null) { + // Empty slot - create new stack + ItemStack newStack = source.copy(); + newStack.stackSize = Math.min(toAdd, maxStack); + sellInventory.setInventorySlotContents(0, newStack); + } else if (itemsMatch(sellStack, source)) { + // Same item - add to existing (capped at 64) + int space = maxStack - sellStack.stackSize; + int add = Math.min(toAdd, space); + if (add > 0) { + sellStack.stackSize += add; + } + } else { + // Different item - replace + ItemStack newStack = source.copy(); + newStack.stackSize = Math.min(toAdd, maxStack); + sellInventory.setInventorySlotContents(0, newStack); + } + } + + /** + * Remove items from sell slot. + * @param removeAll True=clear slot, False=remove 1 item + */ + public void removeFromSellSlot(boolean removeAll) { + ItemStack sellStack = sellInventory.getStackInSlot(0); + if (sellStack == null) return; + + if (removeAll || sellStack.stackSize <= 1) { + sellInventory.setInventorySlotContents(0, null); + } else { + sellStack.stackSize--; + } + } + + /** Check if sell slot is this container slot index */ + public boolean isSellSlot(int slotIndex) { + return slotIndex == sellSlotIndex; + } + + /** Check if two stacks match using NoppesUtilPlayer.compareItems (same item, damage, and NBT) */ + private boolean itemsMatch(ItemStack a, ItemStack b) { + return NoppesUtilPlayer.compareItems(a, b, false, false); + } + + /** + * Count total items of a specific type across player inventory. + * Used for submit validation and UI indicator. + */ + public int countItemInInventory(ItemStack target) { + if (target == null) return 0; + int total = 0; + for (int i = 0; i < PLAYER_INV_SLOT_COUNT; i++) { + ItemStack stack = getPlayerInventoryStack(i); + if (stack != null && itemsMatch(stack, target)) { + total += stack.stackSize; + } + } + return total; + } + + @Override + public void onContainerClosed(EntityPlayer player) { + super.onContainerClosed(player); + clearSellSlot(); + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java new file mode 100644 index 000000000..eab491c70 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java @@ -0,0 +1,353 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.ArrayList; +import java.util.List; + +/** + * Container for viewing player's auction trades. + * Shows active listings, bids, and claims in a 9x5 grid. + * Order: SOLD claims, OUTBID claims, WON claims, SELLING, BIDDING, EXPIRED claims + */ +public class ContainerAuctionTrades extends ContainerAuction { + // Grid layout + public static final int GRID_X = 57; + public static final int GRID_Y = 47; + public static final int COLS = 9; + public static final int ROWS = 5; + public static final int SLOT_COUNT = COLS * ROWS; + + public final InventoryAuctionDisplay displayInventory; + private List activeListings = new ArrayList<>(); + private List activeBids = new ArrayList<>(); + private List claims = new ArrayList<>(); + private int hiddenSlot = -1; + private int maxTradeSlots = 8; // Player's max trade slots (permission-aware, synced from server) + + // Slot type tracking for proper accessor methods + private int[] slotTypes = new int[SLOT_COUNT]; // 0=empty, 1=selling, 2=bidding, 3=claim + private int[] slotDataIndex = new int[SLOT_COUNT]; // Index into respective list + + public ContainerAuctionTrades(EntityNPCInterface npc, EntityPlayer player) { + super(npc, player); + displayInventory = new InventoryAuctionDisplay(SLOT_COUNT); + + // Add display slots (9x5 grid) + for (int row = 0; row < ROWS; row++) { + for (int col = 0; col < COLS; col++) { + int idx = col + row * COLS; + int x = GRID_X + col * 18; + int y = GRID_Y + row * 18; + addSlotToContainer(new SlotAuctionDisplay(displayInventory, idx, x, y)); + } + } + refreshData(); + } + + /** Refresh data from controller (server-side only) */ + public void refreshData() { + // Clear existing data + clearData(); + + // Only works on server where AuctionController exists + if (AuctionController.Instance == null) return; + + List newListings = AuctionController.Instance.getPlayerActiveListings(player.getUniqueID()); + List newBids = AuctionController.Instance.getPlayerActiveBids(player.getUniqueID()); + List newClaims = AuctionController.Instance.getPlayerClaims(player.getUniqueID()); + + populateDisplay(newListings, newBids, newClaims); + } + + /** Set data from NBT received from server (client-side) */ + public void setTradesData(NBTTagCompound compound) { + // Clear existing data + clearData(); + + // Read player's max trade slots (permission-aware) + if (compound.hasKey("MaxTradeSlots")) { + maxTradeSlots = compound.getInteger("MaxTradeSlots"); + } + + List newListings = new ArrayList<>(); + List newBids = new ArrayList<>(); + List newClaims = new ArrayList<>(); + + // Parse active listings + if (compound.hasKey("ActiveListings")) { + NBTTagList listingsNBT = compound.getTagList("ActiveListings", 10); + for (int i = 0; i < listingsNBT.tagCount(); i++) { + newListings.add(AuctionListing.fromNBT(listingsNBT.getCompoundTagAt(i))); + } + } + + // Parse active bids + if (compound.hasKey("ActiveBids")) { + NBTTagList bidsNBT = compound.getTagList("ActiveBids", 10); + for (int i = 0; i < bidsNBT.tagCount(); i++) { + newBids.add(AuctionListing.fromNBT(bidsNBT.getCompoundTagAt(i))); + } + } + + // Parse claims + if (compound.hasKey("Claims")) { + NBTTagList claimsNBT = compound.getTagList("Claims", 10); + for (int i = 0; i < claimsNBT.tagCount(); i++) { + newClaims.add(AuctionClaim.fromNBT(claimsNBT.getCompoundTagAt(i))); + } + } + + populateDisplay(newListings, newBids, newClaims); + } + + /** Clear all data and reset display */ + private void clearData() { + activeListings.clear(); + activeBids.clear(); + claims.clear(); + displayInventory.clear(); + + // Reset slot tracking + for (int i = 0; i < SLOT_COUNT; i++) { + slotTypes[i] = 0; + slotDataIndex[i] = -1; + } + } + + /** Populate display from provided data lists */ + private void populateDisplay(List newListings, List newBids, List allClaims) { + activeListings = new ArrayList<>(newListings); + activeBids = new ArrayList<>(newBids); + + // Sort claims into categories for proper display order + List soldClaims = new ArrayList<>(); // CURRENCY - sold items + List outbidClaims = new ArrayList<>(); // REFUND - outbid refunds + List wonClaims = new ArrayList<>(); // ITEM won (isReturned=false) + List expiredClaims = new ArrayList<>(); // ITEM returned (isReturned=true) + + for (AuctionClaim claim : allClaims) { + if (claim.type == EnumClaimType.CURRENCY) { + soldClaims.add(claim); + } else if (claim.type == EnumClaimType.REFUND) { + outbidClaims.add(claim); + } else if (claim.type == EnumClaimType.ITEM) { + if (claim.isReturned) { + expiredClaims.add(claim); + } else { + wonClaims.add(claim); + } + } + } + + // Populate display in order: SOLD, OUTBID, WON, SELLING, BIDDING, EXPIRED + int slot = 0; + + // 1. Sold claims (green) - you have money to claim! + for (AuctionClaim claim : soldClaims) { + if (slot >= SLOT_COUNT) break; + claims.add(claim); + displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; + slot++; + } + + // 2. Outbid claims (yellow) - refunds available + for (AuctionClaim claim : outbidClaims) { + if (slot >= SLOT_COUNT) break; + claims.add(claim); + displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; + slot++; + } + + // 3. Won claims (green) - items to claim! + for (AuctionClaim claim : wonClaims) { + if (slot >= SLOT_COUNT) break; + claims.add(claim); + displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; + slot++; + } + + // 4. Active listings (blue) - items you're selling + int listingIdx = 0; + for (AuctionListing listing : activeListings) { + if (slot >= SLOT_COUNT) break; + if (listing.item != null) displayInventory.setInventorySlotContents(slot, listing.item.copy()); + slotTypes[slot] = 1; // selling + slotDataIndex[slot] = listingIdx; + slot++; + listingIdx++; + } + + // 5. Active bids (blue) - items you're bidding on + int bidIdx = 0; + for (AuctionListing bid : activeBids) { + if (slot >= SLOT_COUNT) break; + if (bid.item != null) displayInventory.setInventorySlotContents(slot, bid.item.copy()); + slotTypes[slot] = 2; // bidding + slotDataIndex[slot] = bidIdx; + slot++; + bidIdx++; + } + + // 6. Expired claims (red) - returned items (lowest priority) + for (AuctionClaim claim : expiredClaims) { + if (slot >= SLOT_COUNT) break; + claims.add(claim); + displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; + slot++; + } + } + + // ========== Slot Data Accessors ========== + + /** Get listing at slot (or null if claim/empty) */ + public AuctionListing getListingAt(int slot) { + if (slot < 0 || slot >= SLOT_COUNT) return null; + int type = slotTypes[slot]; + int idx = slotDataIndex[slot]; + if (idx < 0) return null; + + if (type == 1 && idx < activeListings.size()) { + return activeListings.get(idx); + } else if (type == 2 && idx < activeBids.size()) { + return activeBids.get(idx); + } + return null; + } + + /** Get claim at slot (or null if listing/empty) */ + public AuctionClaim getClaimAt(int slot) { + if (slot < 0 || slot >= SLOT_COUNT) return null; + if (slotTypes[slot] == 3) { + int idx = slotDataIndex[slot]; + if (idx >= 0 && idx < claims.size()) { + return claims.get(idx); + } + } + return null; + } + + /** Check if slot is player's active listing */ + public boolean isSellingAt(int slot) { + return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == 1; + } + + /** Check if slot is player's active bid */ + public boolean isBiddingAt(int slot) { + return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == 2; + } + + /** Find listing matching item (searches both selling and bidding) */ + public AuctionListing getListingForItem(ItemStack stack) { + if (stack == null) return null; + for (AuctionListing listing : activeListings) { + if (listing.item != null && ItemStack.areItemStacksEqual(listing.item, stack)) return listing; + } + for (AuctionListing bid : activeBids) { + if (bid.item != null && ItemStack.areItemStacksEqual(bid.item, stack)) return bid; + } + return null; + } + + /** Find listing in selling list */ + public AuctionListing getSellingListingForItem(ItemStack stack) { + if (stack == null) return null; + for (AuctionListing listing : activeListings) { + if (listing.item != null && ItemStack.areItemStacksEqual(listing.item, stack)) return listing; + } + return null; + } + + /** Find listing in bidding list */ + public AuctionListing getBiddingListingForItem(ItemStack stack) { + if (stack == null) return null; + for (AuctionListing bid : activeBids) { + if (bid.item != null && ItemStack.areItemStacksEqual(bid.item, stack)) return bid; + } + return null; + } + + /** Find claim matching item */ + public AuctionClaim getClaimForItem(ItemStack stack) { + if (stack == null) return null; + for (AuctionClaim claim : claims) { + if (claim.item != null && ItemStack.areItemStacksEqual(claim.item, stack)) return claim; + } + return null; + } + + // ========== List Getters ========== + + public List getActiveListings() { return activeListings; } + public List getActiveBids() { return activeBids; } + public List getClaims() { return claims; } + public int getTotalTradeCount() { return activeListings.size() + activeBids.size() + claims.size(); } + + // ========== Slot Hiding (for pending operations) ========== + + /** Hide slot for pending operation display */ + public void setHiddenSlot(int slot) { + if (hiddenSlot >= 0 && hiddenSlot < SLOT_COUNT) { + restoreSlotItem(hiddenSlot); + } + hiddenSlot = slot; + if (hiddenSlot >= 0 && hiddenSlot < SLOT_COUNT) { + displayInventory.setInventorySlotContents(hiddenSlot, null); + } + } + + /** Restore hidden slot */ + public void clearHiddenSlot() { + if (hiddenSlot >= 0 && hiddenSlot < SLOT_COUNT) { + restoreSlotItem(hiddenSlot); + } + hiddenSlot = -1; + } + + /** Restore item at slot from data using slot tracking */ + private void restoreSlotItem(int slot) { + if (slot < 0 || slot >= SLOT_COUNT) return; + + int type = slotTypes[slot]; + int idx = slotDataIndex[slot]; + if (idx < 0) return; + + if (type == 1 && idx < activeListings.size()) { + // Selling + AuctionListing listing = activeListings.get(idx); + if (listing != null && listing.item != null) { + displayInventory.setInventorySlotContents(slot, listing.item.copy()); + } + } else if (type == 2 && idx < activeBids.size()) { + // Bidding + AuctionListing bid = activeBids.get(idx); + if (bid != null && bid.item != null) { + displayInventory.setInventorySlotContents(slot, bid.item.copy()); + } + } else if (type == 3 && idx < claims.size()) { + // Claim + displayInventory.setClaimItem(slot, claims.get(idx)); + } + } + + public int getHiddenSlot() { return hiddenSlot; } + + /** Get player's max trade slots (synced from server based on permissions) */ + public int getMaxTradeSlots() { return maxTradeSlots; } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java index e6b4c86c8..807190816 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java @@ -105,7 +105,7 @@ public class ContainerNPCTrader extends ContainerNpcInterface { long currencyCost = role.getCurrencyCost(i); if (currencyCost > 0) { PlayerData data = PlayerData.get(entityplayer); - if (data.currencyData.getBalance() < currencyCost) { + if (data.tradeData.getBalance() < currencyCost) { return null; // Can't afford currency cost } } @@ -117,7 +117,7 @@ public class ContainerNPCTrader extends ContainerNpcInterface { // Withdraw currency cost (after item consumption to maintain order) if (currencyCost > 0) { PlayerData data = PlayerData.get(entityplayer); - data.currencyData.withdraw(currencyCost); + data.tradeData.withdraw(currencyCost); } // Consume stock diff --git a/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java b/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java new file mode 100644 index 000000000..50d2b50e7 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java @@ -0,0 +1,159 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.item.ItemStack; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionListing; + +import java.util.ArrayList; +import java.util.List; + +/** + * Virtual inventory for displaying auction listings and claims. + * This inventory holds ItemStacks from AuctionListings/Claims for display purposes. + */ +public class InventoryAuctionDisplay implements IInventory { + private final ItemStack[] items; + private final List listings; + private final List claims; + private final int size; + + public InventoryAuctionDisplay(int size) { + this.size = size; + this.items = new ItemStack[size]; + this.listings = new ArrayList<>(size); + this.claims = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + listings.add(null); + claims.add(null); + } + } + + public void setListing(int slot, AuctionListing listing) { + if (slot >= 0 && slot < size) { + listings.set(slot, listing); + claims.set(slot, null); + items[slot] = listing != null ? listing.item : null; + } + } + + public AuctionListing getListing(int slot) { + if (slot >= 0 && slot < size) { + return listings.get(slot); + } + return null; + } + + /** + * Set a claim item in the inventory. + * Shows actual item for ITEM claims. + * For CURRENCY claims, shows coin icon (null item). + * For REFUND claims, shows item if present (for rebid option), otherwise coin icon. + */ + public void setClaimItem(int slot, AuctionClaim claim) { + if (slot >= 0 && slot < size) { + claims.set(slot, claim); + listings.set(slot, null); + if (claim != null) { + if (claim.type == EnumClaimType.CURRENCY) { + // Sold - gold coin icon drawn by GUI + items[slot] = null; + } else if (claim.type == EnumClaimType.REFUND) { + // Outbid - show item if present (for rebid), otherwise coin + items[slot] = claim.item != null ? claim.item.copy() : null; + } else if (claim.item != null) { + // Show actual item for ITEM claims + items[slot] = claim.item.copy(); + } else { + items[slot] = null; + } + } else { + items[slot] = null; + } + } + } + + public AuctionClaim getClaim(int slot) { + if (slot >= 0 && slot < size) { + return claims.get(slot); + } + return null; + } + + public void clear() { + for (int i = 0; i < size; i++) { + items[i] = null; + listings.set(i, null); + claims.set(i, null); + } + } + + @Override + public int getSizeInventory() { + return size; + } + + @Override + public ItemStack getStackInSlot(int slot) { + if (slot >= 0 && slot < size) { + return items[slot]; + } + return null; + } + + @Override + public ItemStack decrStackSize(int slot, int amount) { + return null; // Display only + } + + @Override + public ItemStack getStackInSlotOnClosing(int slot) { + return null; + } + + @Override + public void setInventorySlotContents(int slot, ItemStack stack) { + if (slot >= 0 && slot < size) { + items[slot] = stack; + } + } + + @Override + public String getInventoryName() { + return "Auction Display"; + } + + @Override + public boolean hasCustomInventoryName() { + return false; + } + + @Override + public int getInventoryStackLimit() { + return 64; + } + + @Override + public void markDirty() { + } + + @Override + public boolean isUseableByPlayer(EntityPlayer player) { + return true; + } + + @Override + public void openInventory() { + } + + @Override + public void closeInventory() { + } + + @Override + public boolean isItemValidForSlot(int slot, ItemStack stack) { + return false; + } +} diff --git a/src/main/java/noppes/npcs/containers/SlotAuctionDisplay.java b/src/main/java/noppes/npcs/containers/SlotAuctionDisplay.java new file mode 100644 index 000000000..43b745051 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/SlotAuctionDisplay.java @@ -0,0 +1,31 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; + +/** + * A display-only slot for auction listings. + * Items cannot be placed or taken from this slot. + */ +public class SlotAuctionDisplay extends Slot { + public SlotAuctionDisplay(IInventory inventory, int slotIndex, int x, int y) { + super(inventory, slotIndex, x, y); + } + + @Override + public boolean isItemValid(ItemStack stack) { + return false; + } + + @Override + public boolean canTakeStack(EntityPlayer player) { + return false; + } + + @Override + public int getSlotStackLimit() { + return 64; + } +} diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java index 65adfcd3d..b4203b452 100644 --- a/src/main/java/noppes/npcs/controllers/AnimationController.java +++ b/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -8,23 +8,43 @@ import noppes.npcs.LogWriter; import noppes.npcs.api.handler.IAnimationHandler; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; import noppes.npcs.util.NBTJsonUtil; import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; +import java.util.stream.Stream; import java.util.zip.GZIPInputStream; public class AnimationController implements IAnimationHandler { public HashMap animations; private HashMap bootOrder; + // Built-in animations loaded from assets folder (read-only, keyed by name) + public HashMap builtInAnimations = new HashMap<>(); + private static final String BUILTIN_ANIMATIONS_PATH = "/assets/customnpcs/animations"; + private static final String BUILTIN_ANIMATIONS_RESOURCE = "assets/customnpcs/animations"; + public static AnimationController Instance; private int lastUsedID = 0; @@ -42,12 +62,98 @@ public class AnimationController implements IAnimationHandler { public void load() { bootOrder = new HashMap<>(); animations = new HashMap<>(); + builtInAnimations = new HashMap<>(); LogWriter.info("Loading animations..."); + loadBuiltInAnimations(); readAnimationMap(); loadAnimations(); LogWriter.info("Done loading animations."); } + /** + * Load built-in animations from assets/customnpcs/animations/ folder. + * These animations are read-only and accessed by name only (no IDs). + * Scans the folder directly instead of using a manifest. + */ + private void loadBuiltInAnimations() { + try { + URL resourceUrl = CustomNpcs.class.getResource(BUILTIN_ANIMATIONS_PATH); + if (resourceUrl == null) { + LogWriter.info("Built-in animations folder not found: " + BUILTIN_ANIMATIONS_PATH); + return; + } + + URI uri = resourceUrl.toURI(); + Path animationsPath; + + if (uri.getScheme().equals("jar")) { + // Running from JAR - need to create a FileSystem to access it + FileSystem fileSystem = null; + try { + fileSystem = FileSystems.getFileSystem(uri); + } catch (Exception e) { + fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap()); + } + animationsPath = fileSystem.getPath(BUILTIN_ANIMATIONS_PATH); + } else { + // Running from file system (dev environment) + animationsPath = Paths.get(uri); + } + + // Scan the directory for .json files + try (Stream paths = Files.walk(animationsPath, 1)) { + paths.filter(path -> path.toString().endsWith(".json")) + .forEach(path -> { + String fileName = path.getFileName().toString(); + String animName = fileName.substring(0, fileName.length() - 5); // Remove .json + try { + loadBuiltInAnimation(animName); + } catch (Exception e) { + LogWriter.error("Error loading built-in animation: " + animName, e); + } + }); + } + + LogWriter.info("Loaded " + builtInAnimations.size() + " built-in animations."); + } catch (Exception e) { + LogWriter.error("Error scanning built-in animations folder", e); + } + } + + public void loadBuiltInAnimation(Class modClass, String path, String animName) throws Exception { + String resourcePath = path + "/" + animName + ".json"; + try (InputStream stream = modClass.getResourceAsStream(resourcePath)) { + if (stream == null) { + return; + } + + // Read JSON content + StringBuilder content = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String line; + while ((line = reader.readLine()) != null) { + content.append(line); + } + reader.close(); + + // Parse NBT from JSON + NBTTagCompound nbt = NBTJsonUtil.Convert(content.toString()); + + BuiltInAnimation animation = new BuiltInAnimation(animName); + animation.readFromNBT(nbt); + + // Store with lowercase key for case-insensitive lookup + builtInAnimations.put(animName.toLowerCase(), animation); + } + } + + /** + * Load a single built-in animation by name from assets folder. + */ + private void loadBuiltInAnimation(String animName) throws Exception { + loadBuiltInAnimation(CustomNpcs.class, BUILTIN_ANIMATIONS_PATH, animName); + } + private void loadAnimations() { animations.clear(); @@ -110,6 +216,12 @@ public class AnimationController implements IAnimationHandler { } public IAnimation saveAnimation(IAnimation animation) { + // Reject saving built-in animations + if (animation instanceof BuiltInAnimation) { + LogWriter.info("Cannot save built-in animation: " + animation.getName()); + return animation; + } + if (animation.getID() < 0) { animation.setID(getUnusedId()); while (hasName(animation.getName())) @@ -157,6 +269,12 @@ public class AnimationController implements IAnimationHandler { public void delete(String name) { Animation delete = getAnimationFromName(name); if (delete != null) { + // Reject deleting built-in animations + if (delete instanceof BuiltInAnimation) { + LogWriter.info("Cannot delete built-in animation: " + name); + return; + } + Animation foundAnimation = this.animations.remove(delete.getID()); if (foundAnimation != null && foundAnimation.name != null) { File dir = this.getDir(); @@ -175,7 +293,8 @@ public class AnimationController implements IAnimationHandler { } public void delete(int id) { - if (!this.animations.containsKey(id)) + // Only user animations have valid IDs (>= 0) + if (id < 0 || !this.animations.containsKey(id)) return; Animation foundAnimation = this.animations.remove(id); @@ -199,27 +318,109 @@ public class AnimationController implements IAnimationHandler { } public IAnimation get(String name) { - return getAnimationFromName(name); + return get(name, false); } + public IAnimation get(String name, boolean builtIn) { + // Check user/custom animations first (allows overriding built-in) + for (Map.Entry entry : animations.entrySet()) { + if (entry.getValue().name.equalsIgnoreCase(name)) { + return entry.getValue(); + } + } + // Then check built-in animations + if (builtIn) { + BuiltInAnimation builtInAnim = builtInAnimations.get(name.toLowerCase()); + if (builtInAnim != null) { + return builtInAnim; + } + } + + return null; + } + + /** + * Get a user animation by ID. + * Built-in animations have no IDs and cannot be retrieved this way. + */ public IAnimation get(int id) { + // Only user animations have valid IDs + if (id < 0) { + return null; + } return this.animations.get(id); } + /** + * Get a built-in animation by name. + * @param name The animation name (case-insensitive) + * @return The built-in animation, or null if not found + */ + public BuiltInAnimation getBuiltInAnimation(String name) { + return builtInAnimations.get(name.toLowerCase()); + } + public IAnimation[] getAnimations() { ArrayList animations = new ArrayList<>(this.animations.values()); return animations.toArray(new IAnimation[0]); } + /** + * Get all built-in animations. + */ + public IAnimation[] getBuiltInAnimations() { + return builtInAnimations.values().toArray(new IAnimation[0]); + } + + /** + * Get all animations (both built-in and user-created). + */ + public IAnimation[] getAllAnimations() { + ArrayList all = new ArrayList<>(); + all.addAll(builtInAnimations.values()); + all.addAll(animations.values()); + return all.toArray(new IAnimation[0]); + } + + /** + * Check if an animation name is a built-in animation. + * @param name The animation name to check + * @return true if this is a built-in animation + */ + public boolean isBuiltIn(String name) { + return builtInAnimations.containsKey(name.toLowerCase()); + } + + /** + * @deprecated Built-in animations no longer use IDs. Use isBuiltIn(String name) instead. + */ + @Deprecated + public boolean isBuiltIn(int id) { + return false; // Built-in animations have no IDs + } + public Animation getAnimationFromName(String animation) { - for (Map.Entry entryAnimation : AnimationController.getInstance().animations.entrySet()) { + // Check user/custom animations first (allows overriding built-in) + for (Map.Entry entryAnimation : animations.entrySet()) { if (entryAnimation.getValue().name.equalsIgnoreCase(animation)) { return entryAnimation.getValue(); } } + // Then check built-in animations + BuiltInAnimation builtIn = builtInAnimations.get(animation.toLowerCase()); + if (builtIn != null) { + return builtIn; + } return null; } + /** + * Get names of all built-in animations. + */ + public String[] getBuiltInAnimationNames() { + return builtInAnimations.keySet().toArray(new String[0]); + } + public String[] getNames() { String[] names = new String[animations.size()]; int i = 0; @@ -231,10 +432,9 @@ public class AnimationController implements IAnimationHandler { } - //////////////////////////////////////////////////////// - //////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// // ANIMATION MAP - public File getMapDir() { File dir = CustomNpcs.getWorldSaveDirectory(); if (!dir.exists()) diff --git a/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java new file mode 100644 index 000000000..9b8c1f995 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java @@ -0,0 +1,49 @@ +package noppes.npcs.controllers; + +import kamkeel.npcs.util.VaultUtil; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.config.ConfigMarket; + +/** + * Server-side utility for syncing auction configuration to clients. + * Handles Vault integration for currency name resolution. + * + * This class is SERVER-SIDE ONLY. Client reads via AuctionClientConfig. + */ +public class AuctionConfigSync { + + /** + * Write auction config to NBT for sending to client. + * Called on server side when building login packet. + * + * Currency name is resolved from Vault if enabled and available, + * otherwise falls back to ConfigMarket.CurrencyName. + */ + public static NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("AuctionEnabled", ConfigMarket.AuctionEnabled); + compound.setLong("ListingFee", ConfigMarket.ListingFee); + compound.setString("CurrencyName", getEffectiveCurrencyName()); + compound.setInteger("AuctionDurationHours", ConfigMarket.AuctionDurationHours); + compound.setDouble("MinBidIncrement", ConfigMarket.MinBidIncrementPercent); + compound.setInteger("MaxActiveListings", ConfigMarket.DefaultMaxTrades); + compound.setInteger("ClaimExpirationDays", ConfigMarket.ClaimExpirationDays); + return compound; + } + + /** + * Get the effective currency name for display. + * Uses Vault currency name if Vault is enabled and available, + * otherwise uses ConfigMarket.CurrencyName. + * + * @return The currency name to display + */ + public static String getEffectiveCurrencyName() { + if (ConfigMarket.UseVault && VaultUtil.isEnabled()) { + String vaultName = VaultUtil.getCurrencyNamePlural(); + if (vaultName != null && !vaultName.isEmpty() && !vaultName.equals("coins")) { + return vaultName; + } + } + return ConfigMarket.CurrencyName; + } +} diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java new file mode 100644 index 000000000..e6ca0c6cf --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -0,0 +1,1438 @@ +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; +import noppes.npcs.CustomNpcs; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.LogWriter; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.IAuctionHandler; +import noppes.npcs.api.handler.data.IAuctionListing; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionSort; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.constants.EnumNotificationType; +import noppes.npcs.controllers.data.AuctionBlacklist; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerTradeData; +import noppes.npcs.util.CustomNPCsThreader; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Main controller for the Auction system. + * + * Thread Safety: + * - Uses ConcurrentHashMap for all data stores - no locking required + * - Claims are stored in PlayerData, not here - reduces contention + * - Saves are performed asynchronously on the CNPC+ thread + * + * Performance: + * - Auction processing runs every 30 seconds (600 ticks) + * - Save interval is every 30 seconds (600 ticks) + * - Player-specific queries use indexed maps for fast lookups + */ +public class AuctionController implements IAuctionHandler { + public static AuctionController Instance; + + // Main data stores - listings only, claims are stored in PlayerData + private final Map listings = new ConcurrentHashMap<>(); + // Claims are now stored in PlayerData.tradeData, not here + + // ========================================= + // Player Indices for O(1) Lookups + // ========================================= + + /** Listings by seller UUID - for quick "my listings" lookups */ + private final Map> playerListingIds = new ConcurrentHashMap<>(); + + /** Listings where player is current high bidder */ + private final Map> playerBidIds = new ConcurrentHashMap<>(); + + /** Cached max trade slots per player - computed on login, cleared on logout */ + private final Map playerMaxTradesCache = new ConcurrentHashMap<>(); + + // ========================================= + // Threading & State + // ========================================= + + /** Flag indicating data needs to be saved */ + private final AtomicBoolean dirty = new AtomicBoolean(false); + + /** Flag to prevent concurrent saves */ + private final AtomicBoolean saving = new AtomicBoolean(false); + + private String filePath = ""; + private int tickCounter = 0; + + /** Process auctions and save every 30 seconds (600 ticks) */ + private static final int TICK_INTERVAL = 600; + + public AuctionController() { + Instance = this; + AuctionBlacklist.reload(); + load(); + } + + public static AuctionController getInstance() { + if (Instance == null || needsNewInstance()) { + Instance = new AuctionController(); + } + return Instance; + } + + private static boolean needsNewInstance() { + if (Instance == null) return true; + File file = CustomNpcs.getWorldSaveDirectory(); + if (file == null) return false; + return !Instance.filePath.equals(file.getAbsolutePath()); + } + + // ========================================= + // Tick Processing + // ========================================= + + public void onServerTick() { + if (!ConfigMarket.AuctionEnabled) return; + + tickCounter++; + + // Process auctions and save every 30 seconds + if (tickCounter >= TICK_INTERVAL) { + tickCounter = 0; + processEndedAuctions(); + if (dirty.get()) { + saveAsync(); + } + } + } + + private void processEndedAuctions() { + boolean changed = false; + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { + endAuction(listing); + changed = true; + } + } + if (changed) { + markDirty(); + } + } + + private void endAuction(AuctionListing listing) { + listing.status = EnumAuctionStatus.ENDED; + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + // Remove from bidder's active bids if any + if (listing.highBidderUUID != null) { + removeFromPlayerBids(listing.highBidderUUID, listing.id); + } + + // Get item display name safely + String itemDisplayName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item"; + + if (listing.hasBids()) { + // Auction sold - create claims + AuctionClaim itemClaim = AuctionClaim.createItemWonClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.item); + addClaimToPlayer(listing.highBidderUUID, itemClaim); + + long saleAmount = listing.currentBid; + long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); + long sellerReceives = saleAmount - tax; + + AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, + itemDisplayName, listing.highBidderName); + addClaimToPlayer(listing.sellerUUID, currencyClaim); + + // Notifications + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id, + "You won the auction for " + itemDisplayName + "!"); + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + "Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + + logAuction("SOLD", listing.sellerName, itemDisplayName, saleAmount, + "Winner: " + listing.highBidderName + ", Tax: " + tax); + } else { + // No bids - return item to seller + AuctionClaim returnClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaimToPlayer(listing.sellerUUID, returnClaim); + + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id, + "Your auction for " + itemDisplayName + " expired with no bids."); + + logAuction("EXPIRED", listing.sellerName, itemDisplayName, listing.startingPrice, "No bids"); + } + } + + // ========================================= + // Index Management + // ========================================= + + private void addToPlayerListings(UUID playerUUID, String listingId) { + playerListingIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId); + } + + private void removeFromPlayerListings(UUID playerUUID, String listingId) { + Set ids = playerListingIds.get(playerUUID); + if (ids != null) { + ids.remove(listingId); + if (ids.isEmpty()) { + playerListingIds.remove(playerUUID); + } + } + } + + private void addToPlayerBids(UUID playerUUID, String listingId) { + playerBidIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId); + } + + private void removeFromPlayerBids(UUID playerUUID, String listingId) { + Set ids = playerBidIds.get(playerUUID); + if (ids != null) { + ids.remove(listingId); + if (ids.isEmpty()) { + playerBidIds.remove(playerUUID); + } + } + } + + /** + * Add a claim to a player's trade data. + * If player is online, adds directly. If offline, loads their data file and updates it. + */ + private void addClaimToPlayer(UUID playerUUID, AuctionClaim claim) { + if (playerUUID == null || claim == null) return; + + PlayerData data = PlayerDataController.Instance.getData(playerUUID); + if (data != null) { + data.tradeData.addClaim(claim); + data.updateClient = true; + data.save(); + } + } + + // ========================================= + // Listing Management + // ========================================= + + public String createListing(EntityPlayer player, ItemStack item, long startingPrice, long buyoutPrice) { + if (!ConfigMarket.AuctionEnabled) { + return "Auction is disabled."; + } + + // Check blacklist (with bypass for admins) + if (AuctionBlacklist.isBlacklistedForPlayer(item, player)) { + return "This item cannot be listed on the Auction House."; + } + + // Validate minimum price + if (startingPrice < ConfigMarket.MinimumListingPrice) { + return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + "."; + } + + // Validate buyout price if set + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + return "Buyout price must be higher than starting price."; + } + + UUID playerUUID = player.getUniqueID(); + String playerName = player.getCommandSenderName(); + + // Check trade slot limit (listings + bids + claims) + int currentTrades = getPlayerTradeSlotCount(player); + int maxTrades = getMaxTradesForPlayer(player); + if (currentTrades >= maxTrades) { + return "You have reached your maximum trade slots (" + maxTrades + ")."; + } + + // Check listing fee + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + long fee = ConfigMarket.ListingFee; + if (!currency.canAfford(fee)) { + return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ")."; + } + + // Deduct fee + if (!currency.withdraw(fee)) { + return "Failed to deduct listing fee."; + } + + // Calculate duration + long durationMs = ConfigMarket.AuctionDurationHours * 60L * 60L * 1000L; + + // Create listing + AuctionListing listing = new AuctionListing(playerUUID, playerName, item, startingPrice, buyoutPrice, durationMs); + listings.put(listing.id, listing); + addToPlayerListings(playerUUID, listing.id); + + markDirty(); + + logAuction("CREATED", playerName, item.getDisplayName(), startingPrice, + "Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", Fee: " + fee); + + return null; // Success + } + + public String cancelListing(String listingId, EntityPlayer player, boolean isAdmin) { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; + } + + if (!listing.status.canCancel()) { + return "This auction cannot be cancelled."; + } + + UUID playerUUID = player.getUniqueID(); + if (!isAdmin && !listing.isSeller(playerUUID)) { + return "You can only cancel your own listings."; + } + + // Handle penalty if there are bids + if (listing.hasBids()) { + long penalty = (long) (listing.currentBid * ConfigMarket.CancellationPenaltyPercent); + + // Refund bidder (no item - can't rebid on cancelled auction) + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, + listing.item.getDisplayName(), listing.sellerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); + + // Remove from bidder's active bids + removeFromPlayerBids(listing.highBidderUUID, listing.id); + + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, + "The auction for " + listing.item.getDisplayName() + " was cancelled. Your bid has been refunded."); + + // Return item to seller + AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaimToPlayer(listing.sellerUUID, itemClaim); + + logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.currentBid, + "Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName + + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); + } else { + // No bids - just return item + AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaimToPlayer(listing.sellerUUID, itemClaim); + + logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, + "No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); + } + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + listing.status = EnumAuctionStatus.CANCELLED; + markDirty(); + + return null; // Success + } + + // ========================================= + // Bidding + // ========================================= + + public String placeBid(String listingId, EntityPlayer player, long bidAmount) { + if (!ConfigMarket.AuctionEnabled) { + return "Auction is disabled."; + } + + UUID playerUUID = player.getUniqueID(); + String playerName = player.getCommandSenderName(); + + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; + } + + // Check status and expiry + if (!listing.status.canBid()) { + return "This auction has ended."; + } + + if (listing.isExpired()) { + return "This auction has ended."; + } + + // CRITICAL: Prevent bidding on own items + if (listing.isSeller(playerUUID)) { + return "You cannot bid on your own auction."; + } + + // Prevent rebidding if already highest bidder + if (listing.isHighBidder(playerUUID)) { + return "You are already the highest bidder. Use buyout if you want to purchase immediately."; + } + + // Check minimum bid + long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent); + if (bidAmount < minBid) { + return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + "."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + + // Store previous bidder info + UUID previousBidder = listing.highBidderUUID; + long previousBid = listing.currentBid; + + // Full bid amount charged since we don't allow rebidding + long amountToCharge = bidAmount; + + if (!currency.canAfford(amountToCharge)) { + return "You cannot afford this bid."; + } + + // Deduct bid from new bidder FIRST + if (!currency.withdraw(amountToCharge)) { + return "Failed to deduct bid amount."; + } + + // Refund previous bidder (include item for rebid option) + if (listing.hasBids() && previousBidder != null) { + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + previousBidder, listing.highBidderName, listing.id, previousBid, + listing.item.getDisplayName(), playerName, listing.item); + addClaimToPlayer(previousBidder, refundClaim); + + // Remove from previous bidder's active bids + removeFromPlayerBids(previousBidder, listing.id); + + sendNotificationToPlayer(previousBidder, EnumNotificationType.AUCTION_OUTBID, listing.id, + "You were outbid on " + listing.item.getDisplayName() + "!"); + } + + // Update listing + listing.highBidderUUID = playerUUID; + listing.highBidderName = playerName; + listing.currentBid = bidAmount; + listing.bidCount++; + + // Add to new bidder's active bids + addToPlayerBids(playerUUID, listing.id); + + // Snipe protection + listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); + + markDirty(); + + logAuction("BID", playerName, listing.item.getDisplayName(), bidAmount, + "Bids: " + listing.bidCount + ", Seller: " + listing.sellerName); + + return null; // Success + } + + public String buyout(String listingId, EntityPlayer player) { + if (!ConfigMarket.AuctionEnabled) { + return "Auction is disabled."; + } + + UUID playerUUID = player.getUniqueID(); + String playerName = player.getCommandSenderName(); + + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; + } + + // Check status and expiry + if (!listing.status.canBid()) { + return "This auction has ended."; + } + + if (listing.isExpired()) { + return "This auction has ended."; + } + + if (!listing.hasBuyout()) { + return "This auction does not have a buyout price."; + } + + // CRITICAL: Prevent buying own items + if (listing.isSeller(playerUUID)) { + return "You cannot buy your own auction."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + + // If buyer is already the high bidder, only charge the difference + boolean isCurrentBidder = listing.isHighBidder(playerUUID); + long amountToCharge = isCurrentBidder + ? (listing.buyoutPrice - listing.currentBid) + : listing.buyoutPrice; + + if (!currency.canAfford(amountToCharge)) { + if (isCurrentBidder) { + return "You cannot afford the remaining " + amountToCharge + " " + ConfigMarket.CurrencyName + " for buyout."; + } + return "You cannot afford the buyout price."; + } + + // Deduct buyout amount + if (!currency.withdraw(amountToCharge)) { + return "Failed to deduct buyout amount."; + } + + // Refund previous bidder if any AND they're not the buyer (no item - auction ended via buyout) + if (listing.hasBids() && listing.highBidderUUID != null && !isCurrentBidder) { + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, + listing.item.getDisplayName(), playerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); + + // Remove from previous bidder's active bids + removeFromPlayerBids(listing.highBidderUUID, listing.id); + + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, + "The auction for " + listing.item.getDisplayName() + " was bought out. Your bid has been refunded."); + } else if (isCurrentBidder) { + // Current bidder is buying out - just remove from their active bids + removeFromPlayerBids(playerUUID, listing.id); + } + + // Create claims + AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(playerUUID, playerName, listing.id, listing.item); + addClaimToPlayer(playerUUID, itemClaim); + + long saleAmount = listing.buyoutPrice; + long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); + long sellerReceives = saleAmount - tax; + + AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, + listing.item.getDisplayName(), playerName); + addClaimToPlayer(listing.sellerUUID, currencyClaim); + + // Notifications + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + listing.status = EnumAuctionStatus.ENDED; + listing.highBidderUUID = playerUUID; + listing.highBidderName = playerName; + listing.currentBid = listing.buyoutPrice; + + markDirty(); + + logAuction("BUYOUT", playerName, listing.item.getDisplayName(), listing.buyoutPrice, + "Seller: " + listing.sellerName + ", Tax: " + tax); + + return null; // Success + } + + // ========================================= + // Claims - Now stored in PlayerData + // ========================================= + + /** + * Get player claims from PlayerData. + */ + public List getPlayerClaims(EntityPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return new ArrayList<>(); + } + return playerData.tradeData.getClaimsList(); + } + + /** + * Get player claims by UUID (for login notification). + */ + public List getPlayerClaims(UUID playerUUID) { + EntityPlayerMP onlinePlayer = (EntityPlayerMP) PlayerDataController.getPlayerFromUUID(playerUUID); + if (onlinePlayer != null) { + return getPlayerClaims(onlinePlayer); + } + return new ArrayList<>(); + } + + public String claimItem(String claimId, EntityPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + AuctionClaim claim = playerData.tradeData.getClaimInternal(claimId); + if (claim == null) { + return "Claim not found."; + } + + if (claim.claimed) { + return "Already claimed."; + } + + if (!claim.type.isItem()) { + return "This is not an item claim."; + } + + if (claim.item == null) { + return "Item data is missing."; + } + + // Check inventory space + if (!player.inventory.addItemStackToInventory(claim.item.copy())) { + return "Your inventory is full."; + } + + // Remove claim from player's auction data + playerData.tradeData.claimAndRemove(claimId); + playerData.updateClient = true; + playerData.save(); + + // Cleanup listing if no more claims + cleanupListingIfComplete(claim.listingId); + + logAuction("CLAIMED", player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed"); + + return null; // Success + } + + public String claimCurrency(String claimId, EntityPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + AuctionClaim claim = playerData.tradeData.getClaimInternal(claimId); + if (claim == null) { + return "Claim not found."; + } + + if (claim.claimed) { + return "Already claimed."; + } + + if (!claim.type.isCurrency()) { + return "This is not a currency claim."; + } + + if (!playerData.tradeData.deposit(claim.currency)) { + return "Failed to deposit currency."; + } + + // Remove claim from player's auction data + playerData.tradeData.claimAndRemove(claimId); + playerData.updateClient = true; + playerData.save(); + + // Cleanup listing if no more claims + cleanupListingIfComplete(claim.listingId); + + logAuction("CLAIMED", player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency, + claim.type == EnumClaimType.REFUND ? "Refund claimed" : "Sale proceeds claimed"); + + return null; // Success + } + + /** + * Clean up a listing if it's ended and no longer needed. + * Now just removes ended listings after some delay since claims are in PlayerData. + */ + private void cleanupListingIfComplete(String listingId) { + if (listingId == null || listingId.isEmpty()) return; + + AuctionListing listing = listings.get(listingId); + if (listing == null) return; + + // Only cleanup ended/cancelled listings + if (listing.status == EnumAuctionStatus.ACTIVE) return; + + // Remove ended listings (claims are now in PlayerData, not here) + listings.remove(listingId); + markDirty(); + } + + public int claimAll(EntityPlayer player) { + int claimedCount = 0; + List playerClaims = getPlayerClaims(player); + + for (AuctionClaim claim : playerClaims) { + String result; + if (claim.type.isItem()) { + result = claimItem(claim.id, player); + } else { + result = claimCurrency(claim.id, player); + } + if (result == null) { + claimedCount++; + } + } + + return claimedCount; + } + + // ========================================= + // Queries - Using Indexed Lookups + // ========================================= + + public List getActiveListings(AuctionFilter filter, int page, int pageSize) { + List result = new ArrayList<>(); + + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; + + // Apply search filter (searches item name AND seller name) + if (filter.hasSearchText()) { + String itemName = listing.item != null ? listing.item.getDisplayName() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName : ""; + if (!filter.matchesSearch(itemName, sellerName)) continue; + } + + result.add(listing); + } + + // Sort + sortListings(result, filter.sortBy); + + // Paginate + int start = page * pageSize; + int end = Math.min(start + pageSize, result.size()); + if (start >= result.size()) { + return new ArrayList<>(); + } + return new ArrayList<>(result.subList(start, end)); + } + + private void sortListings(List list, EnumAuctionSort sortBy) { + Comparator comparator; + switch (sortBy) { + case ENDING_SOON: + comparator = Comparator.comparingLong(AuctionListing::getTimeRemaining); + break; + case PRICE_LOW: + comparator = Comparator.comparingLong(AuctionListing::getEffectivePrice); + break; + case PRICE_HIGH: + comparator = Comparator.comparingLong(AuctionListing::getEffectivePrice).reversed(); + break; + case MOST_BIDS: + comparator = Comparator.comparingInt(l -> -l.bidCount); + break; + case NEWEST: + default: + comparator = Comparator.comparingLong(l -> -l.createdTime); + break; + } + list.sort(comparator); + } + + public int getTotalActiveListings(AuctionFilter filter) { + int count = 0; + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; + if (filter.hasSearchText()) { + String itemName = listing.item != null ? listing.item.getDisplayName() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName : ""; + if (!filter.matchesSearch(itemName, sellerName)) continue; + } + count++; + } + return count; + } + + @Override + public AuctionListing getListing(String listingId) { + return listings.get(listingId); + } + + /** + * Get a player's current currency balance. + */ + public long getPlayerBalance(EntityPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData == null || playerData.tradeData == null) { + return 0; + } + return playerData.tradeData.getBalance(); + } + + /** + * Get player listing count using index + */ + public int getPlayerListingCount(UUID playerUUID) { + Set ids = playerListingIds.get(playerUUID); + return ids != null ? ids.size() : 0; + } + + /** + * Get player's active listings using indexed lookup + */ + public List getPlayerActiveListings(UUID playerUUID) { + Set listingIds = playerListingIds.get(playerUUID); + if (listingIds == null || listingIds.isEmpty()) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + for (String listingId : listingIds) { + AuctionListing listing = listings.get(listingId); + if (listing != null && listing.status == EnumAuctionStatus.ACTIVE) { + result.add(listing); + } + } + return result; + } + + /** + * Get all active auctions where the player is currently the highest bidder. + * Uses indexed lookup for O(1) access to player's bid set. + */ + public List getPlayerActiveBids(UUID playerUUID) { + Set bidListingIds = playerBidIds.get(playerUUID); + if (bidListingIds == null || bidListingIds.isEmpty()) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + for (String listingId : bidListingIds) { + AuctionListing listing = listings.get(listingId); + if (listing != null && listing.status == EnumAuctionStatus.ACTIVE && + listing.highBidderUUID != null && listing.highBidderUUID.equals(playerUUID)) { + result.add(listing); + } + } + return result; + } + + /** + * Get the total number of trade slots used by a player. + * Includes active listings, active bids, and pending claims. + */ + public int getPlayerTradeSlotCount(EntityPlayer player) { + UUID playerUUID = player.getUniqueID(); + int count = 0; + // Active listings + Set listingIds = playerListingIds.get(playerUUID); + if (listingIds != null) count += listingIds.size(); + // Active bids + Set bidIds = playerBidIds.get(playerUUID); + if (bidIds != null) count += bidIds.size(); + // Pending claims (from PlayerData) + PlayerData playerData = PlayerData.get(player); + if (playerData != null) { + count += playerData.tradeData.getClaimCount(); + } + return count; + } + + /** + * Get the total number of trade slots used by a player (UUID version). + * For offline players, only returns listings + bids (claims are in their PlayerData). + */ + public int getPlayerTradeSlotCount(UUID playerUUID) { + int count = 0; + // Active listings + Set listingIds = playerListingIds.get(playerUUID); + if (listingIds != null) count += listingIds.size(); + // Active bids + Set bidIds = playerBidIds.get(playerUUID); + if (bidIds != null) count += bidIds.size(); + // Can't get claims for offline player without loading their data + return count; + } + + /** + * Get the maximum trade slots for a player. + * Uses cached value if available, otherwise computes from permissions. + */ + public int getMaxTradesForPlayer(EntityPlayer player) { + UUID playerUUID = player.getUniqueID(); + + // Check cache first + Integer cached = playerMaxTradesCache.get(playerUUID); + if (cached != null) { + return cached; + } + + // Compute and cache + int maxTrades = computeMaxTradesForPlayer(player); + playerMaxTradesCache.put(playerUUID, maxTrades); + return maxTrades; + } + + /** + * Compute max trade slots from permissions. + * Checks for customnpcs.auction.trades.X permissions where X is the slot count. + * Uses the highest permission found, or DefaultMaxTrades if no permissions or lower. + * Capped at MAX_TRADE_SLOTS (45). + */ + private int computeMaxTradesForPlayer(EntityPlayer player) { + // Check for unlimited permission + if (CustomNpcsPermissions.hasCustomPermission(player, "customnpcs.auction.trades.*")) { + return ConfigMarket.MAX_TRADE_SLOTS; + } + + // Find highest allowed slot count from permissions + int highestAllowed = 0; + for (int i = 1; i <= ConfigMarket.MAX_TRADE_SLOTS; i++) { + String perm = "customnpcs.auction.trades." + i; + if (CustomNpcsPermissions.hasCustomPermission(player, perm)) { + highestAllowed = i; + } + } + + // Use default if no permissions found or permission is lower than default + if (highestAllowed == 0 || highestAllowed < ConfigMarket.DefaultMaxTrades) { + highestAllowed = ConfigMarket.DefaultMaxTrades; + } + + // Cap at maximum + return Math.min(highestAllowed, ConfigMarket.MAX_TRADE_SLOTS); + } + + /** + * Refresh a player's cached max trades (call when permissions change). + */ + public void refreshPlayerMaxTrades(EntityPlayer player) { + UUID playerUUID = player.getUniqueID(); + int maxTrades = computeMaxTradesForPlayer(player); + playerMaxTradesCache.put(playerUUID, maxTrades); + } + + /** + * Clear a player's cached max trades (call on logout). + */ + public void clearPlayerMaxTradesCache(UUID playerUUID) { + playerMaxTradesCache.remove(playerUUID); + } + + // ========================================= + // Notifications + // ========================================= + + /** + * Send a notification to a player. + * If player is online, sends immediately. Otherwise, discards (claims will notify on login). + */ + private void sendNotificationToPlayer(UUID playerUUID, EnumNotificationType type, String listingId, String message) { + // Try to send immediately if player is online + EntityPlayerMP player = (EntityPlayerMP) PlayerDataController.getPlayerFromUUID(playerUUID); + if (player != null) { + sendNotificationMessage(player, type, message); + } + // If offline, no need to store - they'll get notified about claims on login + } + + /** + * Send a notification chat message to a player. + */ + private void sendNotificationMessage(EntityPlayerMP player, EnumNotificationType type, String message) { + EnumChatFormatting color; + switch (type) { + case AUCTION_WON: + case AUCTION_SOLD: + color = EnumChatFormatting.GREEN; + break; + case AUCTION_OUTBID: + color = EnumChatFormatting.YELLOW; + break; + case AUCTION_EXPIRED: + color = EnumChatFormatting.RED; + break; + default: + color = EnumChatFormatting.GRAY; + } + player.addChatMessage(new ChatComponentText( + EnumChatFormatting.GOLD + "[Auction] " + color + message)); + } + + public void onPlayerLogin(EntityPlayer player) { + // Cache player's max trades permission + refreshPlayerMaxTrades(player); + + // Process expired claims for this player + PlayerData playerData = PlayerData.get(player); + if (playerData != null) { + int expired = playerData.tradeData.processExpiredClaims(); + if (expired > 0) { + playerData.updateClient = true; + playerData.save(); + logAuction("CLAIM_EXPIRED", player.getCommandSenderName(), "Multiple", expired, + expired + " claims expired on login"); + } + } + + // Check for pending claims and notify + List claims = getPlayerClaims(player); + if (!claims.isEmpty()) { + int itemClaims = 0; + int currencyClaims = 0; + long totalCurrency = 0; + + for (AuctionClaim claim : claims) { + if (claim.type.isItem()) { + itemClaims++; + } else { + currencyClaims++; + totalCurrency += claim.currency; + } + } + + StringBuilder msg = new StringBuilder(); + msg.append(EnumChatFormatting.GOLD).append("[Auction] "); + msg.append(EnumChatFormatting.YELLOW).append("You have "); + + if (itemClaims > 0 && currencyClaims > 0) { + msg.append(itemClaims).append(" item(s) and "); + msg.append(String.format("%,d", totalCurrency)).append(" ").append(ConfigMarket.CurrencyName); + } else if (itemClaims > 0) { + msg.append(itemClaims).append(" item(s)"); + } else { + msg.append(String.format("%,d", totalCurrency)).append(" ").append(ConfigMarket.CurrencyName); + } + msg.append(" to claim!"); + + player.addChatMessage(new ChatComponentText(msg.toString())); + } + } + + public void onPlayerLogout(UUID playerUUID) { + // Clear cached permission + clearPlayerMaxTradesCache(playerUUID); + } + + // ========================================= + // Logging + // ========================================= + + private void logAuction(String action, String playerName, String itemName, long amount, String details) { + if (!ConfigMarket.AuctionLoggingEnabled) return; + + boolean shouldLog = false; + switch (action) { + case "CREATED": + shouldLog = ConfigMarket.LogAuctionCreated; + break; + case "BID": + shouldLog = ConfigMarket.LogAuctionBid; + break; + case "BUYOUT": + shouldLog = ConfigMarket.LogAuctionBuyout; + break; + case "SOLD": + shouldLog = ConfigMarket.LogAuctionSold; + break; + case "EXPIRED": + shouldLog = ConfigMarket.LogAuctionExpired; + break; + case "CANCELLED": + shouldLog = ConfigMarket.LogAuctionCancelled; + break; + case "CLAIMED": + case "CLAIM_EXPIRED": + shouldLog = ConfigMarket.LogAuctionClaimed; + break; + } + + if (shouldLog) { + String logMessage = String.format("[AUCTION:%s] Player: %s, Item: %s, Amount: %d, Details: %s", + action, playerName, itemName, amount, details); + LogWriter.info(logMessage); + } + } + + // ========================================= + // Save / Load - Async on CNPC+ Thread + // ========================================= + + /** Mark data as dirty - will be saved on next save cycle */ + private void markDirty() { + dirty.set(true); + } + + /** Save synchronously - used for server shutdown */ + public void save() { + if (!dirty.compareAndSet(true, false)) { + return; // Nothing to save + } + saveInternal(); + } + + /** Save asynchronously on CNPC+ thread */ + public void saveAsync() { + if (!dirty.compareAndSet(true, false)) { + return; // Nothing to save + } + + if (!saving.compareAndSet(false, true)) { + // Another save is in progress, mark dirty again + dirty.set(true); + return; + } + + // Take a snapshot of the data for async save + final NBTTagCompound compound = writeToNBT(new NBTTagCompound()); + + CustomNPCsThreader.customNPCThread.execute(() -> { + try { + saveToFile(compound); + } catch (Exception e) { + LogWriter.error("Error saving auction data asynchronously", e); + // Mark dirty again so we retry + dirty.set(true); + } finally { + saving.set(false); + } + }); + } + + /** Internal synchronous save */ + private void saveInternal() { + try { + NBTTagCompound compound = writeToNBT(new NBTTagCompound()); + saveToFile(compound); + } catch (Exception e) { + LogWriter.error("Error saving auction data", e); + } + } + + private void saveToFile(NBTTagCompound compound) { + try { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) return; + + File fileNew = new File(saveDir, "auction.dat_new"); + File fileOld = new File(saveDir, "auction.dat_old"); + File fileCurrent = new File(saveDir, "auction.dat"); + + CompressedStreamTools.writeCompressed(compound, new FileOutputStream(fileNew)); + + // Backup rotation + if (fileOld.exists()) fileOld.delete(); + if (fileCurrent.exists()) fileCurrent.renameTo(fileOld); + if (fileCurrent.exists()) fileCurrent.delete(); + fileNew.renameTo(fileCurrent); + if (fileNew.exists()) fileNew.delete(); + + } catch (Exception e) { + LogWriter.error("Error saving auction data to file", e); + } + } + + public void load() { + listings.clear(); + playerListingIds.clear(); + playerBidIds.clear(); + + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) return; + filePath = saveDir.getAbsolutePath(); + + // Try current file + try { + File file = new File(saveDir, "auction.dat"); + if (file.exists()) { + loadFromFile(file); + rebuildIndices(); + return; + } + } catch (Exception e) { + LogWriter.error("Error loading auction.dat, trying backup", e); + } + + // Try backup file + try { + File file = new File(saveDir, "auction.dat_old"); + if (file.exists()) { + loadFromFile(file); + rebuildIndices(); + LogWriter.info("Loaded auction data from backup file"); + } + } catch (Exception e) { + LogWriter.error("Error loading auction.dat_old", e); + } + } + + /** Rebuild all indices from loaded data */ + private void rebuildIndices() { + playerListingIds.clear(); + playerBidIds.clear(); + + // Index listings + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE) { + addToPlayerListings(listing.sellerUUID, listing.id); + if (listing.highBidderUUID != null) { + addToPlayerBids(listing.highBidderUUID, listing.id); + } + } + } + + // Claims are now stored in PlayerData, not indexed here + LogWriter.info("Rebuilt auction indices: " + playerListingIds.size() + " sellers, " + + playerBidIds.size() + " bidders"); + } + + private void loadFromFile(File file) throws Exception { + NBTTagCompound compound = CompressedStreamTools.readCompressed(new FileInputStream(file)); + readFromNBT(compound); + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + // Listings only - claims are stored in PlayerData + NBTTagList listingsList = new NBTTagList(); + for (AuctionListing listing : listings.values()) { + listingsList.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + compound.setTag("Listings", listingsList); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + NBTTagList listingsList = compound.getTagList("Listings", 10); + for (int i = 0; i < listingsList.tagCount(); i++) { + AuctionListing listing = AuctionListing.fromNBT(listingsList.getCompoundTagAt(i)); + listings.put(listing.id, listing); + } + } + + // ========================================= + // IAuctionHandler Implementation + // ========================================= + + @Override + public boolean isEnabled() { + return ConfigMarket.AuctionEnabled; + } + + @Override + public IAuctionListing[] getActiveListings() { + List active = new ArrayList<>(); + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && !listing.isExpired()) { + active.add(listing); + } + } + return active.toArray(new IAuctionListing[0]); + } + + @Override + public IAuctionListing[] getListingsBySeller(String sellerUUID) { + if (sellerUUID == null) return new IAuctionListing[0]; + try { + UUID uuid = UUID.fromString(sellerUUID); + List result = getPlayerActiveListings(uuid); + return result.toArray(new IAuctionListing[0]); + } catch (IllegalArgumentException e) { + return new IAuctionListing[0]; + } + } + + @Override + public IAuctionListing[] getListingsByBidder(String bidderUUID) { + if (bidderUUID == null) return new IAuctionListing[0]; + try { + UUID uuid = UUID.fromString(bidderUUID); + List result = getPlayerActiveBids(uuid); + return result.toArray(new IAuctionListing[0]); + } catch (IllegalArgumentException e) { + return new IAuctionListing[0]; + } + } + + @Override + public int getActiveListingCount() { + int count = 0; + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && !listing.isExpired()) { + count++; + } + } + return count; + } + + @Override + public IAuctionListing createListing(IPlayer player, IItemStack item, long startingPrice, long buyoutPrice) { + if (player == null || item == null) return null; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + ItemStack mcItem = item.getMCItemStack(); + String result = createListing(entityPlayer, mcItem, startingPrice, buyoutPrice); + if (result != null) { + return null; // Failed + } + // Find the just-created listing + Set playerListings = playerListingIds.get(entityPlayer.getUniqueID()); + if (playerListings != null && !playerListings.isEmpty()) { + // Get the most recent one + long latestTime = 0; + AuctionListing latestListing = null; + for (String id : playerListings) { + AuctionListing listing = listings.get(id); + if (listing != null && listing.createdTime > latestTime) { + latestTime = listing.createdTime; + latestListing = listing; + } + } + return latestListing; + } + return null; + } + + @Override + public String placeBid(String listingId, IPlayer player, long amount) { + if (player == null) return "Player is null"; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + return placeBid(listingId, entityPlayer, amount); + } + + @Override + public String buyout(String listingId, IPlayer player) { + if (player == null) return "Player is null"; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + return buyout(listingId, entityPlayer); + } + + @Override + public String cancelListing(String listingId, IPlayer player, boolean isAdmin) { + if (player == null) return "Player is null"; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + return cancelListing(listingId, entityPlayer, isAdmin); + } + + @Override + public long getListingFee() { + return ConfigMarket.ListingFee; + } + + @Override + public double getSalesTaxPercent() { + return ConfigMarket.SalesTaxPercent; + } + + @Override + public double getMinBidIncrementPercent() { + return ConfigMarket.MinBidIncrementPercent; + } + + @Override + public int getAuctionDurationHours() { + return ConfigMarket.AuctionDurationHours; + } + + @Override + public int getSnipeProtectionMinutes() { + return ConfigMarket.SnipeProtectionMinutes; + } + + @Override + public String getCurrencyName() { + return ConfigMarket.CurrencyName; + } + + @Override + public long getMinimumListingPrice() { + return ConfigMarket.MinimumListingPrice; + } + + @Override + public IAuctionListing[] searchListings(String searchText) { + if (searchText == null || searchText.trim().isEmpty()) { + return getActiveListings(); + } + String search = searchText.toLowerCase().trim(); + List result = new ArrayList<>(); + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE || listing.isExpired()) continue; + String itemName = listing.item != null ? listing.item.getDisplayName().toLowerCase() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName.toLowerCase() : ""; + if (itemName.contains(search) || sellerName.contains(search)) { + result.add(listing); + } + } + return result.toArray(new IAuctionListing[0]); + } + + // ========================================= + // Admin Methods + // ========================================= + + public List getAllListings() { + return new ArrayList<>(listings.values()); + } + + public void adminCancelListing(String listingId, EntityPlayer admin, String reason) { + cancelListing(listingId, admin, true); + } + + public void clearEndedListings() { + // Remove ended/cancelled listings (claims are in PlayerData now) + listings.entrySet().removeIf(entry -> + entry.getValue().status == EnumAuctionStatus.CLAIMED || + entry.getValue().status == EnumAuctionStatus.ENDED || + entry.getValue().status == EnumAuctionStatus.CANCELLED); + + // Rebuild indices + rebuildIndices(); + markDirty(); + } + + /** + * Check if a player is the seller of a listing. + * Used for GUI visibility checks. + */ + public boolean isPlayerSeller(String listingId, UUID playerUUID) { + AuctionListing listing = listings.get(listingId); + return listing != null && listing.isSeller(playerUUID); + } + + /** + * Reload the item blacklist from config. + */ + public void reloadBlacklist() { + AuctionBlacklist.reload(); + } + + /** + * Check if an item is blacklisted. + */ + public boolean isItemBlacklisted(ItemStack item) { + return AuctionBlacklist.isBlacklisted(item); + } +} diff --git a/src/main/java/noppes/npcs/controllers/HookDefinition.java b/src/main/java/noppes/npcs/controllers/HookDefinition.java new file mode 100644 index 000000000..864e37ee3 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -0,0 +1,301 @@ +package noppes.npcs.controllers; + +import cpw.mods.fml.common.eventhandler.Cancelable; +import noppes.npcs.api.handler.IHookDefinition; +import noppes.npcs.janino.annotations.ParamName; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +/** + * Implementation of {@link IHookDefinition} with a fluent builder API. + * + *

Usage Examples

+ *
{@code
+ * // Recommended: auto-derives imports from event class
+ * HookDefinition.of("onDBCTransform", IDBCEvent.TransformEvent.class);
+ *
+ * // Full control with builder
+ * HookDefinition.builder("onDBCPowerUp")
+ *     .eventClass(IDBCEvent.PowerUpEvent.class)
+ *     .cancelable(true)
+ *     .build();
+ *
+ * // Simple hook (no event metadata)
+ * HookDefinition.simple("customHook");
+ * }
+ */ +public class HookDefinition implements IHookDefinition { + + private final String hookName; + private final String eventClassName; + private final String[] paramNames; + private final String[] requiredImports; + private final boolean cancelable; + + private transient Class cachedEventClass; + private transient boolean classResolutionAttempted; + + private HookDefinition(Builder builder) { + this.hookName = builder.hookName; + this.eventClassName = builder.eventClassName; + this.paramNames = builder.paramNames != null ? builder.paramNames : new String[]{"event"}; + this.requiredImports = builder.requiredImports != null ? builder.requiredImports : new String[0]; + this.cancelable = builder.cancelable; + } + + // ==================== IHookDefinition Implementation ==================== + + @Override + public String hookName() { + return hookName; + } + + @Override + public String eventClassName() { + return eventClassName; + } + + @Override + public String[] paramNames() { + return paramNames; + } + + @Override + public String[] requiredImports() { + return requiredImports; + } + + @Override + public boolean isCancelable() { + return cancelable; + } + + @Override + public Class getEventClass() { + if (classResolutionAttempted) { + return cachedEventClass; + } + + classResolutionAttempted = true; + if (eventClassName == null || eventClassName.isEmpty()) { + return null; + } + + try { + cachedEventClass = Class.forName(eventClassName); + } catch (ClassNotFoundException e) { + cachedEventClass = null; + } + + return cachedEventClass; + } + + // ==================== Factory Methods ==================== + + /** + * Create a hook definition from a hook name and event class. + * Automatically derives imports from the event class. + * + * @param hookName The hook function name + * @param eventClass The event class (e.g., INpcEvent.InitEvent.class) + * @return A hook definition with auto-derived imports + */ + public static HookDefinition of(String hookName, Class eventClass) { + return builder(hookName).eventClass(eventClass).build(); + } + + /** + * Create a simple hook definition with just a name (no event metadata). + * + * @param hookName The hook function name + * @return A minimal hook definition + */ + public static HookDefinition simple(String hookName) { + return builder(hookName).build(); + } + + /** + * Create a new builder for a hook definition. + * + * @param hookName The hook function name + * @return A new builder instance + */ + public static Builder builder(String hookName) { + return new Builder(hookName); + } + + /** + * Create a hook definition from an interface method. + * Automatically extracts metadata from the method signature and @ParamName annotations. + * This provides backward compatibility with the annotation-based hook definition approach. + * + * @param hookName The hook name (may differ from method name) + * @param method The method to extract metadata from + * @return A hook definition with extracted metadata + */ + public static HookDefinition fromMethod(String hookName, Method method) { + Builder builder = builder(hookName); + + // Extract event type from first parameter + if (method.getParameterCount() > 0) { + Class eventType = method.getParameterTypes()[0]; + builder.eventClass(eventType); + + // Auto-detect cancelable from event class + if (eventType.isAnnotationPresent(Cancelable.class)) { + builder.cancelable(true); + } + + // Build required imports from event type + String importName = getImportForClass(eventType); + if (importName != null) { + builder.requiredImports(importName); + } + } + + // Extract parameter names from @ParamName annotations + Parameter[] params = method.getParameters(); + if (params.length > 0) { + String[] names = new String[params.length]; + for (int i = 0; i < params.length; i++) { + ParamName annotation = params[i].getAnnotation(ParamName.class); + if (annotation != null) { + names[i] = annotation.value(); + } else { + // Fallback: use lowercase simple type name + names[i] = Character.toLowerCase(params[i].getType().getSimpleName().charAt(0)) + + params[i].getType().getSimpleName().substring(1); + } + } + builder.paramNames(names); + } + + return builder.build(); + } + + /** + * Create a hook definition from a method using its name as the hook name. + * + * @param method The method to extract metadata from + * @return A hook definition with extracted metadata + */ + public static HookDefinition fromMethod(Method method) { + return fromMethod(method.getName(), method); + } + + /** + * Get the import statement needed for a class. + * For nested classes, returns the enclosing class. + */ + private static String getImportForClass(Class clazz) { + if (clazz == null || clazz.isPrimitive()) { + return null; + } + + // For nested classes, get the top-level enclosing class + Class enclosing = clazz; + while (enclosing.getEnclosingClass() != null) { + enclosing = enclosing.getEnclosingClass(); + } + + String name = enclosing.getName(); + if (name.startsWith("java.lang.")) { + return null; + } + + return name; + } + + // ==================== Builder ==================== + + public static class Builder { + private final String hookName; + private String eventClassName; + private String[] paramNames; + private String[] requiredImports; + private boolean cancelable; + + private Builder(String hookName) { + if (hookName == null || hookName.isEmpty()) { + throw new IllegalArgumentException("hookName cannot be null or empty"); + } + this.hookName = hookName; + } + + /** + * Set the event class directly. + * Automatically derives: class name, required imports, and cancelable status. + */ + public Builder eventClass(Class clazz) { + if (clazz != null) { + this.eventClassName = clazz.getName(); + + String importName = getImportForClass(clazz); + if (importName != null) { + this.requiredImports = new String[]{importName}; + } + + if (clazz.isAnnotationPresent(Cancelable.class)) { + this.cancelable = true; + } + } + return this; + } + + /** + * Set the event class by name (for addon classes that may not be loaded). + */ + public Builder eventClass(String className) { + this.eventClassName = className; + return this; + } + + /** + * Set parameter names for stub generation. + */ + public Builder paramNames(String... names) { + this.paramNames = names; + return this; + } + + /** + * Set required imports (only needed if using eventClass(String)). + */ + public Builder requiredImports(String... imports) { + this.requiredImports = imports; + return this; + } + + /** + * Set whether the event is cancelable. + */ + public Builder cancelable(boolean cancelable) { + this.cancelable = cancelable; + return this; + } + + public HookDefinition build() { + return new HookDefinition(this); + } + } + + // ==================== Object Methods ==================== + + @Override + public String toString() { + return "HookDefinition{hookName='" + hookName + "', eventClass='" + eventClassName + "'}"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof HookDefinition)) return false; + return hookName.equals(((HookDefinition) obj).hookName); + } + + @Override + public int hashCode() { + return hookName.hashCode(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/LinkedItemController.java b/src/main/java/noppes/npcs/controllers/LinkedItemController.java index c2bc19044..78534a82f 100644 --- a/src/main/java/noppes/npcs/controllers/LinkedItemController.java +++ b/src/main/java/noppes/npcs/controllers/LinkedItemController.java @@ -7,7 +7,7 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import noppes.npcs.api.handler.data.ILinkedItem; import noppes.npcs.api.item.IItemStack; -import noppes.npcs.controllers.data.INpcScriptHandler; +import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.LinkedItem; import noppes.npcs.controllers.data.LinkedItemScript; import noppes.npcs.util.NBTJsonUtil; @@ -90,12 +90,12 @@ public class LinkedItemController { return this.linkedItems.containsValue(linkedItem); } - public INpcScriptHandler getScriptHandler(int id) { + public IScriptHandler getScriptHandler(int id) { return this.linkedItemsScripts.get(id); } - //////////////////////////////////////////////////////// - //////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// public void load() { lastUsedID = 0; @@ -247,10 +247,9 @@ public class LinkedItemController { } - //////////////////////////////////////////////////////// - //////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// // LINKED ITEMS MAP - public File getMapDir() { File dir = CustomNpcs.getWorldSaveDirectory(); if (!dir.exists()) diff --git a/src/main/java/noppes/npcs/controllers/MarketRegistry.java b/src/main/java/noppes/npcs/controllers/MarketRegistry.java index 1110f9e5f..55de3a0bf 100644 --- a/src/main/java/noppes/npcs/controllers/MarketRegistry.java +++ b/src/main/java/noppes/npcs/controllers/MarketRegistry.java @@ -15,7 +15,7 @@ import java.util.concurrent.ConcurrentHashMap; * Central registry for linked market synchronization. * Tracks which RoleTrader instances use which markets, * and which players are viewing each market. - * + *

* This enables immediate sync across all traders sharing * the same market when a purchase occurs. */ diff --git a/src/main/java/noppes/npcs/controllers/PlayerDataController.java b/src/main/java/noppes/npcs/controllers/PlayerDataController.java index c8964b31d..235e2a9f8 100644 --- a/src/main/java/noppes/npcs/controllers/PlayerDataController.java +++ b/src/main/java/noppes/npcs/controllers/PlayerDataController.java @@ -364,6 +364,17 @@ public class PlayerDataController { return data; } + public PlayerData getData(UUID uuid) { + EntityPlayer player = getPlayerFromUUID(uuid); + PlayerData data; + if (player == null) { + data = new PlayerData(); + data.setNBT(PlayerDataController.Instance.loadPlayerData(uuid.toString())); + } else + data = PlayerData.get(player); + return data; + } + public void addPlayerMessage(String username, PlayerMail mail) { mail.time = System.currentTimeMillis(); diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index 3ea46b6a7..738819500 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -7,6 +7,7 @@ import noppes.npcs.NBTTags; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.NpcAPI; import javax.script.Compilable; @@ -24,7 +25,7 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; -public class ScriptContainer { +public class ScriptContainer implements IScriptUnit { private static final String lock = "lock"; public static ScriptContainer Current; private static String CurrentType; @@ -69,6 +70,7 @@ public class ScriptContainer { } public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString(IScriptUnit.NBT_TYPE_KEY, IScriptUnit.TYPE_ECMASCRIPT); if (this.script.length() < 65535) { compound.setString("Script", this.script); } else { @@ -251,6 +253,7 @@ public class ScriptContainer { return evaluated && !errored; } + @Override public boolean hasCode() { if (!scripts.isEmpty()) return true; @@ -267,4 +270,77 @@ public class ScriptContainer { this.evaluated = false; } } + + // ==================== IScriptUnit IMPLEMENTATION ==================== + + @Override + public String getScript() { + return this.script; + } + + @Override + public void setScript(String script) { + this.script = script; + this.evaluated = false; + } + + @Override + public List getExternalScripts() { + return this.scripts; + } + + @Override + public void setExternalScripts(List scripts) { + this.scripts = scripts; + this.evaluated = false; + } + + @Override + public TreeMap getConsole() { + return this.console; + } + + @Override + public void clearConsole() { + this.console.clear(); + } + + @Override + public String getLanguage() { + // If a specific language is set for this container, use it + // Otherwise fall back to the handler's language + if (currentScriptLanguage != null) { + return currentScriptLanguage; + } + return handler != null ? handler.getLanguage() : "ECMAScript"; + } + + @Override + public void setLanguage(String language) { + if (!Objects.equals(this.currentScriptLanguage, language)) { + this.currentScriptLanguage = language; + this.evaluated = false; + } + } + + @Override + public String generateHookStub(String hookName, Object hookData) { + // ECMAScript function template + return "function " + hookName + "(event) {\n \n}\n"; + } + + @Override + public void ensureCompiled() { + } + + @Override + public boolean hasErrored() { + return this.errored; + } + + @Override + public void setErrored(boolean errored) { + this.errored = errored; + } } + diff --git a/src/main/java/noppes/npcs/controllers/ScriptController.java b/src/main/java/noppes/npcs/controllers/ScriptController.java index d4246440b..af2153cff 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptController.java @@ -3,7 +3,9 @@ package noppes.npcs.controllers; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import jdk.nashorn.api.scripting.ClassFilter; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; +import kamkeel.npcs.network.packets.request.script.ScriptFilesPacket; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.nbt.NBTTagString; @@ -21,6 +23,7 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.ScriptWorld; import noppes.npcs.util.JsonException; import noppes.npcs.util.NBTJsonUtil; +import somehussar.janino.EntityUnloadListener; import javax.script.ScriptContext; import javax.script.ScriptEngine; @@ -45,7 +48,7 @@ public class ScriptController { public static boolean HasStart = false; private final ScriptEngineManager manager; private ScriptEngineFactory nashornFactory; - public Map languages = new HashMap(); + public final Map languages = new HashMap(); public Map scripts = new HashMap(); public long lastLoaded = 0; public File dir; @@ -61,6 +64,12 @@ public class ScriptController { public GlobalNPCDataScript globalNpcScripts = new GlobalNPCDataScript((EntityNPCInterface) null); public long lastGlobalNpcUpdate = 0L; + /** + * Incremented whenever external scripts loaded. + */ + public int globalRevision; + + public ScriptController() { Instance = this; manager = new ScriptEngineManager(); @@ -97,6 +106,7 @@ public class ScriptController { } catch (Exception ignored) { } } + languages.put("Java", ".java"); } private File forgeScriptsFile() { @@ -124,7 +134,7 @@ public class ScriptController { File file = this.forgeScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.forgeScripts.writeToNBT(new NBTTagCompound())); - this.forgeScripts.lastInited = -1L; + this.forgeScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } @@ -160,7 +170,7 @@ public class ScriptController { File file = this.playerScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.playerScripts.writeToNBT(new NBTTagCompound())); - this.playerScripts.lastInited = -1L; + this.playerScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } @@ -202,7 +212,7 @@ public class ScriptController { File file = this.npcScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.globalNpcScripts.writeToNBT(new NBTTagCompound())); - this.globalNpcScripts.lastInited = -1L; + this.globalNpcScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } @@ -217,7 +227,7 @@ public class ScriptController { File file = this.playerScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.playerScripts.writeToNBT(new NBTTagCompound())); - this.playerScripts.lastInited = -1L; + this.playerScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } @@ -228,6 +238,14 @@ public class ScriptController { customNPCThread.execute(this::saveGlobalNpcScripts); } + public void syncClientScripts(EntityPlayerMP player) { + if (player != null) { + ScriptFilesPacket.sendToPlayer(player, "Java"); + } else { + ScriptFilesPacket.sendToAll("Java"); + } + } + public void loadCategories() { dir = new File(CustomNpcs.getWorldSaveDirectory(), "scripts"); if (!dir.exists()) @@ -245,6 +263,7 @@ public class ScriptController { loadDir(scriptDir, "", ext); } lastLoaded = System.currentTimeMillis(); + globalRevision++; } private void loadDir(File dir, String name, String ext) { @@ -348,7 +367,7 @@ public class ScriptController { return list; } - private List getScripts(String language) { + public List getScripts(String language) { List list = new ArrayList(); String ext = languages.get(language); if (ext == null) @@ -361,6 +380,11 @@ public class ScriptController { return list; } + @SubscribeEvent + public void addWorldAccess(WorldEvent.Load event) { + event.world.addWorldAccess(new EntityUnloadListener()); + } + @SubscribeEvent public void invoke(WorldEvent.Save event) { if (!shouldSave || event.world.isRemote || event.world != MinecraftServer.getServer().worldServers[0]) diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index 0a64be83c..f3bad8e9d 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -1,346 +1,451 @@ package noppes.npcs.controllers; +import noppes.npcs.api.event.IAbilityEvent; +import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.IBlockEvent; +import noppes.npcs.api.event.ICustomGuiEvent; +import noppes.npcs.api.event.ICustomNPCsEvent; +import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IFactionEvent; +import noppes.npcs.api.event.IForgeEvent; +import noppes.npcs.api.event.IItemEvent; +import noppes.npcs.api.event.ILinkedItemEvent; +import noppes.npcs.api.event.INpcEvent; +import noppes.npcs.api.event.IPartyEvent; +import noppes.npcs.api.event.IPlayerEvent; +import noppes.npcs.api.event.IProjectileEvent; +import noppes.npcs.api.event.IQuestEvent; +import noppes.npcs.api.event.IRecipeEvent; +import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.data.EffectScript; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.RecipeScript; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import static noppes.npcs.constants.EnumScriptType.*; +import static noppes.npcs.constants.ScriptContext.BLOCK; +import static noppes.npcs.constants.ScriptContext.EFFECT; +import static noppes.npcs.constants.ScriptContext.FORGE; +import static noppes.npcs.constants.ScriptContext.ITEM; +import static noppes.npcs.constants.ScriptContext.LINKED_ITEM; +import static noppes.npcs.constants.ScriptContext.NPC; +import static noppes.npcs.constants.ScriptContext.PLAYER; +import static noppes.npcs.constants.ScriptContext.RECIPE; + /** * Controller for managing script hooks. * Allows addon mods to register custom hooks that appear in script editor GUIs. + * + *

Addon Hook Registration

+ *
{@code
+ * ScriptHookController.Instance.registerHook("player",
+ *     HookDefinition.of("onDBCTransform", IDBCEvent.TransformEvent.class));
+ * }
*/ public class ScriptHookController implements IScriptHookHandler { public static ScriptHookController Instance; - // Built-in hooks per context - private final Map> builtInHooks = new HashMap<>(); - - // Addon-registered hooks per context - private final Map> addonHooks = new HashMap<>(); - - // All available contexts - private static final String[] CONTEXTS = { - CONTEXT_NPC, - CONTEXT_PLAYER, - CONTEXT_BLOCK, - CONTEXT_ITEM, - CONTEXT_LINKED_ITEM, - CONTEXT_FORGE, - CONTEXT_RECIPE, - CONTEXT_EFFECT - }; + private final Map> hookDefinitions = new HashMap<>(); + private int hookRevision = 0; public ScriptHookController() { Instance = this; initializeBuiltInHooks(); } + // ==================== INITIALIZATION ==================== + private void initializeBuiltInHooks() { - // Initialize empty lists for all contexts - for (String context : CONTEXTS) { - builtInHooks.put(context, new ArrayList<>()); - addonHooks.put(context, new ArrayList<>()); - } - - // NPC hooks - registerBuiltIn(CONTEXT_NPC, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.DIALOG.function, - EnumScriptType.DAMAGED.function, - EnumScriptType.KILLED.function, - EnumScriptType.ATTACK_MELEE.function, - EnumScriptType.ATTACK_SWING.function, - EnumScriptType.RANGED_LAUNCHED.function, - EnumScriptType.TARGET.function, - EnumScriptType.COLLIDE.function, - EnumScriptType.KILLS.function, - EnumScriptType.DIALOG_CLOSE.function, - EnumScriptType.TIMER.function, - EnumScriptType.TARGET_LOST.function, - EnumScriptType.PROJECTILE_TICK.function, - EnumScriptType.PROJECTILE_IMPACT.function, - EnumScriptType.ABILITY_TICK.function, - EnumScriptType.ABILITY_EXECUTE.function, - EnumScriptType.ABILITY_HIT.function, - EnumScriptType.ABILITY_START.function, - EnumScriptType.ABILITY_INTERRUPT.function, - EnumScriptType.ABILITY_COMPLETE.function - ); - - // Player hooks - registerBuiltIn(CONTEXT_PLAYER, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.ATTACKED.function, - EnumScriptType.DAMAGED_ENTITY.function, - EnumScriptType.DAMAGED.function, - EnumScriptType.KILLS.function, - EnumScriptType.KILLED.function, - EnumScriptType.DROP.function, - EnumScriptType.RESPAWN.function, - EnumScriptType.BREAK_BLOCK.function, - EnumScriptType.CHAT.function, - EnumScriptType.LOGIN.function, - EnumScriptType.LOGOUT.function, - EnumScriptType.KEY_PRESSED.function, - EnumScriptType.MOUSE_CLICKED.function, - EnumScriptType.TOSS.function, - EnumScriptType.PICKUP.function, - EnumScriptType.PICKUP_XP.function, - EnumScriptType.RANGED_CHARGE.function, - EnumScriptType.RANGED_LAUNCHED.function, - EnumScriptType.TIMER.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function, - EnumScriptType.CONTAINER_OPEN.function, - EnumScriptType.USE_HOE.function, - EnumScriptType.BONEMEAL.function, - EnumScriptType.FILL_BUCKET.function, - EnumScriptType.JUMP.function, - EnumScriptType.FALL.function, - EnumScriptType.WAKE_UP.function, - EnumScriptType.SLEEP.function, - EnumScriptType.PLAYSOUND.function, - EnumScriptType.LIGHTNING.function, - EnumScriptType.CHANGED_DIM.function, - EnumScriptType.QUEST_START.function, - EnumScriptType.QUEST_COMPLETED.function, - EnumScriptType.QUEST_TURNIN.function, - EnumScriptType.FACTION_POINTS.function, - EnumScriptType.DIALOG_OPEN.function, - EnumScriptType.DIALOG_OPTION.function, - EnumScriptType.DIALOG_CLOSE.function, - EnumScriptType.SCRIPT_COMMAND.function, - EnumScriptType.CUSTOM_GUI_CLOSED.function, - EnumScriptType.CUSTOM_GUI_BUTTON.function, - EnumScriptType.CUSTOM_GUI_SLOT.function, - EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, - EnumScriptType.CUSTOM_GUI_SCROLL.function, - EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, - EnumScriptType.PARTY_QUEST_COMPLETED.function, - EnumScriptType.PARTY_QUEST_SET.function, - EnumScriptType.PARTY_QUEST_TURNED_IN.function, - EnumScriptType.PARTY_INVITE.function, - EnumScriptType.PARTY_KICK.function, - EnumScriptType.PARTY_LEAVE.function, - EnumScriptType.PARTY_DISBAND.function, - EnumScriptType.ANIMATION_START.function, - EnumScriptType.ANIMATION_END.function, - EnumScriptType.ANIMATION_FRAME_ENTER.function, - EnumScriptType.ANIMATION_FRAME_EXIT.function, - EnumScriptType.PROFILE_CHANGE.function, - EnumScriptType.PROFILE_REMOVE.function, - EnumScriptType.PROFILE_CREATE.function, - EffectScript.ScriptType.OnEffectAdd.function, - EffectScript.ScriptType.OnEffectTick.function, - EffectScript.ScriptType.OnEffectRemove.function - ); - - // Block hooks - registerBuiltIn(CONTEXT_BLOCK, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.FALLEN_UPON.function, - EnumScriptType.REDSTONE.function, - EnumScriptType.BROKEN.function, - EnumScriptType.EXPLODED.function, - EnumScriptType.RAIN_FILLED.function, - EnumScriptType.NEIGHBOR_CHANGED.function, - EnumScriptType.CLICKED.function, - EnumScriptType.HARVESTED.function, - EnumScriptType.COLLIDE.function, - EnumScriptType.TIMER.function - ); - - // Item hooks - registerBuiltIn(CONTEXT_ITEM, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.TOSSED.function, - EnumScriptType.PICKEDUP.function, - EnumScriptType.SPAWN.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function - ); - - // Linked item hooks - registerBuiltIn(CONTEXT_LINKED_ITEM, - EnumScriptType.LINKED_ITEM_BUILD.function, - EnumScriptType.LINKED_ITEM_VERSION.function, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.TOSSED.function, - EnumScriptType.PICKEDUP.function, - EnumScriptType.SPAWN.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function - ); - - // Recipe hooks - registerBuiltIn(CONTEXT_RECIPE, - RecipeScript.ScriptType.PRE.function, - RecipeScript.ScriptType.POST.function - ); - - // Effect hooks - registerBuiltIn(CONTEXT_EFFECT, - EffectScript.ScriptType.OnEffectAdd.function, - EffectScript.ScriptType.OnEffectTick.function, - EffectScript.ScriptType.OnEffectRemove.function - ); - - // Forge hooks - just init, the rest are dynamically discovered - registerBuiltIn(CONTEXT_FORGE, - EnumScriptType.INIT.function - ); - } - - private void registerBuiltIn(String context, String... hooks) { - List list = builtInHooks.get(context); - if (list != null) { - for (String hook : hooks) { - if (!list.contains(hook)) { - list.add(hook); - } + for (ScriptContext context : ScriptContext.values()) { + if (!context.hookContext.isEmpty()) { + hookDefinitions.put(context.hookContext, new LinkedHashMap<>()); } } + + initializeNpcHooks(); + initializePlayerHooks(); + initializeBlockHooks(); + initializeItemHooks(); + initializeLinkedItemHooks(); + initializeRecipeHooks(); + initializeEffectHooks(); + initializeForgeHooks(); } - @Override - public void registerHook(String context, String functionName) { - if (context == null || functionName == null || functionName.isEmpty()) { + private void initializeNpcHooks() { + // Core lifecycle + hook(NPC, INIT, INpcEvent.InitEvent.class); + hook(NPC, TICK, INpcEvent.UpdateEvent.class); + hook(NPC, TIMER, INpcEvent.TimerEvent.class); + + // Interaction + hook(NPC, INTERACT, INpcEvent.InteractEvent.class); + hook(NPC, DIALOG, INpcEvent.DialogEvent.class); + hook(NPC, DIALOG_CLOSE, INpcEvent.DialogClosedEvent.class); + hook(NPC, COLLIDE, INpcEvent.CollideEvent.class); + + // Combat + hook(NPC, DAMAGED, INpcEvent.DamagedEvent.class); + hook(NPC, KILLED, INpcEvent.DiedEvent.class); + hook(NPC, KILLS, INpcEvent.KilledEntityEvent.class); + hook(NPC, ATTACK_MELEE, INpcEvent.MeleeAttackEvent.class); + hook(NPC, ATTACK_SWING, INpcEvent.SwingEvent.class); + hook(NPC, RANGED_LAUNCHED, INpcEvent.RangedLaunchedEvent.class); + hook(NPC, TARGET, INpcEvent.TargetEvent.class); + hook(NPC, TARGET_LOST, INpcEvent.TargetLostEvent.class); + + // Projectile + hook(NPC, PROJECTILE_TICK, IProjectileEvent.UpdateEvent.class); + hook(NPC, PROJECTILE_IMPACT, IProjectileEvent.ImpactEvent.class); + + // Ability + hook(NPC, ABILITY_START, IAbilityEvent.StartEvent.class); + hook(NPC, ABILITY_EXECUTE, IAbilityEvent.ExecuteEvent.class); + hook(NPC, ABILITY_HIT, IAbilityEvent.HitEvent.class); + hook(NPC, ABILITY_TICK, IAbilityEvent.TickEvent.class); + hook(NPC, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class); + hook(NPC, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class); + } + + private void initializePlayerHooks() { + // Core lifecycle + hook(PLAYER, INIT, IPlayerEvent.InitEvent.class); + hook(PLAYER, TICK, IPlayerEvent.UpdateEvent.class); + hook(PLAYER, TIMER, IPlayerEvent.TimerEvent.class); + + // Combat + hook(PLAYER, ATTACK, IPlayerEvent.AttackEvent.class); + hook(PLAYER, ATTACKED, IPlayerEvent.AttackedEvent.class); + hook(PLAYER, DAMAGED, IPlayerEvent.DamagedEvent.class); + hook(PLAYER, DAMAGED_ENTITY, IPlayerEvent.DamagedEntityEvent.class); + hook(PLAYER, KILLS, IPlayerEvent.KilledEntityEvent.class); + hook(PLAYER, KILLED, IPlayerEvent.DiedEvent.class); + + // Interaction + hook(PLAYER, INTERACT, IPlayerEvent.InteractEvent.class); + hook(PLAYER, RIGHT_CLICK, IPlayerEvent.RightClickEvent.class); + hook(PLAYER, BREAK_BLOCK, IPlayerEvent.BreakEvent.class); + hook(PLAYER, CHAT, IPlayerEvent.ChatEvent.class); + + // Connection + hook(PLAYER, LOGIN, IPlayerEvent.LoginEvent.class); + hook(PLAYER, LOGOUT, IPlayerEvent.LogoutEvent.class); + hook(PLAYER, RESPAWN, IPlayerEvent.RespawnEvent.class); + + // Input + hook(PLAYER, KEY_PRESSED, IPlayerEvent.KeyPressedEvent.class); + hook(PLAYER, MOUSE_CLICKED, IPlayerEvent.MouseClickedEvent.class); + + // Items + hook(PLAYER, PICKUP, IPlayerEvent.PickUpEvent.class); + hook(PLAYER, TOSS, IPlayerEvent.TossEvent.class); + hook(PLAYER, DROP, IPlayerEvent.DropEvent.class); + hook(PLAYER, PICKUP_XP, IPlayerEvent.PickupXPEvent.class); + hook(PLAYER, START_USING_ITEM, IPlayerEvent.StartUsingItem.class); + hook(PLAYER, USING_ITEM, IPlayerEvent.UsingItem.class); + hook(PLAYER, STOP_USING_ITEM, IPlayerEvent.StopUsingItem.class); + hook(PLAYER, FINISH_USING_ITEM, IPlayerEvent.FinishUsingItem.class); + hook(PLAYER, CONTAINER_OPEN, IPlayerEvent.ContainerOpen.class); + + // Movement + hook(PLAYER, JUMP, IPlayerEvent.JumpEvent.class); + hook(PLAYER, FALL, IPlayerEvent.FallEvent.class); + hook(PLAYER, CHANGED_DIM, IPlayerEvent.ChangedDimension.class); + + // World + hook(PLAYER, RANGED_CHARGE, IPlayerEvent.RangedChargeEvent.class); + hook(PLAYER, RANGED_LAUNCHED, IPlayerEvent.RangedLaunchedEvent.class); + hook(PLAYER, USE_HOE, IPlayerEvent.UseHoeEvent.class); + hook(PLAYER, BONEMEAL, IPlayerEvent.BonemealEvent.class); + hook(PLAYER, FILL_BUCKET, IPlayerEvent.FillBucketEvent.class); + hook(PLAYER, WAKE_UP, IPlayerEvent.WakeUpEvent.class); + hook(PLAYER, SLEEP, IPlayerEvent.SleepEvent.class); + hook(PLAYER, PLAYSOUND, IPlayerEvent.SoundEvent.class); + hook(PLAYER, LIGHTNING, IPlayerEvent.LightningEvent.class); + hook(PLAYER, SCRIPT_COMMAND, ICustomNPCsEvent.ScriptedCommandEvent.class); + + // Quest + hook(PLAYER, QUEST_START, IQuestEvent.QuestStartEvent.class); + hook(PLAYER, QUEST_COMPLETED, IQuestEvent.QuestCompletedEvent.class); + hook(PLAYER, QUEST_TURNIN, IQuestEvent.QuestTurnedInEvent.class); + + // Dialog + hook(PLAYER, DIALOG_OPEN, IDialogEvent.DialogOpen.class); + hook(PLAYER, DIALOG_OPTION, IDialogEvent.DialogOption.class); + hook(PLAYER, DIALOG_CLOSE, IDialogEvent.DialogClosed.class); + + // Faction + hook(PLAYER, FACTION_POINTS, IFactionEvent.FactionPoints.class); + + // Custom GUI + hook(PLAYER, CUSTOM_GUI_CLOSED, ICustomGuiEvent.CloseEvent.class); + hook(PLAYER, CUSTOM_GUI_BUTTON, ICustomGuiEvent.ButtonEvent.class); + hook(PLAYER, CUSTOM_GUI_SLOT, ICustomGuiEvent.SlotEvent.class); + hook(PLAYER, CUSTOM_GUI_SLOT_CLICKED, ICustomGuiEvent.SlotClickEvent.class); + hook(PLAYER, CUSTOM_GUI_SCROLL, ICustomGuiEvent.ScrollEvent.class); + hook(PLAYER, CUSTOM_GUI_TEXTFIELD, ICustomGuiEvent.UnfocusedEvent.class); + + // Party + hook(PLAYER, PARTY_QUEST_COMPLETED, IPartyEvent.PartyQuestCompletedEvent.class); + hook(PLAYER, PARTY_QUEST_SET, IPartyEvent.PartyQuestSetEvent.class); + hook(PLAYER, PARTY_QUEST_TURNED_IN, IPartyEvent.PartyQuestTurnedInEvent.class); + hook(PLAYER, PARTY_INVITE, IPartyEvent.PartyInviteEvent.class); + hook(PLAYER, PARTY_KICK, IPartyEvent.PartyKickEvent.class); + hook(PLAYER, PARTY_LEAVE, IPartyEvent.PartyLeaveEvent.class); + hook(PLAYER, PARTY_DISBAND, IPartyEvent.PartyDisbandEvent.class); + + // Animation + hook(PLAYER, ANIMATION_START, IAnimationEvent.Started.class); + hook(PLAYER, ANIMATION_END, IAnimationEvent.Ended.class); + hook(PLAYER, ANIMATION_FRAME_ENTER, IAnimationEvent.IFrameEvent.Entered.class); + hook(PLAYER, ANIMATION_FRAME_EXIT, IAnimationEvent.IFrameEvent.Exited.class); + + // Profile + hook(PLAYER, PROFILE_CHANGE, IPlayerEvent.ProfileEvent.Changed.class); + hook(PLAYER, PROFILE_REMOVE, IPlayerEvent.ProfileEvent.Removed.class); + hook(PLAYER, PROFILE_CREATE, IPlayerEvent.ProfileEvent.Create.class); + + // Effect (player context) + hook(PLAYER, ON_EFFECT_ADD, IPlayerEvent.EffectEvent.Added.class); + hook(PLAYER, ON_EFFECT_TICK, IPlayerEvent.EffectEvent.Ticked.class); + hook(PLAYER, ON_EFFECT_REMOVE, IPlayerEvent.EffectEvent.Removed.class); + } + + private void initializeBlockHooks() { + hook(BLOCK, INIT, IBlockEvent.InitEvent.class); + hook(BLOCK, TICK, IBlockEvent.UpdateEvent.class); + hook(BLOCK, INTERACT, IBlockEvent.InteractEvent.class); + hook(BLOCK, FALLEN_UPON, IBlockEvent.EntityFallenUponEvent.class); + hook(BLOCK, REDSTONE, IBlockEvent.RedstoneEvent.class); + hook(BLOCK, BROKEN, IBlockEvent.BreakEvent.class); + hook(BLOCK, EXPLODED, IBlockEvent.ExplodedEvent.class); + hook(BLOCK, RAIN_FILLED, IBlockEvent.RainFillEvent.class); + hook(BLOCK, NEIGHBOR_CHANGED, IBlockEvent.NeighborChangedEvent.class); + hook(BLOCK, CLICKED, IBlockEvent.ClickedEvent.class); + hook(BLOCK, HARVESTED, IBlockEvent.HarvestedEvent.class); + hook(BLOCK, COLLIDE, IBlockEvent.CollidedEvent.class); + hook(BLOCK, TIMER, IBlockEvent.TimerEvent.class); + } + + private void initializeItemHooks() { + hook(ITEM, INIT, IItemEvent.InitEvent.class); + hook(ITEM, TICK, IItemEvent.UpdateEvent.class); + hook(ITEM, TOSSED, IItemEvent.TossedEvent.class); + hook(ITEM, PICKEDUP, IItemEvent.PickedUpEvent.class); + hook(ITEM, SPAWN, IItemEvent.SpawnEvent.class); + hook(ITEM, INTERACT, IItemEvent.InteractEvent.class); + hook(ITEM, RIGHT_CLICK, IItemEvent.RightClickEvent.class); + hook(ITEM, ATTACK, IItemEvent.AttackEvent.class); + hook(ITEM, START_USING_ITEM, IItemEvent.StartUsingItem.class); + hook(ITEM, USING_ITEM, IItemEvent.UsingItem.class); + hook(ITEM, STOP_USING_ITEM, IItemEvent.StopUsingItem.class); + hook(ITEM, FINISH_USING_ITEM, IItemEvent.FinishUsingItem.class); + } + + private void initializeLinkedItemHooks() { + // Linked item specific + hook(LINKED_ITEM, LINKED_ITEM_BUILD, ILinkedItemEvent.BuildEvent.class); + hook(LINKED_ITEM, LINKED_ITEM_VERSION, ILinkedItemEvent.VersionChangeEvent.class); + + // Standard item hooks + hook(LINKED_ITEM, INIT, IItemEvent.InitEvent.class); + hook(LINKED_ITEM, TICK, IItemEvent.UpdateEvent.class); + hook(LINKED_ITEM, TOSSED, IItemEvent.TossedEvent.class); + hook(LINKED_ITEM, PICKEDUP, IItemEvent.PickedUpEvent.class); + hook(LINKED_ITEM, SPAWN, IItemEvent.SpawnEvent.class); + hook(LINKED_ITEM, INTERACT, IItemEvent.InteractEvent.class); + hook(LINKED_ITEM, RIGHT_CLICK, IItemEvent.RightClickEvent.class); + hook(LINKED_ITEM, ATTACK, IItemEvent.AttackEvent.class); + hook(LINKED_ITEM, START_USING_ITEM, IItemEvent.StartUsingItem.class); + hook(LINKED_ITEM, USING_ITEM, IItemEvent.UsingItem.class); + hook(LINKED_ITEM, STOP_USING_ITEM, IItemEvent.StopUsingItem.class); + hook(LINKED_ITEM, FINISH_USING_ITEM, IItemEvent.FinishUsingItem.class); + } + + private void initializeRecipeHooks() { + hook(RECIPE, RecipeScript.ScriptType.PRE.function, IRecipeEvent.Pre.class); + hook(RECIPE, RecipeScript.ScriptType.POST.function, IRecipeEvent.Post.class); + } + + private void initializeEffectHooks() { + // Effect context uses IPlayerEvent.EffectEvent (same as player context) + hook(EFFECT, ON_EFFECT_ADD, IPlayerEvent.EffectEvent.Added.class); + hook(EFFECT, ON_EFFECT_TICK, IPlayerEvent.EffectEvent.Ticked.class); + hook(EFFECT, ON_EFFECT_REMOVE, IPlayerEvent.EffectEvent.Removed.class); + } + + private void initializeForgeHooks() { + hook(FORGE, INIT, IForgeEvent.InitEvent.class); + hook(FORGE, FORGE_WORLD, IForgeEvent.WorldEvent.class); + hook(FORGE, FORGE_ENTITY, IForgeEvent.EntityEvent.class); + hook(FORGE, CNPC_NATURAL_SPAWN, ICustomNPCsEvent.CNPCNaturalSpawnEvent.class); + } + + // ==================== INTERNAL REGISTRATION ==================== + + private void hook(ScriptContext context, EnumScriptType type, Class eventClass) { + hookDefinitions.get(context.hookContext) + .put(type.function, HookDefinition.of(type.function, eventClass)); + } + + private void hook(ScriptContext context, EnumScriptType type) { + hookDefinitions.get(context.hookContext) + .put(type.function, HookDefinition.simple(type.function)); + } + + private void hook(ScriptContext context, String hookName) { + hookDefinitions.get(context.hookContext) + .put(hookName, HookDefinition.simple(hookName)); + } + + private void hook(ScriptContext context, String hookName, Class eventClass) { + hookDefinitions.get(context.hookContext) + .put(hookName, HookDefinition.of(hookName, eventClass)); + } + + // ==================== PUBLIC API ==================== + + /** + * Register a hook with event class (auto-derives imports). + * For addon mods to register custom hooks. + * + * @param context Hook context (e.g., "npc", "player") + * @param hookName Function name in scripts + * @param eventClass The event class (imports auto-derived from enclosing class) + */ + public void registerHook(String context, String hookName, Class eventClass) { + if (context == null || hookName == null) return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); } - List hooks = addonHooks.get(context); - if (hooks == null) { - // Custom context - create it - hooks = new ArrayList<>(); - addonHooks.put(context, hooks); - } - - if (!hooks.contains(functionName) && !isBuiltInHook(context, functionName)) { - hooks.add(functionName); - } + contextDefs.put(hookName, HookDefinition.of(hookName, eventClass)); + hookRevision++; } - @Override - public void registerHooks(String functionName, String... contexts) { - if (functionName == null || functionName.isEmpty() || contexts == null) { + public void registerHook(ScriptContext context, String hookName, Class eventClass) { + registerHook(context.hookContext, hookName, eventClass); + } + + /** + * Register a simple hook with no event class metadata. + */ + public void registerHook(String context, String hookName) { + if (context == null || hookName == null) return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); } - for (String context : contexts) { - registerHook(context, functionName); + if (!contextDefs.containsKey(hookName)) { + contextDefs.put(hookName, HookDefinition.simple(hookName)); + hookRevision++; } } - @Override - public void unregisterHook(String context, String functionName) { - if (context == null || functionName == null) { + public void registerHook(ScriptContext context, String hookName) { + registerHook(context.hookContext, hookName); + } + + /** + * Register a hook definition for addon mods. + */ + public void registerHook(String context, HookDefinition definition) { + if (context == null || definition == null) return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); } - List hooks = addonHooks.get(context); - if (hooks != null) { - hooks.remove(functionName); + contextDefs.put(definition.hookName(), definition); + hookRevision++; + } + + public void registerHook(ScriptContext context, HookDefinition definition) { + registerHook(context.hookContext, definition); + } + + @Override + public void registerHookDefinition(String context, IHookDefinition definition) { + if (definition instanceof HookDefinition) { + registerHook(context, (HookDefinition) definition); + } else if (definition != null) { + registerHook(context, HookDefinition.builder(definition.hookName()) + .eventClass(definition.eventClassName()) + .paramNames(definition.paramNames()) + .requiredImports(definition.requiredImports()) + .cancelable(definition.isCancelable()) + .build()); } } @Override - public void unregisterHookFromAll(String functionName) { - if (functionName == null) { - return; - } + public IHookDefinition getHookDefinition(String context, String hookName) { + if (context == null || hookName == null) + return null; - for (List hooks : addonHooks.values()) { - hooks.remove(functionName); - } + Map contextDefs = hookDefinitions.get(context); + return contextDefs != null ? contextDefs.get(hookName) : null; } @Override - public List getAddonHooks(String context) { - List hooks = addonHooks.get(context); - return hooks != null ? Collections.unmodifiableList(hooks) : Collections.emptyList(); + public List getAllHookDefinitions(String context) { + if (context == null) + return Collections.emptyList(); + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null || contextDefs.isEmpty()) + return Collections.emptyList(); + + return new ArrayList<>(contextDefs.values()); } @Override public List getAllHooks(String context) { - List result = new ArrayList<>(); + if (context == null) + return Collections.emptyList(); - // Add built-in hooks - List builtIn = builtInHooks.get(context); - if (builtIn != null) { - result.addAll(builtIn); - } + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null || contextDefs.isEmpty()) + return Collections.emptyList(); - // Add addon hooks - List addon = addonHooks.get(context); - if (addon != null) { - for (String hook : addon) { - if (!result.contains(hook)) { - result.add(hook); - } - } - } - - return result; + return new ArrayList<>(contextDefs.keySet()); } @Override - public boolean hasHook(String context, String functionName) { - if (context == null || functionName == null) { + public boolean hasHook(String context, String hookName) { + if (context == null || hookName == null) return false; - } - return isBuiltInHook(context, functionName) || isAddonHook(context, functionName); + Map contextDefs = hookDefinitions.get(context); + return contextDefs != null && contextDefs.containsKey(hookName); + } + + @Override + public int getHookRevision() { + return hookRevision; } @Override public String[] getContexts() { - return CONTEXTS.clone(); - } - - private boolean isBuiltInHook(String context, String functionName) { - List hooks = builtInHooks.get(context); - return hooks != null && hooks.contains(functionName); - } - - private boolean isAddonHook(String context, String functionName) { - List hooks = addonHooks.get(context); - return hooks != null && hooks.contains(functionName); - } - - /** - * Get built-in hooks for a context (used by GUIs for backwards compatibility). - */ - public List getBuiltInHooks(String context) { - List hooks = builtInHooks.get(context); - return hooks != null ? Collections.unmodifiableList(hooks) : Collections.emptyList(); + return hookDefinitions.keySet().toArray(new String[0]); } } diff --git a/src/main/java/noppes/npcs/controllers/data/Animation.java b/src/main/java/noppes/npcs/controllers/data/Animation.java index c7d006198..8cf9b3827 100644 --- a/src/main/java/noppes/npcs/controllers/data/Animation.java +++ b/src/main/java/noppes/npcs/controllers/data/Animation.java @@ -185,16 +185,26 @@ public class Animation implements IAnimation { } @Override - public boolean hasData(String key) {return dataStore.containsKey(key);} - - @Override - public Object getData(String key) {return dataStore.get(key);} + public boolean hasData(String key) { + return dataStore.containsKey(key); + } @Override - public IAnimation setData(String key, Object v) {dataStore.put(key, v);return this;} + public Object getData(String key) { + return dataStore.get(key); + } @Override - public IAnimation removeData(String key) {dataStore.remove(key);return this;} + public IAnimation setData(String key, Object v) { + dataStore.put(key, v); + return this; + } + + @Override + public IAnimation removeData(String key) { + dataStore.remove(key); + return this; + } public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("ID")) { @@ -323,13 +333,13 @@ public class Animation implements IAnimation { this.onAnimationStart = task; return this; } - + @Override public IAnimation onFrame(BiConsumer task) { this.onAnimationFrame = task; return this; } - + @Override public IAnimation onEnd(Consumer task) { this.onAnimationEnd = task; @@ -355,7 +365,7 @@ public class Animation implements IAnimation { } /** - * Only called on setAnimation, to transfer tasks from + * Only called on setAnimation, to transfer tasks from * this global instance stored in AnimationController * to the new local instance on entity's AnimationData, * and clear them from the global instance. @@ -372,6 +382,6 @@ public class Animation implements IAnimation { this.dataStore.putAll(animation.dataStore); animation.dataStore.clear(); } - - + + } diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java new file mode 100644 index 000000000..75a1a714a --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java @@ -0,0 +1,237 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.registry.GameRegistry; +import kamkeel.npcs.util.AttributeItemUtil; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.config.ConfigMarket; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +public class AuctionBlacklist { + private static Set blacklistedItems = new HashSet<>(); + private static Set wildcardPatterns = new HashSet<>(); + private static Set blacklistedMods = new HashSet<>(); + private static Set blacklistedNBTTags = new HashSet<>(); + + // Built-in bound item requirement keys (always blocked) + private static final String SOULBIND_KEY = "cnpc_soulbind"; + private static final String PROFILE_SLOT_KEY = "cnpc_profile_slot"; + + // Bypass permission + private static final String BYPASS_PERMISSION = "customnpcs.auction.blacklist.bypass"; + + public static void reload() { + blacklistedItems.clear(); + wildcardPatterns.clear(); + blacklistedMods.clear(); + blacklistedNBTTags.clear(); + + // Load blacklisted items + for (String item : ConfigMarket.BlacklistedItems) { + if (item == null || item.isEmpty()) continue; + item = item.toLowerCase().trim(); + + if (item.contains("*")) { + String regex = "^" + item.replace(".", "\\.").replace("*", ".*") + "$"; + wildcardPatterns.add(Pattern.compile(regex)); + } else { + blacklistedItems.add(item); + } + } + + // Load blacklisted mods + for (String mod : ConfigMarket.BlacklistedMods) { + if (mod == null || mod.isEmpty()) continue; + blacklistedMods.add(mod.toLowerCase().trim()); + } + + // Load blacklisted NBT tags + for (String tag : ConfigMarket.BlacklistedNBTTags) { + if (tag == null || tag.isEmpty()) continue; + blacklistedNBTTags.add(tag.trim()); + } + } + + /** + * Check if player can bypass the blacklist (OP or has permission) + */ + public static boolean canBypass(EntityPlayer player) { + if (player == null) return false; + + // Check OP status + if (player.canCommandSenderUseCommand(2, BYPASS_PERMISSION)) { + return true; + } + + // Check custom permission + return CustomNpcsPermissions.hasCustomPermission(player, BYPASS_PERMISSION); + } + + /** + * Check if item is blacklisted (without bypass check) + */ + public static boolean isBlacklisted(ItemStack item) { + if (!ConfigMarket.BlacklistEnabled) return false; + if (item == null || item.getItem() == null) return true; + + // Check bound items (always blocked, even if blacklist is "disabled") + if (hasBoundRequirement(item)) { + return true; + } + + // Get registry name + GameRegistry.UniqueIdentifier uid = GameRegistry.findUniqueIdentifierFor(item.getItem()); + if (uid == null) return true; + + String registryName = (uid.modId + ":" + uid.name).toLowerCase(); + String modId = uid.modId.toLowerCase(); + + // Check exact item match + if (blacklistedItems.contains(registryName)) { + return true; + } + + // Check wildcard patterns + for (Pattern pattern : wildcardPatterns) { + if (pattern.matcher(registryName).matches()) { + return true; + } + } + + // Check mod blacklist + if (blacklistedMods.contains(modId)) { + return true; + } + + // Check NBT tags + if (item.hasTagCompound() && !blacklistedNBTTags.isEmpty()) { + NBTTagCompound tag = item.getTagCompound(); + for (String nbtKey : blacklistedNBTTags) { + if (tag.hasKey(nbtKey)) { + return true; + } + } + } + + return false; + } + + /** + * Check if item is blacklisted for a specific player (with bypass check) + */ + public static boolean isBlacklistedForPlayer(ItemStack item, EntityPlayer player) { + if (canBypass(player)) return false; + return isBlacklisted(item); + } + + /** + * Get the registry name of an item + */ + public static String getRegistryName(ItemStack item) { + if (item == null || item.getItem() == null) return null; + GameRegistry.UniqueIdentifier uid = GameRegistry.findUniqueIdentifierFor(item.getItem()); + if (uid == null) return null; + return uid.modId + ":" + uid.name; + } + + // ========================================= + // Bound Item Checking (RPGCore/Requirements) + // ========================================= + + /** + * Check if item has soulbind or profile slot requirement. + * Requirements are stored under: item.stackTagCompound/RPGCore/Requirements + */ + private static boolean hasBoundRequirement(ItemStack item) { + if (item == null || !item.hasTagCompound()) return false; + + NBTTagCompound root = item.getTagCompound(); + if (!root.hasKey(AttributeItemUtil.TAG_RPGCORE)) return false; + + NBTTagCompound rpgCore = root.getCompoundTag(AttributeItemUtil.TAG_RPGCORE); + if (!rpgCore.hasKey(AttributeItemUtil.TAG_REQUIREMENTS)) return false; + + NBTTagCompound requirements = rpgCore.getCompoundTag(AttributeItemUtil.TAG_REQUIREMENTS); + return requirements.hasKey(SOULBIND_KEY) || requirements.hasKey(PROFILE_SLOT_KEY); + } + + // ========================================= + // Runtime Blacklist Management + // ========================================= + + public static boolean addItem(String registryName) { + if (registryName == null || registryName.isEmpty()) return false; + registryName = registryName.toLowerCase().trim(); + + if (registryName.contains("*")) { + String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; + wildcardPatterns.add(Pattern.compile(regex)); + } else { + blacklistedItems.add(registryName); + } + return true; + } + + public static boolean removeItem(String registryName) { + if (registryName == null || registryName.isEmpty()) return false; + registryName = registryName.toLowerCase().trim(); + + if (registryName.contains("*")) { + String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; + return wildcardPatterns.removeIf(p -> p.pattern().equals(regex)); + } else { + return blacklistedItems.remove(registryName); + } + } + + public static boolean addMod(String modId) { + if (modId == null || modId.isEmpty()) return false; + return blacklistedMods.add(modId.toLowerCase().trim()); + } + + public static boolean removeMod(String modId) { + if (modId == null || modId.isEmpty()) return false; + return blacklistedMods.remove(modId.toLowerCase().trim()); + } + + public static boolean addNBTTag(String tag) { + if (tag == null || tag.isEmpty()) return false; + return blacklistedNBTTags.add(tag.trim()); + } + + public static boolean removeNBTTag(String tag) { + if (tag == null || tag.isEmpty()) return false; + return blacklistedNBTTags.remove(tag.trim()); + } + + // ========================================= + // Getters for listing + // ========================================= + + public static List getBlacklistedItems() { + List result = new ArrayList<>(blacklistedItems); + for (Pattern p : wildcardPatterns) { + // Convert regex back to wildcard format + String pattern = p.pattern(); + pattern = pattern.substring(1, pattern.length() - 1); // Remove ^ and $ + pattern = pattern.replace("\\.", ".").replace(".*", "*"); + result.add(pattern); + } + return result; + } + + public static List getBlacklistedMods() { + return new ArrayList<>(blacklistedMods); + } + + public static List getBlacklistedNBTTags() { + return new ArrayList<>(blacklistedNBTTags); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java new file mode 100644 index 000000000..ad4ae3238 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java @@ -0,0 +1,248 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.data.IAuctionClaim; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.scripted.NpcAPI; + +import java.util.UUID; + +public class AuctionClaim implements IAuctionClaim { + public String id; + public UUID playerUUID; + public String playerName; + public String listingId; + public EnumClaimType type; + public ItemStack item; // The item (for ITEM claims only) + public String itemName; // Display name of item (for CURRENCY/REFUND tooltip) + public String otherPlayerName; // Buyer name (CURRENCY) or who outbid (REFUND) + public long currency; + public long createdTime; + public boolean claimed; + public boolean isReturned; // For ITEM claims: true if returned to seller, false if won by buyer + + public AuctionClaim() { + this.id = UUID.randomUUID().toString(); + this.createdTime = System.currentTimeMillis(); + this.claimed = false; + this.isReturned = false; + this.itemName = ""; + this.otherPlayerName = ""; + } + + /** Create claim for winning an item (buyer won auction or buyout) */ + public static AuctionClaim createItemWonClaim(UUID playerUUID, String playerName, String listingId, ItemStack item) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.ITEM; + claim.item = item != null ? item.copy() : null; + claim.currency = 0; + claim.isReturned = false; + return claim; + } + + /** Create claim for returned item (seller's expired/cancelled listing) */ + public static AuctionClaim createItemReturnedClaim(UUID playerUUID, String playerName, String listingId, ItemStack item) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.ITEM; + claim.item = item != null ? item.copy() : null; + claim.currency = 0; + claim.isReturned = true; + return claim; + } + + /** Create claim for seller's proceeds (stores item name for tooltip, not full item) */ + public static AuctionClaim createCurrencyClaim(UUID playerUUID, String playerName, String listingId, long currency, String itemName, String buyerName) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.CURRENCY; + claim.item = null; // No item stored for currency claims + claim.itemName = itemName != null ? itemName : ""; + claim.otherPlayerName = buyerName != null ? buyerName : ""; + claim.currency = currency; + claim.isReturned = false; + return claim; + } + + /** Create claim for outbid refund (stores item for display and rebid option) */ + public static AuctionClaim createRefundClaim(UUID playerUUID, String playerName, String listingId, long currency, String itemName, String outbidderName, ItemStack item) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.REFUND; + claim.item = item != null ? item.copy() : null; // Store item for display and rebid + claim.itemName = itemName != null ? itemName : ""; + claim.otherPlayerName = outbidderName != null ? outbidderName : ""; + claim.currency = currency; + claim.isReturned = false; + return claim; + } + + public boolean isExpired(int expirationDays) { + long expirationMs = expirationDays * 24L * 60L * 60L * 1000L; + return System.currentTimeMillis() - createdTime > expirationMs; + } + + public long getDaysUntilExpiration(int expirationDays) { + long expirationMs = expirationDays * 24L * 60L * 60L * 1000L; + long remaining = (createdTime + expirationMs) - System.currentTimeMillis(); + return Math.max(0, remaining / (24L * 60L * 60L * 1000L)); + } + + public boolean isForPlayer(UUID uuid) { + return playerUUID != null && playerUUID.equals(uuid); + } + + // ========================================= + // IAuctionClaim Implementation + // ========================================= + + @Override + public String getId() { + return id; + } + + @Override + public String getPlayerUUID() { + return playerUUID != null ? playerUUID.toString() : null; + } + + @Override + public String getPlayerName() { + return playerName; + } + + @Override + public String getListingId() { + return listingId; + } + + @Override + public int getType() { + return type.ordinal(); + } + + @Override + public boolean isItemClaim() { + return type == EnumClaimType.ITEM; + } + + @Override + public boolean isCurrencyClaim() { + return type == EnumClaimType.CURRENCY; + } + + @Override + public boolean isRefundClaim() { + return type == EnumClaimType.REFUND; + } + + @Override + public IItemStack getItem() { + return item != null ? NpcAPI.Instance().getIItemStack(item) : null; + } + + @Override + public String getItemName() { + return itemName; + } + + @Override + public long getCurrency() { + return currency; + } + + @Override + public String getOtherPlayerName() { + return otherPlayerName; + } + + @Override + public long getCreatedTime() { + return createdTime; + } + + @Override + public boolean isClaimed() { + return claimed; + } + + @Override + public boolean isReturnedItem() { + return isReturned; + } + + @Override + public long getDaysUntilExpiration() { + return getDaysUntilExpiration(ConfigMarket.ClaimExpirationDays); + } + + @Override + public boolean isExpired() { + return isExpired(ConfigMarket.ClaimExpirationDays); + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("ID", id); + compound.setString("PlayerUUID", playerUUID != null ? playerUUID.toString() : ""); + compound.setString("PlayerName", playerName != null ? playerName : ""); + compound.setString("ListingID", listingId != null ? listingId : ""); + compound.setInteger("Type", type.ordinal()); + + if (item != null) { + NBTTagCompound itemTag = new NBTTagCompound(); + item.writeToNBT(itemTag); + compound.setTag("Item", itemTag); + } + + compound.setString("ItemName", itemName != null ? itemName : ""); + compound.setString("OtherPlayerName", otherPlayerName != null ? otherPlayerName : ""); + compound.setLong("Currency", currency); + compound.setLong("CreatedTime", createdTime); + compound.setBoolean("Claimed", claimed); + compound.setBoolean("IsReturned", isReturned); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + id = compound.getString("ID"); + + String playerUUIDStr = compound.getString("PlayerUUID"); + playerUUID = playerUUIDStr.isEmpty() ? null : UUID.fromString(playerUUIDStr); + playerName = compound.getString("PlayerName"); + + listingId = compound.getString("ListingID"); + type = EnumClaimType.fromOrdinal(compound.getInteger("Type")); + + if (compound.hasKey("Item")) { + item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + } else { + item = null; + } + + itemName = compound.getString("ItemName"); + otherPlayerName = compound.getString("OtherPlayerName"); + currency = compound.getLong("Currency"); + createdTime = compound.getLong("CreatedTime"); + claimed = compound.getBoolean("Claimed"); + isReturned = compound.getBoolean("IsReturned"); + } + + public static AuctionClaim fromNBT(NBTTagCompound compound) { + AuctionClaim claim = new AuctionClaim(); + claim.readFromNBT(compound); + return claim; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java new file mode 100644 index 000000000..95bf2ed24 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java @@ -0,0 +1,118 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumChatFormatting; +import noppes.npcs.constants.EnumAuctionSort; + +import java.util.regex.Pattern; + +/** + * Filter for auction listings. + * Supports search by item name or seller name and sorting options. + */ +public class AuctionFilter { + private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0-9a-fk-or]", Pattern.CASE_INSENSITIVE); + + public String searchText; + public EnumAuctionSort sortBy; + + // Cached normalized search for efficiency + private String normalizedSearch; + + public AuctionFilter() { + this.searchText = ""; + this.sortBy = EnumAuctionSort.NEWEST; + this.normalizedSearch = ""; + } + + public boolean hasSearchText() { + return searchText != null && !searchText.trim().isEmpty(); + } + + /** Normalize string for search (strip colors, lowercase, trim) */ + public static String normalizeForSearch(String input) { + if (input == null || input.isEmpty()) return ""; + String stripped = COLOR_CODE_PATTERN.matcher(input).replaceAll(""); + stripped = EnumChatFormatting.getTextWithoutFormattingCodes(stripped); + return stripped.toLowerCase().trim(); + } + + /** Check if item name or seller name matches search text */ + public boolean matchesSearch(String itemName, String sellerName) { + if (!hasSearchText()) return true; + String normalizedItem = normalizeForSearch(itemName); + String normalizedSeller = normalizeForSearch(sellerName); + // Match if search text is found in either item name or seller name + return normalizedItem.contains(normalizedSearch) || normalizedSeller.contains(normalizedSearch); + } + + /** Legacy method - only checks item name */ + public boolean matchesSearch(String itemName) { + return matchesSearch(itemName, ""); + } + + /** Advanced search - all words must be present in item name OR seller name */ + public boolean matchesSearchAdvanced(String itemName, String sellerName) { + if (!hasSearchText()) return true; + String normalizedItem = normalizeForSearch(itemName); + String normalizedSeller = normalizeForSearch(sellerName); + String[] words = normalizedSearch.split("\\s+"); + for (String word : words) { + if (!word.isEmpty()) { + // Word must be in item name OR seller name + if (!normalizedItem.contains(word) && !normalizedSeller.contains(word)) { + return false; + } + } + } + return true; + } + + /** Legacy method - only checks item name */ + public boolean matchesSearchAdvanced(String itemName) { + return matchesSearchAdvanced(itemName, ""); + } + + public void reset() { + searchText = ""; + sortBy = EnumAuctionSort.NEWEST; + normalizedSearch = ""; + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("SearchText", searchText != null ? searchText : ""); + compound.setInteger("SortBy", sortBy.ordinal()); + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + searchText = compound.getString("SearchText"); + normalizedSearch = normalizeForSearch(searchText); + sortBy = EnumAuctionSort.fromOrdinal(compound.getInteger("SortBy")); + } + + public static AuctionFilter fromNBT(NBTTagCompound compound) { + AuctionFilter filter = new AuctionFilter(); + filter.readFromNBT(compound); + return filter; + } + + public AuctionFilter copy() { + AuctionFilter copy = new AuctionFilter(); + copy.searchText = this.searchText; + copy.normalizedSearch = this.normalizedSearch; + copy.sortBy = this.sortBy; + return copy; + } + + // Getters and setters + public String getSearchText() { return searchText; } + + public void setSearchText(String searchText) { + this.searchText = searchText; + this.normalizedSearch = normalizeForSearch(searchText); + } + + public EnumAuctionSort getSortBy() { return sortBy; } + public void setSortBy(EnumAuctionSort sortBy) { this.sortBy = sortBy; } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java new file mode 100644 index 000000000..6d57942c1 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -0,0 +1,265 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.data.IAuctionListing; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.scripted.NpcAPI; + +import java.util.UUID; + +public class AuctionListing implements IAuctionListing { + public String id; + public UUID sellerUUID; + public String sellerName; + public ItemStack item; + public long startingPrice; + public long buyoutPrice; + public long currentBid; + public UUID highBidderUUID; + public String highBidderName; + public int bidCount; + public long createdTime; + public long endTime; + public EnumAuctionStatus status; + + public AuctionListing() { + this.id = UUID.randomUUID().toString(); + this.status = EnumAuctionStatus.ACTIVE; + this.bidCount = 0; + this.currentBid = 0; + this.createdTime = System.currentTimeMillis(); + } + + public AuctionListing(UUID sellerUUID, String sellerName, ItemStack item, long startingPrice, long buyoutPrice, long durationMs) { + this(); + this.sellerUUID = sellerUUID; + this.sellerName = sellerName; + this.item = item.copy(); + this.startingPrice = startingPrice; + this.buyoutPrice = buyoutPrice; + this.currentBid = 0; + this.endTime = this.createdTime + durationMs; + } + + public long getTimeRemaining() { + return Math.max(0, endTime - System.currentTimeMillis()); + } + + public long getEffectivePrice() { + return hasBids() ? currentBid : startingPrice; + } + + public long getMinimumBid(double minIncrementPercent) { + if (!hasBids()) { + return startingPrice; + } + long increment = (long) Math.ceil(currentBid * minIncrementPercent); + return currentBid + Math.max(1, increment); + } + + public boolean isSeller(UUID playerUUID) { + return sellerUUID != null && sellerUUID.equals(playerUUID); + } + + public boolean isHighBidder(UUID playerUUID) { + return highBidderUUID != null && highBidderUUID.equals(playerUUID); + } + + public void extendForSnipeProtection(int snipeProtectionMinutes) { + long snipeProtectionMs = snipeProtectionMinutes * 60 * 1000L; + long timeRemaining = getTimeRemaining(); + if (timeRemaining < snipeProtectionMs) { + endTime = System.currentTimeMillis() + snipeProtectionMs; + } + } + + // ========================================= + // IAuctionListing Implementation + // ========================================= + + @Override + public String getId() { + return id; + } + + @Override + public String getSellerUUID() { + return sellerUUID != null ? sellerUUID.toString() : null; + } + + @Override + public String getSellerName() { + return sellerName; + } + + @Override + public IItemStack getItem() { + return item != null ? NpcAPI.Instance().getIItemStack(item) : null; + } + + @Override + public long getStartingPrice() { + return startingPrice; + } + + @Override + public long getBuyoutPrice() { + return buyoutPrice; + } + + @Override + public long getCurrentBid() { + return currentBid; + } + + @Override + public String getHighBidderUUID() { + return highBidderUUID != null ? highBidderUUID.toString() : null; + } + + @Override + public String getHighBidderName() { + return highBidderName; + } + + @Override + public int getBidCount() { + return bidCount; + } + + @Override + public long getCreatedTime() { + return createdTime; + } + + @Override + public long getEndTime() { + return endTime; + } + + @Override + public long getRemainingTime() { + return getTimeRemaining(); + } + + @Override + public int getStatus() { + return status.ordinal(); + } + + @Override + public long getMinimumBid() { + return getMinimumBid(ConfigMarket.MinBidIncrementPercent); + } + + @Override + public boolean isSeller(String playerUUID) { + if (playerUUID == null || sellerUUID == null) return false; + return sellerUUID.toString().equals(playerUUID); + } + + @Override + public boolean isHighBidder(String playerUUID) { + if (playerUUID == null || highBidderUUID == null) return false; + return highBidderUUID.toString().equals(playerUUID); + } + + @Override + public String getRemainingTimeFormatted() { + long remaining = getTimeRemaining(); + if (remaining <= 0) return "Ended"; + + long hours = remaining / (60 * 60 * 1000); + long minutes = (remaining % (60 * 60 * 1000)) / (60 * 1000); + + if (hours > 0) { + return hours + "h " + minutes + "m"; + } else if (minutes > 0) { + return minutes + "m"; + } else { + long seconds = (remaining % (60 * 1000)) / 1000; + return seconds + "s"; + } + } + + // Override existing methods with @Override annotation + @Override + public boolean hasBids() { + return bidCount > 0 && highBidderUUID != null; + } + + @Override + public boolean hasBuyout() { + return buyoutPrice > 0; + } + + @Override + public boolean isExpired() { + return System.currentTimeMillis() >= endTime; + } + + @Override + public boolean isActive() { + return status == EnumAuctionStatus.ACTIVE && !isExpired(); + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("ID", id); + compound.setString("SellerUUID", sellerUUID != null ? sellerUUID.toString() : ""); + compound.setString("SellerName", sellerName != null ? sellerName : ""); + + if (item != null) { + NBTTagCompound itemTag = new NBTTagCompound(); + item.writeToNBT(itemTag); + compound.setTag("Item", itemTag); + } + + compound.setLong("StartingPrice", startingPrice); + compound.setLong("BuyoutPrice", buyoutPrice); + compound.setLong("CurrentBid", currentBid); + + compound.setString("HighBidderUUID", highBidderUUID != null ? highBidderUUID.toString() : ""); + compound.setString("HighBidderName", highBidderName != null ? highBidderName : ""); + + compound.setInteger("BidCount", bidCount); + compound.setLong("CreatedTime", createdTime); + compound.setLong("EndTime", endTime); + compound.setInteger("Status", status.ordinal()); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + id = compound.getString("ID"); + + String sellerUUIDStr = compound.getString("SellerUUID"); + sellerUUID = sellerUUIDStr.isEmpty() ? null : UUID.fromString(sellerUUIDStr); + sellerName = compound.getString("SellerName"); + + if (compound.hasKey("Item")) { + item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + } + + startingPrice = compound.getLong("StartingPrice"); + buyoutPrice = compound.getLong("BuyoutPrice"); + currentBid = compound.getLong("CurrentBid"); + + String highBidderUUIDStr = compound.getString("HighBidderUUID"); + highBidderUUID = highBidderUUIDStr.isEmpty() ? null : UUID.fromString(highBidderUUIDStr); + highBidderName = compound.getString("HighBidderName"); + + bidCount = compound.getInteger("BidCount"); + createdTime = compound.getLong("CreatedTime"); + endTime = compound.getLong("EndTime"); + status = EnumAuctionStatus.fromOrdinal(compound.getInteger("Status")); + } + + public static AuctionListing fromNBT(NBTTagCompound compound) { + AuctionListing listing = new AuctionListing(); + listing.readFromNBT(compound); + return listing; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionNotification.java b/src/main/java/noppes/npcs/controllers/data/AuctionNotification.java new file mode 100644 index 000000000..01ebd1e47 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionNotification.java @@ -0,0 +1,64 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumNotificationType; + +import java.util.UUID; + +public class AuctionNotification { + public String id; + public UUID playerUUID; + public EnumNotificationType type; + public String listingId; + public String message; + public long timestamp; + public boolean sent; + + public AuctionNotification() { + this.id = UUID.randomUUID().toString(); + this.timestamp = System.currentTimeMillis(); + this.sent = false; + } + + public AuctionNotification(UUID playerUUID, EnumNotificationType type, String listingId, String message) { + this(); + this.playerUUID = playerUUID; + this.type = type; + this.listingId = listingId; + this.message = message; + } + + public boolean isForPlayer(UUID uuid) { + return playerUUID != null && playerUUID.equals(uuid); + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("ID", id); + compound.setString("PlayerUUID", playerUUID != null ? playerUUID.toString() : ""); + compound.setInteger("Type", type.ordinal()); + compound.setString("ListingID", listingId != null ? listingId : ""); + compound.setString("Message", message != null ? message : ""); + compound.setLong("Timestamp", timestamp); + compound.setBoolean("Sent", sent); + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + id = compound.getString("ID"); + + String playerUUIDStr = compound.getString("PlayerUUID"); + playerUUID = playerUUIDStr.isEmpty() ? null : UUID.fromString(playerUUIDStr); + + type = EnumNotificationType.fromOrdinal(compound.getInteger("Type")); + listingId = compound.getString("ListingID"); + message = compound.getString("Message"); + timestamp = compound.getLong("Timestamp"); + sent = compound.getBoolean("Sent"); + } + + public static AuctionNotification fromNBT(NBTTagCompound compound) { + AuctionNotification notification = new AuctionNotification(); + notification.readFromNBT(compound); + return notification; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java b/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java new file mode 100644 index 000000000..a30923eac --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java @@ -0,0 +1,94 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.data.IAnimation; + +/** + * A read-only animation loaded from mod assets. + * Built-in animations are identified by name only, never by ID. + * They cannot be saved, deleted, or modified. + */ +public class BuiltInAnimation extends Animation { + + public BuiltInAnimation() { + super(); + this.id = -1; // No valid ID for built-in animations + } + + public BuiltInAnimation(String name) { + super(); + this.name = name; + this.id = -1; + } + + /** + * Built-in animations cannot be saved. + * @return this animation unchanged + */ + @Override + public IAnimation save() { + // Do nothing - built-in animations are read-only + return this; + } + + /** + * Built-in animations have no valid ID. + * @return always -1 + */ + @Override + public int getID() { + return -1; + } + + /** + * Built-in animations cannot have their ID changed. + */ + @Override + public void setID(int newID) { + // Ignore - built-in animations don't use IDs + } + + /** + * Check if this is a built-in animation. + * @return always true for BuiltInAnimation + */ + public boolean isBuiltIn() { + return true; + } + + /** + * Read animation data from NBT. + * Preserves the name set in constructor (from filename) rather than reading from NBT. + */ + @Override + public void readFromNBT(NBTTagCompound compound) { + // Preserve the name set in constructor - don't read from NBT + // (filename is the source of truth for built-in animations) + String preservedName = this.name; + + speed = compound.getFloat("Speed"); + smooth = compound.getByte("Smooth"); + loop = compound.getInteger("Loop"); + + frames.clear(); + net.minecraft.nbt.NBTTagList list = compound.getTagList("Frames", 10); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound item = list.getCompoundTagAt(i); + Frame frame = new Frame(); + frame.parent = this; + frame.readFromNBT(item); + frames.add(frame); + } + + this.whileStanding = compound.getBoolean("WhileStanding"); + this.whileMoving = compound.getBoolean("WhileWalking"); + this.whileAttacking = compound.getBoolean("WhileAttacking"); + + this.currentFrame = compound.getInteger("CurrentFrame"); + this.currentFrameTime = compound.getInteger("CurrentFrameTime"); + + // Ensure name and ID are preserved + this.name = preservedName; + this.id = -1; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java index 3834bef08..7a5db74de 100644 --- a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java +++ b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java @@ -7,6 +7,7 @@ import noppes.npcs.EventHooks; import noppes.npcs.NoppesUtilServer; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.handler.data.ICustomEffect; +import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.CustomEffectController; import noppes.npcs.scripted.event.player.PlayerEvent; @@ -193,7 +194,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EffectScript.ScriptType.OnEffectAdd, event); + script.callScript(EnumScriptType.ON_EFFECT_ADD, event); } EventHooks.onEffectAdded(iPlayer, playerEffect); @@ -213,7 +214,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EffectScript.ScriptType.OnEffectTick, event); + script.callScript(EnumScriptType.ON_EFFECT_TICK, event); } EventHooks.onEffectTick(iPlayer, playerEffect); @@ -234,7 +235,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EffectScript.ScriptType.OnEffectRemove, event); + script.callScript(EnumScriptType.ON_EFFECT_REMOVE, event); } EventHooks.onEffectRemove(iPlayer, playerEffect, type); diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index bf4327d38..74806605f 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -3,6 +3,7 @@ package noppes.npcs.controllers.data; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.Event; import cpw.mods.fml.relauncher.Side; +import kamkeel.npcs.network.packets.request.script.EventScriptPacket; import net.minecraft.entity.Entity; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -16,9 +17,11 @@ 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; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.janino.EventJaninoScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.ScriptWorld; import noppes.npcs.scripted.constants.EntityType; @@ -35,8 +38,8 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -public class DataScript implements INpcScriptHandler { - public List eventScripts = new ArrayList<>(); +public class DataScript implements IScriptHandlerPacket { + public List eventScripts = new ArrayList<>(); private HashMap scripts = new HashMap<>(); private final static EntityType entities = new EntityType(); @@ -146,13 +149,13 @@ public class DataScript implements INpcScriptHandler { if (!hasInited && !npc.isRemote() && type != EnumScriptType.INIT) { hasInited = true; - for (ScriptContainer scriptContainer : this.eventScripts) { - scriptContainer.errored = false; + for (IScriptUnit scriptUnit : this.eventScripts) { + scriptUnit.setErrored(false); } EventHooks.onNPCInit(this.npc); } - for (ScriptContainer script : this.eventScripts) { + for (IScriptUnit script : this.eventScripts) { script.run(type, event); } @@ -209,6 +212,21 @@ public class DataScript implements INpcScriptHandler { return enabled && ScriptController.HasStart && !npc.worldObj.isRemote && !scripts.isEmpty() && ConfigScript.ScriptingEnabled; } + @Override + public ScriptContext getContext() { + return ScriptContext.NPC; + } + + @Override + public void requestData() { + EventScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + EventScriptPacket.Save(index, totalCount, nbt); + } + public Map getOldConsoleText() { Map map = new TreeMap<>(); @@ -221,24 +239,6 @@ public class DataScript implements INpcScriptHandler { return map; } - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (ScriptContainer script : this.getScripts()) { - ++tab; - - for (Map.Entry longStringEntry : script.console.entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); - } - } - return map; - } - - public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); - } - } @Override public void callScript(EnumScriptType type, Event event) { @@ -274,11 +274,11 @@ public class DataScript implements INpcScriptHandler { this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.eventScripts = list; } - public List getScripts() { + public List getScripts() { return this.eventScripts; } @@ -312,4 +312,9 @@ public class DataScript implements INpcScriptHandler { public Collection getNPCScripts() { return this.scripts.values(); } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new EventJaninoScript(ScriptContext.NPC); + } } diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index bd2b75dae..457eb11d6 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -1,183 +1,56 @@ package noppes.npcs.controllers.data; -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.Event; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.util.ByteBufUtils; +import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.common.util.Constants; -import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptController; -import noppes.npcs.scripted.event.player.PlayerEvent; +import noppes.npcs.constants.ScriptContext; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; +/** + * Script handler for CustomEffect scripts. + * Manages a single script container for effect hooks (onEffectAdd, onEffectTick, onEffectRemove). + */ +public class EffectScript extends SingleScriptHandler implements IScriptHandlerPacket { -public class EffectScript implements INpcScriptHandler { - public ScriptContainer container; - public String scriptLanguage = "ECMAScript"; - public boolean enabled = false; + /** + * The effect ID for packet communication. -1 if not bound to an effect. + */ + private int effectId = -1; - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("ScriptLanguage", scriptLanguage); - compound.setBoolean("ScriptEnabled", enabled); - - if (container != null) - compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); - return compound; + /** + * Create an unbound EffectScript (for server-side use). + */ + public EffectScript() { } - public EffectScript readFromNBT(NBTTagCompound compound) { - scriptLanguage = compound.getString("ScriptLanguage"); - enabled = compound.getBoolean("ScriptEnabled"); - - if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = new ScriptContainer(this); - container.readFromNBT(compound.getCompoundTag("ScriptContent")); - } - return this; - } - - public void callScript(ScriptType type, PlayerEvent.EffectEvent event) { - callScript(type.function, event); - } - - public boolean isEnabled() { - return this.enabled && ScriptController.HasStart && container != null && ConfigScript.ScriptingEnabled; + /** + * Create an EffectScript bound to a specific effect (for GUI use). + * + * @param effectId The ID of the CustomEffect + */ + public EffectScript(int effectId) { + this.effectId = effectId; } @Override - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } - - @Override - public void callScript(String s, Event event) { - if (!this.isEnabled()) { - return; - } - container.run(s, event); - } - - @Override - public boolean isClient() { - return FMLCommonHandler.instance().getEffectiveSide().isClient(); - } - - @Override - public boolean getEnabled() { - return this.enabled; - } - - @Override - public void setEnabled(boolean b) { - this.enabled = b; - } - - @Override - public String getLanguage() { - return this.scriptLanguage; - } - - @Override - public void setLanguage(String s) { - this.scriptLanguage = s; - } - - @Override - public void setScripts(List list) { - if (list == null || list.isEmpty()) { - container = null; - return; - } - container = list.get(0); - } - - @Override - public List getScripts() { - if (container == null) - return new ArrayList<>(); - return Collections.singletonList(container); + public ScriptContext getContext() { + return ScriptContext.EFFECT; } @Override public String noticeString() { - return ""; + return effectId >= 0 ? "CustomEffect[" + effectId + "]" : "CustomEffect"; + } + + + @Override + public void requestData() { + if (effectId >= 0) + EffectScriptPacket.Get(effectId); } @Override - public Map getConsoleText() { - TreeMap map = new TreeMap(); - int tab = 0; - Iterator var3 = this.getScripts().iterator(); - - while (var3.hasNext()) { - ScriptContainer script = (ScriptContainer) var3.next(); - ++tab; - Iterator var5 = script.console.entrySet().iterator(); - - while (var5.hasNext()) { - Map.Entry longStringEntry = (Map.Entry) var5.next(); - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + (String) longStringEntry.getValue()); - } - } - - return map; + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + if (effectId >= 0) + EffectScriptPacket.Save(effectId, index, totalCount, nbt); } - @Override - public void clearConsole() { - Iterator var1 = this.getScripts().iterator(); - - while (var1.hasNext()) { - ScriptContainer script = (ScriptContainer) var1.next(); - script.console.clear(); - } - } - - public void saveScript(ByteBuf buffer) throws IOException { - int tab = buffer.readInt(); - int totalScripts = buffer.readInt(); - if (totalScripts == 0) { - this.container = null; - } - - if (tab == 0) { - NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - ScriptContainer script = new ScriptContainer(this); - script.readFromNBT(tabCompound); - this.container = script; - } else { - NBTTagCompound compound = ByteBufUtils.readNBT(buffer); - this.setLanguage(compound.getString("ScriptLanguage")); - if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); - } else { - this.setLanguage("ECMAScript"); - } - } - this.setEnabled(compound.getBoolean("ScriptEnabled")); - } - } - - public enum ScriptType { - OnEffectAdd("onEffectAdd"), - OnEffectTick("onEffectTick"), - OnEffectRemove("onEffectRemove"); - - public final String function; - - ScriptType(String functionName) { - this.function = functionName; - } - } } diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index dde123aae..1c6f09da7 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -1,142 +1,168 @@ package noppes.npcs.controllers.data; +import com.google.common.reflect.ClassPath; import cpw.mods.fml.common.eventhandler.Event; +import cpw.mods.fml.common.gameevent.TickEvent; +import cpw.mods.fml.common.network.FMLNetworkEvent; +import kamkeel.npcs.network.packets.request.script.ForgeScriptPacket; import net.minecraft.nbt.NBTTagCompound; +import net.minecraftforge.event.entity.EntityEvent; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +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.EventHooks; -import noppes.npcs.NBTTags; import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.ScriptHookController; +import org.apache.commons.lang3.StringUtils; +import java.io.IOException; +import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; -public class ForgeDataScript implements INpcScriptHandler { - private List scripts = new ArrayList(); - private String scriptLanguage = "ECMAScript"; - public long lastInited = -1L; +public class ForgeDataScript extends MultiScriptHandler { + private static final Object HOOK_LOCK = new Object(); + private static List cachedHooks; + private long lastForgeUpdate = -1L; - private boolean enabled = false; public ForgeDataScript() { } - public void clear() { - this.scripts = new ArrayList(); - } - - public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("Scripts")) { - this.scripts = NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this); - } else { - this.scripts = NBTTags.GetScript(compound, this); - } - this.scriptLanguage = compound.getString("ScriptLanguage"); - if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; - } else { - this.scriptLanguage = "ECMAScript"; - } - } - this.enabled = compound.getBoolean("ScriptEnabled"); - } - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("TotalScripts", this.scripts.size()); - for (int i = 0; i < this.scripts.size(); i++) { - compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); - } - compound.setString("ScriptLanguage", this.scriptLanguage); - compound.setBoolean("ScriptEnabled", this.enabled); - return compound; - } - - @Override - public void callScript(EnumScriptType var1, Event var2) { - this.callScript(var1.function, var2); - } - - public void callScript(String type, Event event) { - if (this.isEnabled()) { - if (ScriptController.Instance.lastLoaded > this.lastInited || ScriptController.Instance.lastForgeUpdate > this.lastForgeUpdate) { - this.lastInited = ScriptController.Instance.lastLoaded; - this.lastForgeUpdate = ScriptController.Instance.lastForgeUpdate; - - for (ScriptContainer script : this.scripts) { - script.errored = false; - } - - if (!type.equals("init")) { - EventHooks.onForgeInit(this); - } - } - - for (ScriptContainer script : this.scripts) { - if (script == null || script.errored || !script.hasCode()) - continue; - - script.run(type, event); - } - } - } - public boolean isEnabled() { return this.enabled && ConfigScript.GlobalForgeScripts && ScriptController.HasStart && this.scripts.size() > 0; } + @Override + protected boolean canRunScripts() { + return isEnabled(); + } + + @Override + public ScriptContext getContext() { + return ScriptContext.FORGE; + } + + @Override + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited || ScriptController.Instance.lastForgeUpdate > lastForgeUpdate; + } + + @Override + protected void reInitScripts() { + lastInited = ScriptController.Instance.lastLoaded; + lastForgeUpdate = ScriptController.Instance.lastForgeUpdate; + + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; + } + } + + @Override + public void callScript(String type, Event event) { + if (!canRunScripts()) { + return; + } + + if (needsReInit()) { + reInitScripts(); + if (!type.equals("init")) { + EventHooks.onForgeInit(this); + } + } + + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + script.run(type, event); + } + } + + @Override + public List getHooks() { + if (cachedHooks != null) + return new ArrayList<>(cachedHooks); + + synchronized (HOOK_LOCK) { + if (cachedHooks != null) + return new ArrayList<>(cachedHooks); + + // Start with built-in + addon hooks from ScriptHookController + List hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(ScriptContext.FORGE.hookContext)); + + // Dynamically discover Forge event classes + ArrayList list = new ArrayList<>(); + try { + list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("cpw.mods.fml.common.gameevent")); + list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event")); + list.removeAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event.terraingen")); + } catch (IOException e) { + e.printStackTrace(); + } + + for (ClassPath.ClassInfo classInfo : list) { + Class infoClass = classInfo.load(); + List> classes = new ArrayList<>(Arrays.asList(infoClass.getDeclaredClasses())); + if (classes.isEmpty()) { + classes.add(infoClass); + } + + for (Class eventClass : classes) { + if (isValidForgeEvent(eventClass)) { + String eventName = eventClass.getName(); + int lastDot = eventName.lastIndexOf("."); + eventName = StringUtils.uncapitalize(eventName.substring(lastDot + 1).replace("$", "")); + + if (!hookList.contains(eventName)) { + hookList.add(eventName); + } + } + } + } + + cachedHooks = hookList; + } + + return new ArrayList<>(cachedHooks); + } + + /** + * Check if a class is a valid Forge event that should be exposed as a hook. + */ + private boolean isValidForgeEvent(Class eventClass) { + return Event.class.isAssignableFrom(eventClass) + && Modifier.isPublic(eventClass.getModifiers()) + && !Modifier.isAbstract(eventClass.getModifiers()) + && !EntityEvent.EntityConstructing.class.isAssignableFrom(eventClass) + && !WorldEvent.PotentialSpawns.class.isAssignableFrom(eventClass) + && !TickEvent.RenderTickEvent.class.isAssignableFrom(eventClass) + && !TickEvent.ClientTickEvent.class.isAssignableFrom(eventClass) + && !FMLNetworkEvent.ClientCustomPacketEvent.class.isAssignableFrom(eventClass) + && !ItemTooltipEvent.class.isAssignableFrom(eventClass) + && !ChunkEvent.class.isAssignableFrom(eventClass) + && !ChunkWatchEvent.class.isAssignableFrom(eventClass) + && !ChunkDataEvent.class.isAssignableFrom(eventClass); + } + + @Override + public void requestData() { + ForgeScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + ForgeScriptPacket.Save(index, totalCount, nbt); + } + + @Override public boolean isClient() { return false; } - - public boolean getEnabled() { - return this.enabled; - } - - public void setEnabled(boolean bo) { - this.enabled = bo; - } - - public String getLanguage() { - return this.scriptLanguage; - } - - public void setLanguage(String lang) { - this.scriptLanguage = lang; - } - - public void setScripts(List list) { - this.scripts = list; - } - - public List getScripts() { - return this.scripts; - } - - public String noticeString() { - return "ForgeScript"; - } - - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (ScriptContainer script : this.getScripts()) { - ++tab; - - for (Entry longStringEntry : script.console.entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); - } - } - return map; - } - - public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); - } - } } diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index 335256e0f..805ae7c8d 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -1,39 +1,20 @@ package noppes.npcs.controllers.data; -import com.google.common.base.Preconditions; -import cpw.mods.fml.common.eventhandler.Event; +import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.math.BlockPos; -import noppes.npcs.NBTTags; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.util.ScriptToStringHelper; -import javax.annotation.CheckForNull; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalInt; -import java.util.OptionalLong; -import java.util.TreeMap; - -public class GlobalNPCDataScript implements INpcScriptHandler { - public List scripts = new ArrayList(); - public String scriptLanguage = "ECMAScript"; +public class GlobalNPCDataScript extends MultiScriptHandler { private EntityNPCInterface npc; private ICustomNpc npcAPI; - public long lastInited = -1L; private long lastNpcUpdate = -1L; - public boolean enabled = false; public GlobalNPCDataScript(EntityNPCInterface npc) { if (npc != null) { @@ -41,100 +22,53 @@ public class GlobalNPCDataScript implements INpcScriptHandler { } } - public void clear() { - this.scripts = new ArrayList(); - } - - public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("Scripts")) { - this.scripts = NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this); - } else { - this.scripts = NBTTags.GetScript(compound, this); - } - this.scriptLanguage = compound.getString("ScriptLanguage"); - if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; - } else { - this.scriptLanguage = "ECMAScript"; - } - } - this.enabled = compound.getBoolean("ScriptEnabled"); - } - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("TotalScripts", this.scripts.size()); - for (int i = 0; i < this.scripts.size(); i++) { - compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); - } - compound.setString("ScriptLanguage", this.scriptLanguage); - compound.setBoolean("ScriptEnabled", this.enabled); - return compound; + @Override + protected boolean canRunScripts() { + return isEnabled(); } public boolean isEnabled() { return ConfigScript.GlobalNPCScripts && this.enabled && ScriptController.HasStart && this.scripts.size() > 0; } - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); + @Override + public ScriptContext getContext() { + return ScriptContext.NPC; } @Override - public void callScript(String hookName, Event event) { - if (this.isEnabled()) { - if (ScriptController.Instance.lastLoaded > this.lastInited || ScriptController.Instance.lastGlobalNpcUpdate > this.lastNpcUpdate) { - this.lastInited = ScriptController.Instance.lastLoaded; - this.lastNpcUpdate = ScriptController.Instance.lastGlobalNpcUpdate; - - for (ScriptContainer script : this.scripts) { - script.errored = false; - } - } - - for (ScriptContainer script : this.scripts) { - if (script == null || script.errored || !script.hasCode()) - continue; - - script.run(hookName, event); - } - } + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited || ScriptController.Instance.lastGlobalNpcUpdate > lastNpcUpdate; } + @Override + protected void reInitScripts() { + super.reInitScripts(); + lastNpcUpdate = ScriptController.Instance.lastGlobalNpcUpdate; + } + + @Override + public void requestData() { + GlobalNPCScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + GlobalNPCScriptPacket.Save(index, totalCount, nbt); + } + + @Override public boolean isClient() { - return this.npc.isClientWorld(); - } - - public boolean getEnabled() { - return this.enabled; - } - - public void setEnabled(boolean bo) { - this.enabled = bo; - } - - public String getLanguage() { - return this.scriptLanguage; - } - - public void setLanguage(String lang) { - this.scriptLanguage = lang; - } - - public void setScripts(List list) { - this.scripts = list; - } - - public List getScripts() { - return this.scripts; + return this.npc != null && this.npc.isClientWorld(); } + @Override public String noticeString() { if (this.npc == null) { return "Global script"; } else { BlockPos pos = new BlockPos(this.npc); - return GlobalNPCDataScript.toStringHelper(this.npc).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); + return ScriptToStringHelper.toStringHelper(this.npc).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); } } @@ -144,216 +78,4 @@ public class GlobalNPCDataScript implements INpcScriptHandler { } return this.npcAPI; } - - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (ScriptContainer script : this.getScripts()) { - ++tab; - - for (Map.Entry longStringEntry : script.console.entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); - } - } - return map; - } - - public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); - } - } - - public static final class ToStringHelper { - private final String className; - private final GlobalNPCDataScript.ToStringHelper.ValueHolder holderHead; - private GlobalNPCDataScript.ToStringHelper.ValueHolder holderTail; - private boolean omitNullValues; - private boolean omitEmptyValues; - - private ToStringHelper(String className) { - this.holderHead = new GlobalNPCDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderHead; - this.omitNullValues = false; - this.omitEmptyValues = false; - this.className = (String) Preconditions.checkNotNull(className); - } - - public GlobalNPCDataScript.ToStringHelper omitNullValues() { - this.omitNullValues = true; - return this; - } - - public GlobalNPCDataScript.ToStringHelper add(String name, @CheckForNull Object value) { - return this.addHolder(name, value); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, boolean value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, char value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, double value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, float value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, int value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, long value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(@CheckForNull Object value) { - return this.addHolder(value); - } - - public GlobalNPCDataScript.ToStringHelper addValue(boolean value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(char value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(double value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(float value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(int value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(long value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - private static boolean isEmpty(Object value) { - if (value instanceof CharSequence) { - return ((CharSequence) value).length() == 0; - } else if (value instanceof Collection) { - return ((Collection) value).isEmpty(); - } else if (value instanceof Map) { - return ((Map) value).isEmpty(); - } else if (value instanceof Optional) { - return !((Optional) value).isPresent(); - } else if (value instanceof OptionalInt) { - return !((OptionalInt) value).isPresent(); - } else if (value instanceof OptionalLong) { - return !((OptionalLong) value).isPresent(); - } else if (value instanceof OptionalDouble) { - return !((OptionalDouble) value).isPresent(); - } else if (value instanceof com.google.common.base.Optional) { - return !((com.google.common.base.Optional) value).isPresent(); - } else if (value.getClass().isArray()) { - return Array.getLength(value) == 0; - } else { - return false; - } - } - - public String toString() { - boolean omitNullValuesSnapshot = this.omitNullValues; - boolean omitEmptyValuesSnapshot = this.omitEmptyValues; - String nextSeparator = ""; - StringBuilder builder = (new StringBuilder(32)).append(this.className).append('{'); - for (GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = this.holderHead.next; valueHolder != null; valueHolder = valueHolder.next) { - Object value = valueHolder.value; - if (!(valueHolder instanceof GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder)) { - if (value == null) { - if (omitNullValuesSnapshot) { - continue; - } - } else if (omitEmptyValuesSnapshot && isEmpty(value)) { - continue; - } - } - builder.append(nextSeparator); - nextSeparator = ", "; - if (valueHolder.name != null) { - builder.append(valueHolder.name).append('='); - } - if (value != null && value.getClass().isArray()) { - Object[] objectArray = new Object[]{value}; - String arrayString = Arrays.deepToString(objectArray); - builder.append(arrayString, 1, arrayString.length() - 1); - } else { - builder.append(value); - } - } - return builder.append('}').toString(); - } - - private GlobalNPCDataScript.ToStringHelper.ValueHolder addHolder() { - GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = new GlobalNPCDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private GlobalNPCDataScript.ToStringHelper addHolder(@CheckForNull Object value) { - GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - return this; - } - - private GlobalNPCDataScript.ToStringHelper addHolder(String name, @CheckForNull Object value) { - GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder addUnconditionalHolder() { - GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = new GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private GlobalNPCDataScript.ToStringHelper addUnconditionalHolder(Object value) { - GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - return this; - } - - private GlobalNPCDataScript.ToStringHelper addUnconditionalHolder(String name, Object value) { - GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private static final class UnconditionalValueHolder extends GlobalNPCDataScript.ToStringHelper.ValueHolder { - private UnconditionalValueHolder() { - super(); - } - } - - private static class ValueHolder { - @CheckForNull - String name; - @CheckForNull - Object value; - @CheckForNull - GlobalNPCDataScript.ToStringHelper.ValueHolder next; - - private ValueHolder() { - } - } - } - - public static GlobalNPCDataScript.ToStringHelper toStringHelper(Object self) { - return new GlobalNPCDataScript.ToStringHelper(self.getClass().getSimpleName()); - } } diff --git a/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java index 508836d1c..82119a6d4 100644 --- a/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java @@ -1,8 +1,7 @@ package noppes.npcs.controllers.data; -import cpw.mods.fml.common.eventhandler.Event; -import noppes.npcs.constants.EnumScriptType; - public interface INpcScriptHandler extends IScriptHandler { - void callScript(EnumScriptType var1, Event var2); + // This interface is now empty - callScript methods moved to IScriptHandler + // Kept for backward compatibility + } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java index c0445594e..f39b87112 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java @@ -2,7 +2,7 @@ package noppes.npcs.controllers.data; import noppes.npcs.api.IBlock; -public interface IScriptBlockHandler extends INpcScriptHandler { +public interface IScriptBlockHandler extends IScriptHandler { IBlock getBlock(); } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index be37c86f1..812bef6d8 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -1,15 +1,71 @@ package noppes.npcs.controllers.data; +import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.Event; -import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptHookController; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.TreeMap; public interface IScriptHandler { + default void callScript(EnumScriptType type, Event event) { + if (type != null) + callScript(type.function, event); + + } + void callScript(String hookName, Event event); - boolean isClient(); + /** + * Call a function by name and return its result. + *

+ * Default implementation calls the function on each script unit (in order) and returns + * the first non-null value. + */ + default Object callFunction(String hookName, Object... args) { + if (hookName == null || hookName.isEmpty()) + return null; + + if (!getEnabled()) + return null; + + List scripts = getScripts(); + if (scripts == null || scripts.isEmpty()) + return null; + + for (IScriptUnit script : scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + + Object result = script.callFunction(hookName, args); + if (result != null) + return result; + } + + return null; + } + + /** + * Typed convenience wrapper around {@link #callFunction(String, Object...)}. + */ + default S callFunction(String hookName, Class returnType, Object... args) { + Object result = callFunction(hookName, args); + if (result == null || returnType == null) + return null; + + if (returnType.isInstance(result)) + return returnType.cast(result); + + return null; + } + + default boolean isClient() { + return FMLCommonHandler.instance().getEffectiveSide().isClient(); + } boolean getEnabled(); @@ -19,13 +75,138 @@ public interface IScriptHandler { void setLanguage(String language); - void setScripts(List list); + void setScripts(List list); - List getScripts(); + List getScripts(); - String noticeString(); + default String noticeString() { + return ""; + } - Map getConsoleText(); + default Map getConsoleText() { + Map map = new TreeMap<>(); + int tab = 0; - void clearConsole(); + for (IScriptUnit script : getScripts()) { + ++tab; + for (Map.Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); + } + } + return map; + } + + default void clearConsole() { + for (IScriptUnit script : getScripts()) + script.clearConsole(); + + } + + /** + * Create a new Janino (Java) script unit for this handler. + * Handlers that don't support Janino scripts should return null. + * + * @return A new JaninoScript instance, or null if not supported + */ + default IScriptUnit createJaninoScriptUnit() { + return null; // Default: Janino not supported + } + + /** + * Check if this handler supports Janino (Java) scripts. + * + * @return true if createJaninoScriptUnit() can create valid Janino scripts + */ + default boolean supportsJanino() { + return createJaninoScriptUnit() != null; + } + + /** + * Get the script context for autocomplete and type resolution. + * + * @return The script context (default: GLOBAL) + */ + default ScriptContext getContext() { + return ScriptContext.GLOBAL; + } + + /** + * Get the hook context identifier for this handler. + * Used by ScriptHookController to look up available hooks. + *

+ * By default, derives from getContext().hookContext. + * Override if a different hook context is needed. + * + * @return The context string (e.g., "effect", "npc"), or empty if none + */ + default String getHookContext() { + ScriptContext ctx = getContext(); + return ctx != null ? ctx.hookContext : ""; + } + + /** + * Get the list of available hooks for this handler. + * Uses getHookContext() to look up hooks from ScriptHookController. + * + * @return List of hook names, or empty list if no context + */ + default List getHooks() { + String context = getHookContext(); + if (context == null || context.isEmpty()) + return Collections.emptyList(); + + return ScriptHookController.Instance.getAllHooks(context); + } + + /** + * Check if this handler manages only a single script container. + * Single-container handlers use simplified UI (no numbered tabs). + * + * @return true if this handler uses a single container + */ + default boolean isSingleContainer() { + return false; + } + + /** + * Get the single script unit for single-container handlers. + * Convenience method that returns the first script or null. + * + * @return The single script unit, or null if none exists + */ + default IScriptUnit getSingleScript() { + List scripts = getScripts(); + return (scripts != null && !scripts.isEmpty()) ? scripts.get(0) : null; + } + + /** + * Add a script unit to this handler. + * For single-container handlers, replaces the current unit. + */ + default void addScriptUnit(IScriptUnit unit) { + if (unit != null) + getScripts().add(unit); + } + + /** + * Replace a script unit at the given index. + * For single-container handlers, replaces the current unit. + */ + default void replaceScriptUnit(int index, IScriptUnit unit) { + List scripts = getScripts(); + if (index < 0 || index >= scripts.size()) + scripts.add(unit); + else + scripts.set(index, unit); + } + + /** + * Remove a script unit at the given index. + * For single-container handlers, clears the unit. + */ + default void removeScriptUnit(int index) { + List scripts = getScripts(); + if (index >= 0 && index < scripts.size()) + scripts.remove(index); + } } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java new file mode 100644 index 000000000..88ff5fb94 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java @@ -0,0 +1,86 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NBTTags; + +import java.util.List; + +public interface IScriptHandlerPacket extends IScriptHandler { + + /** + * Client-side: request initial script data from the server (typically a {@code ...Packet.Get(...)}). + */ + void requestData(); + + /** + * Client-side: send a script container packet to the server. + * + * @param index {@code >= 0} for a script tab, {@code -1} for handler metadata (language/enabled/console) + * @param totalCount Total number of scripts for this handler + * @param nbt The script container NBT (tab content or metadata) + */ + void sendSavePacket(int index, int totalCount, NBTTagCompound nbt); + + enum GuiDataKind { + LOAD_COMPLETE, + METADATA, + TAB + } + + class GuiDataResult { + public final GuiDataKind kind; + public final int tabIndex; + + public GuiDataResult(GuiDataKind kind, int tabIndex) { + this.kind = kind; + this.tabIndex = tabIndex; + } + } + + /** + * Apply one incoming gui-data payload to this handler. + *

+ * Notes: + *

    + *
  • {@code Languages} / {@code ScriptConsole} are not consumed here; GuiScriptInterface owns those UI concerns.
  • + *
  • Tab payloads are converted to an {@link IScriptUnit} via {@link IScriptUnit#createFromNBT}.
  • + *
+ * + * @return A small classification result so GuiScriptInterface can update GUI state. + */ + default GuiDataResult setGuiData(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + return new GuiDataResult(GuiDataKind.LOAD_COMPLETE, -1); + } + + if (!compound.hasKey("Tab")) { + setLanguage(compound.getString("ScriptLanguage")); + setEnabled(compound.getBoolean("ScriptEnabled")); + return new GuiDataResult(GuiDataKind.METADATA, -1); + } + + int tab = compound.getInteger("Tab"); + NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); + IScriptUnit unit = IScriptUnit.createFromNBT(scriptCompound, this); + replaceScriptUnit(tab, unit); + return new GuiDataResult(GuiDataKind.TAB, tab); + } + + /** + * Emit save packets for all tabs plus one metadata packet. + * GuiScriptInterface is responsible for updating the active {@link IScriptUnit} with current editor text first. + */ + default void sync() { + List containers = getScripts(); + for (int i = 0; i < containers.size(); i++) { + IScriptUnit container = containers.get(i); + sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); + } + + NBTTagCompound scriptData = new NBTTagCompound(); + scriptData.setString("ScriptLanguage", getLanguage()); + scriptData.setBoolean("ScriptEnabled", getEnabled()); + scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(getConsoleText())); + sendSavePacket(-1, containers.size(), scriptData); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java new file mode 100644 index 000000000..b439d7d0a --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -0,0 +1,200 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.controllers.ScriptContainer; + +import java.util.List; +import java.util.TreeMap; + +/** + * Common interface for script units that can be edited in a script GUI. + * Implemented by both ScriptContainer (ECMAScript) and JaninoScript (Java/Janino). + * This allows GuiScriptInterface to work with both script types uniformly. + */ +public interface IScriptUnit { + + // NBT type identifiers + String NBT_TYPE_KEY = "ScriptUnitType"; + String TYPE_ECMASCRIPT = "ECMAScript"; + String TYPE_JANINO = "Janino"; + + /** + * Create an IScriptUnit from NBT data. + * Uses the "ScriptUnitType" tag to determine which implementation to create. + * + * @param compound The NBT data + * @param handler The script handler (used for ScriptContainer) + * @return A ScriptContainer or JaninoScript based on the NBT type + */ + static IScriptUnit createFromNBT(NBTTagCompound compound, IScriptHandler handler) { + String type = compound.getString(NBT_TYPE_KEY); + IScriptUnit unit; + + if (TYPE_JANINO.equals(type)) { + // Create appropriate JaninoScript based on handler + unit = handler.createJaninoScriptUnit(); + if (unit == null) { + // Fallback to ScriptContainer if handler doesn't support Janino + unit = new ScriptContainer(handler); + } + } else { + // Default to ScriptContainer for ECMAScript or missing type + unit = new ScriptContainer(handler); + } + + unit.readFromNBT(compound); + return unit; + } + + // ==================== SCRIPT CONTENT ==================== + + /** + * Get the main script text content. + */ + String getScript(); + + /** + * Set the main script text content. + */ + void setScript(String script); + + /** + * Get the list of external script file names loaded for this unit. + */ + List getExternalScripts(); + + /** + * Set the list of external script file names to load. + */ + void setExternalScripts(List scripts); + + // ==================== CONSOLE ==================== + + /** + * Get the console output map (timestamp -> message). + */ + TreeMap getConsole(); + + /** + * Clear all console output. + */ + void clearConsole(); + + /** + * Append a message to the console. + */ + void appendConsole(String message); + + // ==================== LANGUAGE ==================== + + /** + * Get the scripting language for this unit. + * Returns "ECMAScript", "Java", etc. + */ + String getLanguage(); + + /** + * Set the scripting language for this unit. + * + * @param language The language name (e.g., "ECMAScript", "Java") + */ + void setLanguage(String language); + + /** + * Check if this script unit uses Janino (Java) compilation. + */ + default boolean isJanino() { + return "Java".equals(getLanguage()); + } + + // ==================== HOOK GENERATION ==================== + + /** + * Generate a stub/template for the given hook name. + * ECMAScript: function hookName(event) { } + * Janino: full Java method signature with return type + * + * @param hookName The hook name to generate a stub for + * @param hookData Optional data (e.g., Method object for Janino) + * @return The generated stub string + */ + String generateHookStub(String hookName, Object hookData); + + // ==================== NBT ==================== + + /** + * Write this script unit to NBT. + */ + NBTTagCompound writeToNBT(NBTTagCompound compound); + + /** + * Read this script unit from NBT. + */ + void readFromNBT(NBTTagCompound compound); + + // ==================== STATE ==================== + + /** + * Ensure this script unit is compiled (for Janino). + * No-op for ScriptContainer. + */ + void ensureCompiled(); + + /** + * Check if this script unit has any code (script or external scripts). + */ + boolean hasCode(); + + /** + * Check if this script unit has encountered an error during execution. + */ + boolean hasErrored(); + + /** + * Set the error state of this script unit. + */ + void setErrored(boolean errored); + + // ==================== EXECUTION ==================== + + /** + * Execute this script unit for the given hook type and event. + * + * @param type The script hook type + * @param event The event object + */ + void run(EnumScriptType type, Event event); + + /** + * Execute this script unit for the given hook name and event. + * + * @param hookName The hook name (e.g., "init", "interact") + * @param event The event object + */ + void run(String hookName, Object event); + + /** + * Call a script function by name and return its result. + *

+ * Default implementation returns null (not supported). + */ + default Object callFunction(String hookName, Object... args) { + return null; + } + + /** + * Typed convenience wrapper around {@link #callFunction(String, Object...)}. + */ + default S callFunction(String hookName, Class returnType, Object... args) { + Object result = callFunction(hookName, args); + if (result == null || returnType == null) + return null; + + if (returnType.isInstance(result)) + return returnType.cast(result); + + return null; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java new file mode 100644 index 000000000..cbe1230e0 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -0,0 +1,136 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.janino.JaninoScript; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/** + * Reusable IScriptHandler implementation for objects that own an optional single JaninoScript unit. + *

+ * This is meant for "function provider" scripts (not EnumScriptType driven). Execution is typically + * initiated by the owning system calling script.call(...) directly. + *

+ * NBT storage is kept under the parent compound key "Script" (same convention as JaninoScriptable). + */ +public class JaninoScriptHandler> extends ScriptHandler { + + protected S script; + + private final Supplier factory; + private final Class scriptClass; + + public JaninoScriptHandler(Supplier factory, Class scriptClass) { + this.factory = factory; + this.scriptClass = scriptClass; + scriptLanguage = "Java"; + } + + // -------------------- Script ownership (JaninoScriptable) -------------------- + + public boolean hasScript() { + return script != null; + } + + public S getScript() { + return script; + } + + public JaninoScript createScript() { + if (script == null) + script = factory.get(); + + return script; + } + + public void deleteScript() { + script = null; + } + + // -------------------- NBT helpers -------------------- + + public void readFromNBT(NBTTagCompound compound) { + // JaninoScriptable's helper generics assume JaninoScript, but most of our scripts are JaninoScript. + // Keep it simple and safe: treat it as raw. + script = (S) JaninoScript.readFromNBT(compound, (JaninoScript) script, (Supplier) factory); + } + + public void writeToNBT(NBTTagCompound compound) { + JaninoScript.writeToNBT(compound, (JaninoScript) script); + } + + // -------------------- IScriptHandler -------------------- + + @Override + public void callScript(String hookName, Event event) { + // No-op: these scripts are not executed through the global hook dispatcher. + } + + @Override + public void callScript(EnumScriptType type, Event event) { + // No-op + } + + @Override + public void setScripts(List list) { + if (list == null || list.isEmpty() || list.get(0) == null) { + deleteScript(); + return; + } + + IScriptUnit unit = list.get(0); + if (scriptClass.isInstance(unit)) { + this.script = scriptClass.cast(unit); + return; + } + + // Fallback: copy code into a real instance of this script type. + S target = scriptClass.cast(createScript()); + target.setScript(unit.getScript()); + target.setExternalScripts(unit.getExternalScripts()); + } + + @Override + public List getScripts() { + if (script == null) + return Collections.emptyList(); + + return Collections.singletonList(script); + } + + @Override + public void addScriptUnit(IScriptUnit unit) { + setScripts(Collections.singletonList(unit)); + } + + @Override + public void replaceScriptUnit(int index, IScriptUnit unit) { + setScripts(Collections.singletonList(unit)); + } + + @Override + public void removeScriptUnit(int index) { + deleteScript(); + } + + public List getHooks() { + if (script != null) + return script.getHookList(); + + return Collections.emptyList(); + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return factory.get(); + } + + @Override + public boolean supportsJanino() { + return true; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItem.java b/src/main/java/noppes/npcs/controllers/data/LinkedItem.java index f1840a625..961c570f9 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItem.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItem.java @@ -249,8 +249,8 @@ public class LinkedItem implements ILinkedItem { @Override public void setAttackSpeed(int time) { - if(time <= 0) { - time = 10; + if (time <= 0) { + time = 10; } this.attackSpeed = time; } diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index cce8df255..a54c20727 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -1,105 +1,38 @@ package noppes.npcs.controllers.data; -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.Event; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.util.ByteBufUtils; +import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.common.util.Constants; -import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptController; +import noppes.npcs.constants.ScriptContext; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; +/** + * Script handler for LinkedItem scripts. + * Manages a single script container for linked item hooks. + */ +public class LinkedItemScript extends SingleScriptHandler implements IScriptHandlerPacket { -public class LinkedItemScript implements INpcScriptHandler { - public ScriptContainer container; - public String scriptLanguage = "ECMAScript"; - public boolean enabled = false; + /** + * The linked item ID for packet communication. -1 if not bound. + */ + private int linkedItemId = -1; - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("ScriptLanguage", scriptLanguage); - compound.setBoolean("ScriptEnabled", enabled); - - if (container != null) - compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); - return compound; + /** + * Create an unbound LinkedItemScript (for server-side use). + */ + public LinkedItemScript() { } - public LinkedItemScript readFromNBT(NBTTagCompound compound) { - scriptLanguage = compound.getString("ScriptLanguage"); - enabled = compound.getBoolean("ScriptEnabled"); - - if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = new ScriptContainer(this); - container.readFromNBT(compound.getCompoundTag("ScriptContent")); - } - return this; - } - - public boolean isEnabled() { - return this.enabled && ScriptController.HasStart && container != null && ConfigScript.ScriptingEnabled; + /** + * Create a LinkedItemScript bound to a specific linked item (for GUI use). + * + * @param linkedItemId The ID of the LinkedItem + */ + public LinkedItemScript(int linkedItemId) { + this.linkedItemId = linkedItemId; } @Override - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } - - @Override - public void callScript(String s, Event event) { - if (!this.isEnabled()) { - return; - } - container.run(s, event); - } - - @Override - public boolean isClient() { - return FMLCommonHandler.instance().getEffectiveSide().isClient(); - } - - @Override - public boolean getEnabled() { - return this.enabled; - } - - @Override - public void setEnabled(boolean b) { - this.enabled = b; - } - - @Override - public String getLanguage() { - return this.scriptLanguage; - } - - @Override - public void setLanguage(String s) { - this.scriptLanguage = s; - } - - @Override - public void setScripts(List list) { - if (list == null || list.isEmpty()) { - container = null; - return; - } - container = list.get(0); - } - - @Override - public List getScripts() { - if (container == null) - return new ArrayList<>(); - return Collections.singletonList(container); + public ScriptContext getContext() { + return ScriptContext.LINKED_ITEM; } @Override @@ -108,58 +41,15 @@ public class LinkedItemScript implements INpcScriptHandler { } @Override - public Map getConsoleText() { - TreeMap map = new TreeMap(); - int tab = 0; - Iterator var3 = this.getScripts().iterator(); - - while (var3.hasNext()) { - ScriptContainer script = (ScriptContainer) var3.next(); - ++tab; - Iterator var5 = script.console.entrySet().iterator(); - - while (var5.hasNext()) { - Map.Entry longStringEntry = (Map.Entry) var5.next(); - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + (String) longStringEntry.getValue()); - } - } - - return map; + public void requestData() { + if (linkedItemId >= 0) + LinkedItemScriptPacket.Get(linkedItemId); } @Override - public void clearConsole() { - Iterator var1 = this.getScripts().iterator(); + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + if (linkedItemId >= 0) + LinkedItemScriptPacket.Save(linkedItemId, index, totalCount, nbt); - while (var1.hasNext()) { - ScriptContainer script = (ScriptContainer) var1.next(); - script.console.clear(); - } - } - - public void saveScript(ByteBuf buffer) throws IOException { - int tab = buffer.readInt(); - int totalScripts = buffer.readInt(); - if (totalScripts == 0) { - this.container = null; - } - - if (tab == 0) { - NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - ScriptContainer script = new ScriptContainer(this); - script.readFromNBT(tabCompound); - this.container = script; - } else { - NBTTagCompound compound = ByteBufUtils.readNBT(buffer); - this.setLanguage(compound.getString("ScriptLanguage")); - if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); - } else { - this.setLanguage("ECMAScript"); - } - } - this.setEnabled(compound.getBoolean("ScriptEnabled")); - } } } diff --git a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java new file mode 100644 index 000000000..d4b4c027b --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java @@ -0,0 +1,109 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NBTTags; +import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.ScriptController; + +import java.util.ArrayList; + +/** + * Base class for script handlers that manage multiple script containers (tabs). + * Provides shared NBT serialization and common script execution patterns. + *

+ * Subclasses should: + * - Override getContext() to return the appropriate ScriptContext + * - Override getHookContext() if not using ScriptContext's hookContext + * - Override requestData() and sendSavePacket() for packet communication + * - Override isEnabled() if custom enable logic is needed + */ +public abstract class MultiScriptHandler extends ScriptHandler implements IScriptHandlerPacket { + + // ==================== NBT SERIALIZATION ==================== + + /** + * Read script data from NBT using the Tab0/Tab1/etc. format. + */ + public void readFromNBT(NBTTagCompound compound) { + if (compound.hasKey("Scripts")) { + this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); + } else { + this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); + } + + this.scriptLanguage = compound.getString("ScriptLanguage"); + normalizeLanguage(); + + this.enabled = compound.getBoolean("ScriptEnabled"); + } + + /** + * Write script data to NBT using the Tab0/Tab1/etc. format. + */ + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setInteger("TotalScripts", this.scripts.size()); + for (int i = 0; i < this.scripts.size(); i++) { + compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); + } + compound.setString("ScriptLanguage", this.scriptLanguage); + compound.setBoolean("ScriptEnabled", this.enabled); + return compound; + } + + /** + * 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 (!ScriptController.Instance.languages.isEmpty()) { + this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; + } else { + this.scriptLanguage = "ECMAScript"; + } + } + } + + // ==================== SCRIPT EXECUTION ==================== + + /** + * Check if scripts need to be re-initialized due to reload. + * Subclasses can override to add additional update tracking (e.g., lastPlayerUpdate). + * + * @return true if scripts should be re-initialized + */ + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited; + } + + /** + * Called when scripts need re-initialization. + * Clears error state on all script containers. + */ + protected void reInitScripts() { + lastInited = ScriptController.Instance.lastLoaded; + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) { + ((ScriptContainer) script).errored = false; + } + } + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) { + return; + } + + if (needsReInit()) { + reInitScripts(); + } + + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) { + continue; + } + script.run(hookName, event); + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java new file mode 100644 index 000000000..45f9dfe44 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -0,0 +1,503 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.UserType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagString; +import net.minecraft.util.DamageSource; +import noppes.npcs.api.ability.IPlayerAbilityData; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; + +import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; + +import java.util.ArrayList; +import java.util.List; + +/** + * Manages player ability data - unlocked abilities, selection, cooldowns, and execution. + * Players reference abilities by key (built-in keys or preset names), never customized copies. + *

+ * Key differences from NPC DataAbilities: + * - Universal cooldown (shared across all abilities) + * - Player manually selects ability (no weighted random) + * - Conditions requiring a target are skipped + * - Uses ability's base cooldownTicks only + */ +public class PlayerAbilityData implements IPlayerAbilityData { + + private final PlayerData playerData; + + // ═══════════════════════════════════════════════════════════════════ + // CONFIGURATION (saved to NBT) + // ═══════════════════════════════════════════════════════════════════ + + /** + * List of unlocked ability keys (preset names or built-in keys). + * Order matters - determines selection index. + */ + private List unlockedAbilities = new ArrayList<>(); + + /** + * Currently selected ability index in the unlocked list. + */ + private int selectedIndex = 0; + + // ═══════════════════════════════════════════════════════════════════ + // RUNTIME STATE (not saved) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Universal cooldown end time (shared across all abilities). + */ + private transient long cooldownEndTime = 0; + + /** + * Currently executing ability instance (null if none). + */ + private transient Ability currentAbility; + + /** + * Current ability's key (for cooldown tracking after completion). + */ + private transient String currentAbilityKey; + + /** + * Target for the current ability (can be null for self-targeting). + */ + private transient EntityLivingBase currentTarget; + + // ═══════════════════════════════════════════════════════════════════ + // CONSTRUCTOR + // ═══════════════════════════════════════════════════════════════════ + + public PlayerAbilityData(PlayerData playerData) { + this.playerData = playerData; + } + + // ═══════════════════════════════════════════════════════════════════ + // TICK - Called from player tick handler + // ═══════════════════════════════════════════════════════════════════ + + /** + * Main tick method - handles ability execution. + * Should be called every server tick. + */ + public void tick(EntityPlayer player) { + if (player.worldObj.isRemote || player.isDead) { + return; + } + + if (currentAbility != null && currentAbility.isExecuting()) { + tickCurrentAbility(player); + } + } + + /** + * Tick the currently executing ability. + */ + private void tickCurrentAbility(EntityPlayer player) { + AbilityPhase oldPhase = currentAbility.getPhase(); + + // Tick advances time and possibly changes phase + boolean phaseChanged = currentAbility.tick(); + + switch (currentAbility.getPhase()) { + case WINDUP: + currentAbility.onWindUpTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick()); + break; + + case ACTIVE: + if (phaseChanged && oldPhase == AbilityPhase.WINDUP) { + // Just entered ACTIVE phase + TelegraphInstance telegraph = currentAbility.getTelegraphInstance(); + if (telegraph != null) { + telegraph.lockPosition(); + } + removeTelegraph(currentAbility, player); + + // Play active sound and animation + playAbilitySound(player, currentAbility.getActiveSound()); + playAbilityAnimation(currentAbility.getActiveAnimation()); + + // Call onExecute + currentAbility.onExecute(player, currentTarget, player.worldObj); + + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(player); + return; + } + } + currentAbility.onActiveTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick()); + + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(player); + return; + } + break; + + case DAZED: + // Player is dazed - just wait + break; + + case IDLE: + handleAbilityCompletion(player); + break; + } + } + + /** + * Handle ability completion. + */ + private void handleAbilityCompletion(EntityPlayer player) { + if (currentAbility == null) return; + + currentAbility.onComplete(player, currentTarget); + + // Apply universal cooldown (using base cooldownTicks only, not min/max random) + if (!currentAbility.isIgnoreCooldown()) { + cooldownEndTime = player.worldObj.getTotalWorldTime() + currentAbility.getCooldownTicks(); + } + + // Stop animation + stopAbilityAnimation(); + + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + } + + // ═══════════════════════════════════════════════════════════════════ + // ABILITY ACTIVATION - Called on Special Key press + // ═══════════════════════════════════════════════════════════════════ + + /** + * Activate the currently selected ability. + * Called when the player presses the Special Key. + * + * @param player The player + * @return true if the ability was started + */ + public boolean activateAbility(EntityPlayer player) { + if (player.worldObj.isRemote) return false; + if (unlockedAbilities.isEmpty()) return false; + if (selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) return false; + + String key = unlockedAbilities.get(selectedIndex); + return activateAbility(player, key); + } + + /** + * Activate a specific ability by key. + * + * @param player The player + * @param key The ability key (built-in or preset name) + * @return true if the ability was started + */ + public boolean activateAbility(EntityPlayer player, String key) { + if (player.worldObj.isRemote || key == null || key.isEmpty()) return false; + + // Can't activate if already executing + if (currentAbility != null && currentAbility.isExecuting()) return false; + + // Resolve ability from controller + if (AbilityController.Instance == null) return false; + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability == null) return false; + + // Check user type allows player + if (!ability.getAllowedBy().allowsPlayer()) return false; + + // Check universal cooldown + if (!ability.isIgnoreCooldown() && isOnCooldown(player)) return false; + + // Check conditions (skip target-requiring ones) + if (!ability.checkConditionsForPlayer(player)) return false; + + // Start the ability + currentAbility = ability; + currentAbilityKey = key; + currentTarget = null; // Players don't have auto-targets + ability.start(null); + + // Spawn telegraph + spawnTelegraph(ability, player, null); + + // Play wind up sound and animation + playAbilitySound(player, ability.getWindUpSound()); + playAbilityAnimation(ability.getWindUpAnimation()); + + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // CLIENT SYNC + // ═══════════════════════════════════════════════════════════════════ + + /** + * Sync ability data to the client. + * Called when abilities change (unlock/lock/select) and on login. + */ + public void syncToClient() { + EntityPlayer player = playerData.player; + if (player instanceof EntityPlayerMP) { + PlayerAbilitySyncPacket.sendToPlayer((EntityPlayerMP) player); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // SELECTION + // ═══════════════════════════════════════════════════════════════════ + + public int getSelectedIndex() { + return selectedIndex; + } + + public void setSelectedIndex(int index) { + if (index >= 0 && index < unlockedAbilities.size()) { + this.selectedIndex = index; + syncToClient(); + } + } + + public void selectNext() { + if (unlockedAbilities.isEmpty()) return; + selectedIndex = (selectedIndex + 1) % unlockedAbilities.size(); + syncToClient(); + } + + public void selectPrevious() { + if (unlockedAbilities.isEmpty()) return; + selectedIndex = (selectedIndex - 1 + unlockedAbilities.size()) % unlockedAbilities.size(); + syncToClient(); + } + + public String getSelectedAbilityKey() { + if (unlockedAbilities.isEmpty() || selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) { + return null; + } + return unlockedAbilities.get(selectedIndex); + } + + // ═══════════════════════════════════════════════════════════════════ + // UNLOCKED ABILITIES + // ═══════════════════════════════════════════════════════════════════ + + public List getUnlockedAbilityList() { + return unlockedAbilities; + } + + @Override + public String[] getUnlockedAbilities() { + return unlockedAbilities.toArray(new String[0]); + } + + public void setUnlockedAbilities(List abilities) { + this.unlockedAbilities = abilities != null ? new ArrayList<>(abilities) : new ArrayList<>(); + // Clamp selected index + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + syncToClient(); + } + + public void unlockAbility(String key) { + if (key != null && !key.isEmpty() && !unlockedAbilities.contains(key)) { + unlockedAbilities.add(key); + syncToClient(); + } + } + + public void lockAbility(String key) { + unlockedAbilities.remove(key); + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + syncToClient(); + } + + public boolean hasUnlockedAbility(String key) { + return unlockedAbilities.contains(key); + } + + // ═══════════════════════════════════════════════════════════════════ + // COOLDOWNS + // ═══════════════════════════════════════════════════════════════════ + + public boolean isOnCooldown(EntityPlayer player) { + return player.worldObj.getTotalWorldTime() < cooldownEndTime; + } + + /** + * Check if a specific ability key is on cooldown. + * Currently uses universal cooldown (same for all abilities). + */ + public boolean isOnCooldown(String key, EntityPlayer player) { + return isOnCooldown(player); + } + + public long getRemainingCooldown(EntityPlayer player) { + long remaining = cooldownEndTime - player.worldObj.getTotalWorldTime(); + return remaining > 0 ? remaining : 0; + } + + /** + * Reset cooldown for a specific ability key. + * Currently resets the universal cooldown. + */ + public void resetCooldown(String key) { + cooldownEndTime = 0; + } + + /** + * Reset all cooldowns. + */ + public void resetAllCooldowns() { + cooldownEndTime = 0; + } + + @Override + public boolean activateAbility() { + return activateAbility(playerData.player); + } + + @Override + public boolean activateAbility(String key) { + return activateAbility(playerData.player, key); + } + + // ═══════════════════════════════════════════════════════════════════ + // STATE QUERIES + // ═══════════════════════════════════════════════════════════════════ + + public boolean isExecutingAbility() { + return currentAbility != null && currentAbility.isExecuting(); + } + + public Ability getCurrentAbility() { + return currentAbility; + } + + public void interruptCurrentAbility() { + if (currentAbility != null && currentAbility.isExecuting()) { + currentAbility.interrupt(); + stopAbilityAnimation(); + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + } + } + + @Override + public boolean isOnCooldown() { + EntityPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(player); + } + + @Override + public boolean isOnCooldown(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(key, player); + } + + @Override + public void resetCooldown() { + cooldownEndTime = 0; + } + + /** + * Check if the player's movement should be locked due to an executing ability. + * Used by the tick handler to restrict player movement during ability phases. + */ + public boolean isMovementLocked() { + return currentAbility != null && currentAbility.isExecuting() + && currentAbility.isMovementLockedForCurrentPhase(); + } + + // ═══════════════════════════════════════════════════════════════════ + // TELEGRAPH + // ═══════════════════════════════════════════════════════════════════ + + private void spawnTelegraph(Ability ability, EntityPlayer player, EntityLivingBase target) { + TelegraphInstance telegraph = ability.createTelegraph(player, target); + if (telegraph != null) { + ability.setTelegraphInstance(telegraph); + if (player instanceof EntityPlayerMP) { + TelegraphSpawnPacket.sendToTracking(telegraph, player); + } + } + } + + private void removeTelegraph(Ability ability, EntityPlayer player) { + TelegraphInstance telegraph = ability.getTelegraphInstance(); + if (telegraph != null) { + if (player instanceof EntityPlayerMP) { + TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player); + } + ability.setTelegraphInstance(null); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // ANIMATION + // ═══════════════════════════════════════════════════════════════════ + + private void playAbilityAnimation(Animation animation) { + if (animation == null || AnimationController.Instance == null) return; + playerData.animationData.setEnabled(true); + playerData.animationData.setAnimation(animation); + playerData.animationData.updateClient(); + } + + private void stopAbilityAnimation() { + playerData.animationData.setAnimation(null); + playerData.animationData.updateClient(); + } + + private void playAbilitySound(EntityPlayer player, String sound) { + if (sound != null && !sound.isEmpty()) { + player.worldObj.playSoundAtEntity(player, sound, 1.0f, 1.0f); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT SERIALIZATION + // ═══════════════════════════════════════════════════════════════════ + + public void writeToNBT(NBTTagCompound compound) { + NBTTagList list = new NBTTagList(); + for (String key : unlockedAbilities) { + list.appendTag(new NBTTagString(key)); + } + compound.setTag("PlayerAbilities", list); + compound.setInteger("PlayerAbilitySelected", selectedIndex); + } + + public void readFromNBT(NBTTagCompound compound) { + unlockedAbilities.clear(); + if (compound.hasKey("PlayerAbilities")) { + NBTTagList list = compound.getTagList("PlayerAbilities", 8); // 8 = string tag + for (int i = 0; i < list.tagCount(); i++) { + String key = list.getStringTagAt(i); + if (key != null && !key.isEmpty()) { + unlockedAbilities.add(key); + } + } + } + selectedIndex = compound.getInteger("PlayerAbilitySelected"); + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java deleted file mode 100644 index 1271594ba..000000000 --- a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java +++ /dev/null @@ -1,230 +0,0 @@ -package noppes.npcs.controllers.data; - -import kamkeel.npcs.util.VaultUtil; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IPlayerCurrencyData; -import noppes.npcs.config.ConfigMarket; - -/** - * Stores player currency balance. - * This data is NOT profile slot bound - all profile slots share the same currency pool. - * When Vault API is available and UseVault config is enabled, operations delegate to Vault. - * The built-in CNPC+ currency data is always preserved in NBT, but unused when Vault is active. - */ -public class PlayerCurrencyData implements IPlayerCurrencyData { - private final PlayerData playerData; - - // Built-in CNPC+ currency (always saved, used as fallback) - private long balance; - private long lifetimeEarned; - private long lifetimeSpent; - - public PlayerCurrencyData(PlayerData playerData) { - this.playerData = playerData; - this.balance = ConfigMarket.StartingBalance; - this.lifetimeEarned = 0; - this.lifetimeSpent = 0; - } - - /** - * Check if Vault should be used for currency operations. - * Requires both config enabled AND Vault actually available. - */ - private boolean shouldUseVault() { - return ConfigMarket.UseVault && VaultUtil.isEnabled(); - } - - /** - * Get player name for Vault operations - */ - private String getPlayerName() { - if (playerData != null && playerData.player != null) { - return playerData.player.getCommandSenderName(); - } - return playerData != null ? playerData.playername : null; - } - - public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("CurrencyBalance")) { - this.balance = compound.getLong("CurrencyBalance"); - } else { - this.balance = ConfigMarket.StartingBalance; - } - this.lifetimeEarned = compound.getLong("CurrencyLifetimeEarned"); - this.lifetimeSpent = compound.getLong("CurrencyLifetimeSpent"); - } - - public void writeToNBT(NBTTagCompound compound) { - // Always save built-in currency data (preserved even when using Vault) - compound.setLong("CurrencyBalance", this.balance); - compound.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); - compound.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); - } - - /** - * Get the current balance - */ - @Override - public long getBalance() { - if (shouldUseVault()) { - String playerName = getPlayerName(); - if (playerName != null) { - return (long) VaultUtil.getBalance(playerName); - } - } - return balance; - } - - /** - * Set the balance directly (use with caution) - */ - @Override - public void setBalance(long balance) { - if (balance < 0) { - balance = 0; - } - if (balance > ConfigMarket.MaxBalance) { - balance = ConfigMarket.MaxBalance; - } - - if (shouldUseVault()) { - String playerName = getPlayerName(); - if (playerName != null) { - VaultUtil.setBalance(playerName, balance); - return; - } - } - this.balance = balance; - } - - /** - * Add currency to balance (deposit) - * @param amount Amount to add - * @return true if successful, false if would exceed max balance - */ - @Override - public boolean deposit(long amount) { - if (amount <= 0) { - return false; - } - - if (shouldUseVault()) { - String playerName = getPlayerName(); - if (playerName != null) { - boolean success = VaultUtil.addMoney(playerName, amount); - if (success) { - this.lifetimeEarned += amount; - } - return success; - } - } - - // Built-in currency - long newBalance = this.balance + amount; - if (newBalance > ConfigMarket.MaxBalance) { - return false; - } - if (newBalance < this.balance) { - // Overflow protection - return false; - } - this.balance = newBalance; - this.lifetimeEarned += amount; - return true; - } - - /** - * Remove currency from balance (withdraw) - * @param amount Amount to remove - * @return true if successful, false if insufficient funds - */ - @Override - public boolean withdraw(long amount) { - if (amount <= 0) { - return false; - } - - if (shouldUseVault()) { - String playerName = getPlayerName(); - if (playerName != null) { - boolean success = VaultUtil.withdrawMoney(playerName, amount); - if (success) { - this.lifetimeSpent += amount; - } - return success; - } - } - - // Built-in currency - if (this.balance < amount) { - return false; - } - this.balance -= amount; - this.lifetimeSpent += amount; - return true; - } - - /** - * Check if player can afford an amount - */ - @Override - public boolean canAfford(long amount) { - if (shouldUseVault()) { - String playerName = getPlayerName(); - if (playerName != null) { - return VaultUtil.has(playerName, amount); - } - } - return this.balance >= amount; - } - - /** - * Get lifetime earned currency - */ - @Override - public long getLifetimeEarned() { - return lifetimeEarned; - } - - /** - * Get lifetime spent currency - */ - @Override - public long getLifetimeSpent() { - return lifetimeSpent; - } - - /** - * Format the balance for display - */ - @Override - public String formatBalance() { - if (shouldUseVault()) { - return VaultUtil.format(getBalance()); - } - return formatAmount(this.balance); - } - - /** - * Format an amount for display - */ - public static String formatAmount(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - /** - * Check if Vault is being used for currency operations. - * Returns true only if config is enabled AND Vault is actually available. - */ - @Override - public boolean isUsingVault() { - return shouldUseVault(); - } - - /** - * Check if Vault is configured (regardless of availability) - */ - public boolean isVaultConfigured() { - return ConfigMarket.UseVault; - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index d2a949074..fa4e7d961 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -15,9 +15,10 @@ import net.minecraft.world.World; import net.minecraftforge.common.IExtendedEntityProperties; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; +import noppes.npcs.api.ability.IPlayerAbilityData; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.handler.IPlayerBankData; -import noppes.npcs.api.handler.IPlayerCurrencyData; +import noppes.npcs.api.handler.IPlayerTradeData; import noppes.npcs.api.handler.IPlayerData; import noppes.npcs.api.handler.IPlayerDialogData; import noppes.npcs.api.handler.IPlayerFactionData; @@ -46,6 +47,8 @@ import java.io.FileOutputStream; import java.util.HashSet; import java.util.UUID; +import static noppes.npcs.util.CustomNPCsThreader.customNPCThread; + public class PlayerData implements IExtendedEntityProperties, IPlayerData { public PlayerDialogData dialogData = new PlayerDialogData(this); public PlayerBankData bankData = new PlayerBankData(this); @@ -60,9 +63,10 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { public DataSkinOverlays skinOverlays = new DataSkinOverlays(this); public MagicData magicData = new MagicData(); - // Currency data - NOT slot-bound, shared across all profile slots - public PlayerCurrencyData currencyData = new PlayerCurrencyData(this); + // Trade data (currency + auction claims) - shared across all profile slots + public PlayerTradeData tradeData = new PlayerTradeData(this); + public PlayerAbilityData abilityData = new PlayerAbilityData(this); public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -106,6 +110,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { if (!controller.playerEffects.containsKey(playerID)) { controller.playerEffects.put(playerID, effectData.getEffects()); } + + // Sync player ability data to client + abilityData.syncToClient(); } public void onLogout() { @@ -134,6 +141,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { animationData.readFromNBT(data); effectData.readFromNBT(data); magicData.readToNBT(data); + tradeData.readFromNBT(data); + abilityData.readFromNBT(data); if (player != null) { playername = player.getCommandSenderName(); @@ -176,6 +185,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { animationData.writeToNBT(compound); effectData.writeToNBT(compound); magicData.writeToNBT(compound); + tradeData.writeToNBT(compound); + abilityData.writeToNBT(compound); compound.setString("PlayerName", playername); compound.setString("UUID", uuid); @@ -228,6 +239,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { transportData.saveNBTData(compound); factionData.saveNBTData(compound); mailData.saveNBTData(compound); + tradeData.writeToNBT(compound); compound.setString("PlayerName", playername); compound.setString("UUID", uuid); DBCAddon.instance.writeToNBT(this, compound); @@ -241,6 +253,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { transportData.loadNBTData(data); factionData.loadNBTData(data); mailData.loadNBTData(data); + tradeData.readFromNBT(data); if (player != null) { playername = player.getCommandSenderName(); uuid = player.getPersistentID().toString(); @@ -401,8 +414,12 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { return mailData; } - public IPlayerCurrencyData getCurrencyData() { - return currencyData; + public IPlayerTradeData getTradeData() { + return tradeData; + } + + public IPlayerAbilityData getAbilityData() { + return abilityData; } public synchronized void save() { @@ -414,9 +431,6 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { } final NBTTagCompound compound = getNBT(); - // Currency data is saved at player level, NOT per profile slot - currencyData.writeToNBT(compound); - final String filename; if (ConfigMain.DatFormat) { filename = uuid + ".dat"; @@ -453,10 +467,6 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { if (data.hasNoTags()) { data = getNBT(); } - - // Load currency BEFORE setNBT - currency is player-level, NOT per profile slot - currencyData.readFromNBT(data); - setNBT(data); } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index a425bdcd4..38d9e8447 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -1,7 +1,7 @@ package noppes.npcs.controllers.data; -import com.google.common.base.Preconditions; import cpw.mods.fml.common.eventhandler.Event; +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; @@ -10,39 +10,29 @@ import noppes.npcs.NBTTags; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.janino.EventJaninoScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.player.PlayerEvent; +import noppes.npcs.util.ScriptToStringHelper; -import javax.annotation.CheckForNull; -import java.lang.reflect.Array; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; -import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalInt; -import java.util.OptionalLong; import java.util.TreeMap; -public class PlayerDataScript implements INpcScriptHandler { - public List scripts = new ArrayList(); - public String scriptLanguage = "ECMAScript"; +public class PlayerDataScript extends MultiScriptHandler { private EntityPlayer player; private IPlayer playerAPI; private long lastPlayerUpdate = 0L; - public long lastInited = -1; - public boolean hadInteract = true; - private boolean enabled = false; - - private static Map console = new TreeMap(); - private static List errored = new ArrayList(); + // Static console/errored for IndividualPlayerScripts mode + private static Map staticConsole = new TreeMap<>(); + private static List errored = new ArrayList<>(); public PlayerDataScript(EntityPlayer player) { if (player != null) { @@ -51,37 +41,21 @@ public class PlayerDataScript implements INpcScriptHandler { } public void clear() { - console = new TreeMap(); - errored = new ArrayList(); - scripts = new ArrayList(); + super.clear(); + staticConsole = new TreeMap<>(); + errored = new ArrayList<>(); } + @Override public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("Scripts")) { - this.scripts = NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this); - } else { - this.scripts = NBTTags.GetScript(compound, this); - } - this.scriptLanguage = compound.getString("ScriptLanguage"); - if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; - } else { - this.scriptLanguage = "ECMAScript"; - } - } - this.enabled = compound.getBoolean("ScriptEnabled"); - console = NBTTags.GetLongStringMap(compound.getTagList("ScriptConsole", 10)); + super.readFromNBT(compound); + staticConsole = NBTTags.GetLongStringMap(compound.getTagList("ScriptConsole", 10)); } + @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("TotalScripts", this.scripts.size()); - for (int i = 0; i < this.scripts.size(); i++) { - compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); - } - compound.setString("ScriptLanguage", this.scriptLanguage); - compound.setBoolean("ScriptEnabled", this.enabled); - compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(console)); + super.writeToNBT(compound); + compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(staticConsole)); return compound; } @@ -89,8 +63,24 @@ public class PlayerDataScript implements INpcScriptHandler { return ConfigScript.GlobalPlayerScripts && ScriptController.Instance.playerScripts.enabled && ScriptController.HasStart && (this.player == null || !this.player.worldObj.isRemote); } - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); + @Override + protected boolean canRunScripts() { + return isEnabled(); + } + + @Override + public ScriptContext getContext() { + return ScriptContext.PLAYER; + } + + @Override + public void requestData() { + PlayerScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + PlayerScriptPacket.Save(index, totalCount, nbt); } @Override @@ -104,10 +94,11 @@ public class PlayerDataScript implements INpcScriptHandler { errored.clear(); if (player != null) { scripts.clear(); - for (ScriptContainer script : ScriptController.Instance.playerScripts.scripts) { - ScriptContainer s = new ScriptContainer(this); - s.readFromNBT(script.writeToNBT(new NBTTagCompound())); - scripts.add(s); + for (IScriptUnit script : ScriptController.Instance.playerScripts.scripts) { + // Clone the script unit by writing to NBT and reading back with factory + NBTTagCompound nbt = script.writeToNBT(new NBTTagCompound()); + IScriptUnit cloned = IScriptUnit.createFromNBT(nbt, this); + scripts.add(cloned); } } lastPlayerUpdate = ScriptController.Instance.lastPlayerUpdate; @@ -117,26 +108,27 @@ public class PlayerDataScript implements INpcScriptHandler { } } for (int i = 0; i < scripts.size(); i++) { - ScriptContainer script = scripts.get(i); + IScriptUnit script = scripts.get(i); if (errored.contains(i)) continue; script.run(hookName, event); - if (script.errored) { + if (script.hasErrored()) { errored.add(i); } - for (Entry entry : script.console.entrySet()) { - if (!console.containsKey(entry.getKey())) - console.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); + for (Entry entry : script.getConsole().entrySet()) { + if (!staticConsole.containsKey(entry.getKey())) + staticConsole.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); } - script.console.clear(); + script.clearConsole(); } } else { if (ScriptController.Instance.lastLoaded > this.lastInited || ScriptController.Instance.lastPlayerUpdate > this.lastPlayerUpdate) { this.lastInited = ScriptController.Instance.lastLoaded; this.lastPlayerUpdate = ScriptController.Instance.lastPlayerUpdate; - for (ScriptContainer scriptContainer : this.scripts) { - scriptContainer.errored = false; + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; } if (!Objects.equals(hookName, EnumScriptType.INIT.function) && event instanceof PlayerEvent) { @@ -145,51 +137,42 @@ public class PlayerDataScript implements INpcScriptHandler { } } - for (ScriptContainer script : this.scripts) { - if (script == null || script.errored || !script.hasCode()) + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) continue; - script.run(hookName, event); } } } + @Override public boolean isClient() { return this.player != null && this.player.isClientWorld(); } + @Override public boolean getEnabled() { return ScriptController.Instance.playerScripts.enabled; } + @Override public void setEnabled(boolean bo) { ScriptController.Instance.playerScripts.enabled = bo; this.enabled = bo; } + @Override public String getLanguage() { return ScriptController.Instance.playerScripts.scriptLanguage; } - public void setLanguage(String lang) { - this.scriptLanguage = lang; - } - - public void setScripts(List list) { - this.scripts = list; - } - - public List getScripts() { - return this.scripts; - } - + @Override public String noticeString() { if (this.player == null) { return "Global script"; - } else { - BlockPos pos = new BlockPos(this.player); - return PlayerDataScript.toStringHelper(this.player).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); } + BlockPos pos = new BlockPos(this.player); + return ScriptToStringHelper.toStringHelper(this.player).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); } public IPlayer getPlayer() { @@ -199,222 +182,25 @@ public class PlayerDataScript implements INpcScriptHandler { return this.playerAPI; } + @Override public Map getConsoleText() { - if (ConfigScript.IndividualPlayerScripts) - return console; - - TreeMap map = new TreeMap<>(); - int tab = 0; - for (ScriptContainer script : this.getScripts()) { - ++tab; - - for (Entry longStringEntry : script.console.entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); - } + if (ConfigScript.IndividualPlayerScripts) { + return staticConsole; } - return map; + return super.getConsoleText(); } + @Override public void clearConsole() { - if (ConfigScript.IndividualPlayerScripts) - console.clear(); - else { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); - } + if (ConfigScript.IndividualPlayerScripts) { + staticConsole.clear(); + return; } + super.clearConsole(); } - public static final class ToStringHelper { - private final String className; - private final PlayerDataScript.ToStringHelper.ValueHolder holderHead; - private PlayerDataScript.ToStringHelper.ValueHolder holderTail; - private boolean omitNullValues; - private boolean omitEmptyValues; - - private ToStringHelper(String className) { - this.holderHead = new PlayerDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderHead; - this.omitNullValues = false; - this.omitEmptyValues = false; - this.className = (String) Preconditions.checkNotNull(className); - } - - public PlayerDataScript.ToStringHelper omitNullValues() { - this.omitNullValues = true; - return this; - } - - public PlayerDataScript.ToStringHelper add(String name, @CheckForNull Object value) { - return this.addHolder(name, value); - } - - public PlayerDataScript.ToStringHelper add(String name, boolean value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, char value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, double value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, float value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, int value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, long value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(@CheckForNull Object value) { - return this.addHolder(value); - } - - public PlayerDataScript.ToStringHelper addValue(boolean value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(char value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(double value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(float value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(int value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(long value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - private static boolean isEmpty(Object value) { - if (value instanceof CharSequence) { - return ((CharSequence) value).length() == 0; - } else if (value instanceof Collection) { - return ((Collection) value).isEmpty(); - } else if (value instanceof Map) { - return ((Map) value).isEmpty(); - } else if (value instanceof Optional) { - return !((Optional) value).isPresent(); - } else if (value instanceof OptionalInt) { - return !((OptionalInt) value).isPresent(); - } else if (value instanceof OptionalLong) { - return !((OptionalLong) value).isPresent(); - } else if (value instanceof OptionalDouble) { - return !((OptionalDouble) value).isPresent(); - } else if (value instanceof com.google.common.base.Optional) { - return !((com.google.common.base.Optional) value).isPresent(); - } else if (value.getClass().isArray()) { - return Array.getLength(value) == 0; - } else { - return false; - } - } - - public String toString() { - boolean omitNullValuesSnapshot = this.omitNullValues; - boolean omitEmptyValuesSnapshot = this.omitEmptyValues; - String nextSeparator = ""; - StringBuilder builder = (new StringBuilder(32)).append(this.className).append('{'); - for (PlayerDataScript.ToStringHelper.ValueHolder valueHolder = this.holderHead.next; valueHolder != null; valueHolder = valueHolder.next) { - Object value = valueHolder.value; - if (!(valueHolder instanceof PlayerDataScript.ToStringHelper.UnconditionalValueHolder)) { - if (value == null) { - if (omitNullValuesSnapshot) { - continue; - } - } else if (omitEmptyValuesSnapshot && isEmpty(value)) { - continue; - } - } - builder.append(nextSeparator); - nextSeparator = ", "; - if (valueHolder.name != null) { - builder.append(valueHolder.name).append('='); - } - if (value != null && value.getClass().isArray()) { - Object[] objectArray = new Object[]{value}; - String arrayString = Arrays.deepToString(objectArray); - builder.append(arrayString, 1, arrayString.length() - 1); - } else { - builder.append(value); - } - } - return builder.append('}').toString(); - } - - private PlayerDataScript.ToStringHelper.ValueHolder addHolder() { - PlayerDataScript.ToStringHelper.ValueHolder valueHolder = new PlayerDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private PlayerDataScript.ToStringHelper addHolder(@CheckForNull Object value) { - PlayerDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - return this; - } - - private PlayerDataScript.ToStringHelper addHolder(String name, @CheckForNull Object value) { - PlayerDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private PlayerDataScript.ToStringHelper.UnconditionalValueHolder addUnconditionalHolder() { - PlayerDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = new PlayerDataScript.ToStringHelper.UnconditionalValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private PlayerDataScript.ToStringHelper addUnconditionalHolder(Object value) { - PlayerDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - return this; - } - - private PlayerDataScript.ToStringHelper addUnconditionalHolder(String name, Object value) { - PlayerDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private static final class UnconditionalValueHolder extends PlayerDataScript.ToStringHelper.ValueHolder { - private UnconditionalValueHolder() { - super(); - } - } - - private static class ValueHolder { - @CheckForNull - String name; - @CheckForNull - Object value; - @CheckForNull - PlayerDataScript.ToStringHelper.ValueHolder next; - - private ValueHolder() { - } - } - } - - public static PlayerDataScript.ToStringHelper toStringHelper(Object self) { - return new PlayerDataScript.ToStringHelper(self.getClass().getSimpleName()); + @Override + public IScriptUnit createJaninoScriptUnit() { + return new EventJaninoScript(ScriptContext.PLAYER); } } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java b/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java new file mode 100644 index 000000000..a0a8e0bb7 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java @@ -0,0 +1,414 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.util.VaultUtil; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.api.handler.IPlayerTradeData; +import noppes.npcs.api.handler.data.IAuctionClaim; +import noppes.npcs.config.ConfigMarket; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Stores player's trade-related data: currency and auction claims. + * This data is shared across all profile slots. + * Uses CopyOnWriteArrayList for thread-safety when adding claims asynchronously. + */ +public class PlayerTradeData implements IPlayerTradeData { + // NBT key used by trade data - excluded from per-slot profile saving + public static final String NBT_KEY = "TradeData"; + + private final PlayerData playerData; + + // ========================================= + // Currency Data + // ========================================= + + // Built-in CNPC+ currency (always saved, used as fallback) + private long balance; + private long lifetimeEarned; + private long lifetimeSpent; + + // ========================================= + // Auction Claims Data + // ========================================= + + // Thread-safe list for claims - can be modified from CNPC+ thread + private final CopyOnWriteArrayList claims = new CopyOnWriteArrayList<>(); + + public PlayerTradeData(PlayerData playerData) { + this.playerData = playerData; + this.balance = ConfigMarket.StartingBalance; + this.lifetimeEarned = 0; + this.lifetimeSpent = 0; + } + + // ========================================= + // Currency Methods + // ========================================= + + /** + * Check if Vault should be used for currency operations. + * Requires both config enabled AND Vault actually available. + */ + private boolean shouldUseVault() { + return ConfigMarket.UseVault && VaultUtil.isEnabled(); + } + + /** + * Get player name for Vault operations + */ + private String getPlayerName() { + if (playerData != null && playerData.player != null) { + return playerData.player.getCommandSenderName(); + } + return playerData != null ? playerData.playername : null; + } + + /** + * Get the current balance + */ + @Override + public long getBalance() { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return (long) VaultUtil.getBalance(playerName); + } + } + return balance; + } + + /** + * Set the balance directly (use with caution) + */ + @Override + public void setBalance(long balance) { + if (balance < 0) { + balance = 0; + } + if (balance > ConfigMarket.MaxBalance) { + balance = ConfigMarket.MaxBalance; + } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + VaultUtil.setBalance(playerName, balance); + return; + } + } + this.balance = balance; + } + + /** + * Add currency to balance (deposit) + * + * @param amount Amount to add + * @return true if successful, false if would exceed max balance + */ + @Override + public boolean deposit(long amount) { + if (amount <= 0) { + return false; + } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.addMoney(playerName, amount); + if (success) { + this.lifetimeEarned += amount; + } + return success; + } + } + + // Built-in currency + long newBalance = this.balance + amount; + if (newBalance > ConfigMarket.MaxBalance) { + return false; + } + if (newBalance < this.balance) { + // Overflow protection + return false; + } + this.balance = newBalance; + this.lifetimeEarned += amount; + return true; + } + + /** + * Remove currency from balance (withdraw) + * + * @param amount Amount to remove + * @return true if successful, false if insufficient funds + */ + @Override + public boolean withdraw(long amount) { + if (amount <= 0) { + return false; + } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.withdrawMoney(playerName, amount); + if (success) { + this.lifetimeSpent += amount; + } + return success; + } + } + + // Built-in currency + if (this.balance < amount) { + return false; + } + this.balance -= amount; + this.lifetimeSpent += amount; + return true; + } + + /** + * Check if player can afford an amount + */ + @Override + public boolean canAfford(long amount) { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return VaultUtil.has(playerName, amount); + } + } + return this.balance >= amount; + } + + /** + * Get lifetime earned currency + */ + @Override + public long getLifetimeEarned() { + return lifetimeEarned; + } + + /** + * Get lifetime spent currency + */ + @Override + public long getLifetimeSpent() { + return lifetimeSpent; + } + + /** + * Format the balance for display + */ + @Override + public String formatBalance() { + if (shouldUseVault()) { + return VaultUtil.format(getBalance()); + } + return formatAmount(this.balance); + } + + /** + * Format an amount for display + */ + public static String formatAmount(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + /** + * Check if Vault is being used for currency operations. + * Returns true only if config is enabled AND Vault is actually available. + */ + @Override + public boolean isUsingVault() { + return shouldUseVault(); + } + + /** + * Check if Vault is configured (regardless of availability) + */ + public boolean isVaultConfigured() { + return ConfigMarket.UseVault; + } + + // ========================================= + // Auction Claims Methods + // ========================================= + + /** + * Add a claim to this player's pending claims. + * Thread-safe - can be called from any thread. + */ + public void addClaim(AuctionClaim claim) { + if (claim != null && !claim.claimed) { + claims.add(claim); + } + } + + /** + * Get all unclaimed claims for this player (internal use). + * Returns a copy of the list for safe iteration. + */ + public List getClaimsList() { + List result = new ArrayList<>(); + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + result.add(claim); + } + } + return result; + } + + /** + * Get all unclaimed claims for this player (API). + */ + @Override + public IAuctionClaim[] getClaims() { + List result = getClaimsList(); + return result.toArray(new IAuctionClaim[0]); + } + + /** + * Get a specific claim by ID (internal use). + */ + public AuctionClaim getClaimInternal(String claimId) { + for (AuctionClaim claim : claims) { + if (claim.id.equals(claimId) && !claim.claimed) { + return claim; + } + } + return null; + } + + /** + * Get a specific claim by ID (API). + */ + @Override + public IAuctionClaim getClaim(String claimId) { + return getClaimInternal(claimId); + } + + /** + * Mark a claim as claimed and remove it. + */ + public boolean claimAndRemove(String claimId) { + for (AuctionClaim claim : claims) { + if (claim.id.equals(claimId) && !claim.claimed) { + claim.claimed = true; + claims.remove(claim); + return true; + } + } + return false; + } + + /** + * Get count of unclaimed claims. + */ + @Override + public int getClaimCount() { + int count = 0; + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + count++; + } + } + return count; + } + + /** + * Check if player has any unclaimed claims. + */ + @Override + public boolean hasClaims() { + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + return true; + } + } + return false; + } + + /** + * Process expired claims - removes claims older than expiration days. + * Returns number of claims removed. + */ + public int processExpiredClaims() { + int expirationDays = ConfigMarket.ClaimExpirationDays; + int removed = 0; + + Iterator iterator = claims.iterator(); + while (iterator.hasNext()) { + AuctionClaim claim = iterator.next(); + if (!claim.claimed && claim.isExpired(expirationDays)) { + claims.remove(claim); + removed++; + } + } + + return removed; + } + + // ========================================= + // NBT Serialization + // ========================================= + + public void readFromNBT(NBTTagCompound compound) { + NBTTagCompound tradeData = compound.getCompoundTag(NBT_KEY); + if (tradeData == null || tradeData.hasNoTags()) { + // No trade data found, use defaults + this.balance = ConfigMarket.StartingBalance; + this.lifetimeEarned = 0; + this.lifetimeSpent = 0; + claims.clear(); + return; + } + + // Currency + if (tradeData.hasKey("CurrencyBalance")) { + this.balance = tradeData.getLong("CurrencyBalance"); + } else { + this.balance = ConfigMarket.StartingBalance; + } + this.lifetimeEarned = tradeData.getLong("CurrencyLifetimeEarned"); + this.lifetimeSpent = tradeData.getLong("CurrencyLifetimeSpent"); + + // Claims + claims.clear(); + if (tradeData.hasKey("AuctionClaims")) { + NBTTagList claimsList = tradeData.getTagList("AuctionClaims", 10); + for (int i = 0; i < claimsList.tagCount(); i++) { + AuctionClaim claim = AuctionClaim.fromNBT(claimsList.getCompoundTagAt(i)); + if (!claim.claimed) { + claims.add(claim); + } + } + } + } + + public void writeToNBT(NBTTagCompound compound) { + NBTTagCompound tradeData = new NBTTagCompound(); + + // Currency - always save built-in currency data (preserved even when using Vault) + tradeData.setLong("CurrencyBalance", this.balance); + tradeData.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); + tradeData.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); + + // Claims + NBTTagList claimsList = new NBTTagList(); + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + claimsList.appendTag(claim.writeToNBT(new NBTTagCompound())); + } + } + tradeData.setTag("AuctionClaims", claimsList); + + compound.setTag(NBT_KEY, tradeData); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index b94acfbc6..97f81df90 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -1,101 +1,45 @@ package noppes.npcs.controllers.data; -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.Event; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.util.ByteBufUtils; +import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.common.util.Constants; -import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptController; +import noppes.npcs.constants.ScriptContext; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; +/** + * Script handler for Recipe scripts (both carpentry and anvil). + * Manages a single script container for recipe hooks (pre, post). + */ +public class RecipeScript extends SingleScriptHandler implements IScriptHandlerPacket { -public class RecipeScript implements INpcScriptHandler { - public ScriptContainer container; - public String scriptLanguage = "ECMAScript"; - public boolean enabled = false; + /** + * The recipe ID for packet communication. -1 if not bound. + */ + private int recipeId = -1; - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("ScriptLanguage", scriptLanguage); - compound.setBoolean("ScriptEnabled", enabled); - if (container != null) - compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); - return compound; + /** + * Whether this is an anvil recipe (vs carpentry). + */ + private boolean anvil = false; + + /** + * Create an unbound RecipeScript (for server-side use). + */ + public RecipeScript() { } - public RecipeScript readFromNBT(NBTTagCompound compound) { - scriptLanguage = compound.getString("ScriptLanguage"); - enabled = compound.getBoolean("ScriptEnabled"); - if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = new ScriptContainer(this); - container.readFromNBT(compound.getCompoundTag("ScriptContent")); - } - return this; - } - - public boolean isEnabled() { - return this.enabled && ScriptController.HasStart && container != null && ConfigScript.ScriptingEnabled; + /** + * Create a RecipeScript bound to a specific recipe (for GUI use). + * + * @param recipeId The ID of the recipe + * @param anvil true for anvil recipe, false for carpentry recipe + */ + public RecipeScript(int recipeId, boolean anvil) { + this.recipeId = recipeId; + this.anvil = anvil; } @Override - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } - - @Override - public void callScript(String s, Event event) { - if (!this.isEnabled()) - return; - container.run(s, event); - } - - @Override - public boolean isClient() { - return FMLCommonHandler.instance().getEffectiveSide().isClient(); - } - - @Override - public boolean getEnabled() { - return this.enabled; - } - - @Override - public void setEnabled(boolean b) { - this.enabled = b; - } - - @Override - public String getLanguage() { - return this.scriptLanguage; - } - - @Override - public void setLanguage(String s) { - this.scriptLanguage = s; - } - - @Override - public void setScripts(List list) { - if (list == null || list.isEmpty()) { - container = null; - return; - } - container = list.get(0); - } - - @Override - public List getScripts() { - if (container == null) - return new ArrayList<>(); - return Collections.singletonList(container); + public ScriptContext getContext() { + return ScriptContext.RECIPE; } @Override @@ -104,47 +48,15 @@ public class RecipeScript implements INpcScriptHandler { } @Override - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (ScriptContainer script : this.getScripts()) { - ++tab; - for (Map.Entry entry : script.console.entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; + public void requestData() { + if (recipeId >= 0) + RecipeScriptPacket.Get(anvil, recipeId); } @Override - public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); - } - } - - public void saveScript(ByteBuf buffer) throws IOException { - int tab = buffer.readInt(); - int totalScripts = buffer.readInt(); - if (totalScripts == 0) - this.container = null; - if (tab == 0) { - NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - ScriptContainer script = new ScriptContainer(this); - script.readFromNBT(tabCompound); - this.container = script; - } else { - NBTTagCompound compound = ByteBufUtils.readNBT(buffer); - this.setLanguage(compound.getString("ScriptLanguage")); - if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); - } else { - this.setLanguage("ECMAScript"); - } - } - this.setEnabled(compound.getBoolean("ScriptEnabled")); - } + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + if (recipeId >= 0) + RecipeScriptPacket.Save(anvil, recipeId, index, totalCount, nbt); } public enum ScriptType { diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java new file mode 100644 index 000000000..684c9a45b --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -0,0 +1,78 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.ScriptController; + +import java.util.ArrayList; +import java.util.List; + +public abstract class ScriptHandler implements IScriptHandler { + public List scripts = new ArrayList<>(); + protected String scriptLanguage = "ECMAScript"; + protected boolean enabled = false; + protected long lastInited = -1; + + protected boolean canRunScripts() { + return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && scripts != null && !scripts.isEmpty(); + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) + return; + + if (ScriptController.Instance.lastLoaded > lastInited) { + lastInited = ScriptController.Instance.lastLoaded; + for (IScriptUnit script : scripts) { + if (script != null) + script.setErrored(false); + } + } + + for (IScriptUnit script : scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + script.run(hookName, event); + } + } + + @Override + public boolean getEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public String getLanguage() { + return scriptLanguage; + } + + @Override + public void setLanguage(String language) { + this.scriptLanguage = language; + } + + public void setScripts(List list) { + this.scripts = list; + } + + public List getScripts() { + return this.scripts; + } + + public void clear() { + this.scripts = new ArrayList<>(); + } + + /** + * Reset the lastInited timestamp to force re-initialization on next script call. + */ + public void resetLastInited() { + this.lastInited = -1; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java new file mode 100644 index 000000000..00c273bca --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java @@ -0,0 +1,153 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +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.controllers.ScriptController; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Base class for script handlers that manage a single script container. + * Provides shared NBT/ByteBuf serialization and packet handling. + *

+ * Subclasses should: + * - Store their parent object (e.g., CustomEffect, LinkedItem) + * - Override getHookContext() to return the appropriate context string + * - Override getContext() if needed (default is GLOBAL) + * - Override requestData() and sendSavePacket() for packet communication + */ +public abstract class SingleScriptHandler extends ScriptHandler { + public IScriptUnit container; + + @Override + public boolean isSingleContainer() { + return true; + } + + protected boolean canRunScripts() { + return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && container != null; + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) { + return; + } + container.run(hookName, event); + } + + @Override + public void setScripts(List list) { + if (list == null || list.isEmpty()) { + container = null; + this.scripts = new ArrayList<>(); + } else { + container = list.get(0); + this.scripts = new ArrayList<>(1); + this.scripts.add(container); + } + } + + @Override + public List getScripts() { + if (container == null) + return new ArrayList<>(); + + return Collections.singletonList(container); + } + + public void addScriptUnit(IScriptUnit unit) { + this.container = unit; + this.scripts.clear(); + if (unit != null) + this.scripts.add(unit); + } + + public void replaceScriptUnit(int index, IScriptUnit unit) { + this.container = unit; + this.scripts.clear(); + if (unit != null) + this.scripts.add(unit); + } + + public void removeScriptUnit(int index) { + this.container = null; + this.scripts.clear(); + } + + /** + * Write script data to NBT. + * Uses standard keys: ScriptLanguage, ScriptEnabled, ScriptContent + */ + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("ScriptLanguage", scriptLanguage); + compound.setBoolean("ScriptEnabled", enabled); + + if (container != null) { + compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); + } + return compound; + } + + /** + * Read script data from NBT. + * Uses standard keys: ScriptLanguage, ScriptEnabled, ScriptContent + */ + public void readFromNBT(NBTTagCompound compound) { + scriptLanguage = compound.getString("ScriptLanguage"); + enabled = compound.getBoolean("ScriptEnabled"); + + if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { + container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); + } + } + + /** + * Save script data from a ByteBuf packet. + * Handles the tab/totalScripts protocol used by script packets. + *

+ * Protocol: + * - tab == 0: Script content NBT follows + * - tab != 0 (typically -1): Script metadata (language, enabled) follows + */ + public void saveScript(ByteBuf buffer) throws IOException { + int tab = buffer.readInt(); + int totalScripts = buffer.readInt(); + + if (totalScripts == 0) { + this.container = null; + } + + if (tab == 0) { + // Script content + NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); + this.container = IScriptUnit.createFromNBT(tabCompound, this); + } else { + // Metadata (language, enabled) + NBTTagCompound compound = ByteBufUtils.readNBT(buffer); + this.setLanguage(compound.getString("ScriptLanguage")); + normalizeLanguage(); + this.setEnabled(compound.getBoolean("ScriptEnabled")); + } + } + + /** + * 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 (!ScriptController.Instance.languages.isEmpty()) { + this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); + } else { + this.setLanguage("ECMAScript"); + } + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/src/main/java/noppes/npcs/controllers/data/TraderStock.java index 68bc96c53..85e76ec02 100644 --- a/src/main/java/noppes/npcs/controllers/data/TraderStock.java +++ b/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -41,7 +41,8 @@ public class TraderStock { /** * Get available stock for a slot - * @param slot The slot index + * + * @param slot The slot index * @param playerName The player name (for per-player mode) * @return Available stock count, or Integer.MAX_VALUE if unlimited */ @@ -73,6 +74,7 @@ public class TraderStock { /** * Consume stock when purchase is made + * * @return true if successful, false if insufficient stock */ public synchronized boolean consumeStock(int slot, String playerName, int amount) { @@ -98,6 +100,7 @@ public class TraderStock { /** * Check if stock should reset based on current time + * * @param currentTime Current MC world time (ticks) or real time (ms) depending on reset type */ public boolean shouldReset(long currentTime) { @@ -113,6 +116,7 @@ public class TraderStock { /** * Reset all stock to max values + * * @param currentTime Current time to record as reset time */ public synchronized void resetStock(long currentTime) { @@ -129,6 +133,7 @@ public class TraderStock { /** * Get reset interval based on reset type + * * @return Interval in ticks (MC time) or milliseconds (real time) */ public long getResetInterval() { @@ -152,6 +157,7 @@ public class TraderStock { /** * Get time until next reset + * * @param currentTime Current time * @return Time remaining until reset, or -1 if no reset scheduled */ @@ -166,6 +172,7 @@ public class TraderStock { /** * Format time until reset for display + * * @param currentTime Current time * @return Formatted string like "2 hours" or "1 day, 5 hours" */ @@ -183,7 +190,7 @@ public class TraderStock { if (days > 0) { return days + " day" + (days != 1 ? "s" : "") + - (hours > 0 ? ", " + hours + " hour" + (hours != 1 ? "s" : "") : ""); + (hours > 0 ? ", " + hours + " hour" + (hours != 1 ? "s" : "") : ""); } else if (hours > 0) { return hours + " hour" + (hours != 1 ? "s" : ""); } else { @@ -198,7 +205,7 @@ public class TraderStock { if (mcDays > 0) { return mcDays + " MC day" + (mcDays != 1 ? "s" : "") + - (mcHours > 0 ? ", " + mcHours + " MC hour" + (mcHours != 1 ? "s" : "") : ""); + (mcHours > 0 ? ", " + mcHours + " MC hour" + (mcHours != 1 ? "s" : "") : ""); } else { return mcHours + " MC hour" + (mcHours != 1 ? "s" : ""); } @@ -220,6 +227,7 @@ public class TraderStock { /** * Validate and clamp all current stock values to not exceed max stock. * Should be called when trader GUI opens to ensure consistency. + * * @return true if any stock was clamped */ public boolean validateStock() { diff --git a/src/main/java/noppes/npcs/controllers/data/action/Action.java b/src/main/java/noppes/npcs/controllers/data/action/Action.java index 729603019..b7bce1102 100644 --- a/src/main/java/noppes/npcs/controllers/data/action/Action.java +++ b/src/main/java/noppes/npcs/controllers/data/action/Action.java @@ -274,10 +274,9 @@ public class Action implements IAction { // return this; // } - /////////////////////////////////////////////////// - ////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// /////////////////////////////////////////////// // Handling - @Override public IAction start() { manager.start(); @@ -405,10 +404,10 @@ public class Action implements IAction { public String toString() { return String.format("%s [queue='%s', scheduled=%s, done=%s, paused=%s, updateEvery=%s, duration=%d/%d, count=%d/%d, threaded=%s]", getIdentifier(), getQueueName(), isScheduled, done, isPaused(), updateEveryXTick, duration, maxDuration, count, maxCount, isThreaded); } - /////////////////////////////////////////////////// - ////////////////////////////////////////////////// - // Thread + /// //////////////////////////////////////////////// + /// /////////////////////////////////////////////// + // Thread @Override public IAction threadify() { if (!isThreaded) { @@ -469,10 +468,9 @@ public class Action implements IAction { return startAfterTicks > 0; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Sequential - @Override public IAction getNext() { if (queue == null) @@ -606,10 +604,9 @@ public class Action implements IAction { } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Parallels - @Override public IAction parallel(IAction act) { if (unscheduledList != null) @@ -658,10 +655,9 @@ public class Action implements IAction { return parallel(manager.create(name, maxDuration, delay, t)); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Conditionals - @Override public IConditionalAction conditional(IConditionalAction after) { if (unscheduledList != null) diff --git a/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java b/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java index c36aa04a9..9cdaf4e57 100644 --- a/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java +++ b/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java @@ -117,10 +117,9 @@ public class ActionManager implements IActionManager { // return false; // } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Creators - @Override public IAction create(String name) { return new Action(this, name); @@ -186,10 +185,9 @@ public class ActionManager implements IActionManager { return new ConditionalAction(this, name, condition, task, terminateWhen, onTermination); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Queues - @Override public IActionQueue createQueue(String name) { return createQueue(name, false); @@ -237,10 +235,9 @@ public class ActionManager implements IActionManager { return true; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Sequential - @Override public IActionQueue getSequentialQueue() { return sequentialQueue; @@ -299,10 +296,9 @@ public class ActionManager implements IActionManager { return sequentialQueue.scheduleActionAt(index, action); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Parallels - @Override public IActionQueue getParallelQueue() { return parallelQueue; @@ -345,7 +341,7 @@ public class ActionManager implements IActionManager { @Override public IAction scheduleParallel(String name, int delay, Consumer task) { - return parallelQueue.schedule(name,delay, task); + return parallelQueue.schedule(name, delay, task); } @Override @@ -353,10 +349,9 @@ public class ActionManager implements IActionManager { return parallelQueue.schedule(name, maxDuration, delay, task); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Conditionals - @Override public IActionQueue getConditionalQueue() { return conditionalQueue; @@ -403,10 +398,9 @@ public class ActionManager implements IActionManager { return schedule(new ConditionalAction(this, name, condition, task, terminateWhen, onTermination)); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Handling - @Override public IActionQueue[] getAllQueues() { ArrayList allQueues = new ArrayList<>(); diff --git a/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java b/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java index 0b3d0b483..332adab39 100644 --- a/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java +++ b/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java @@ -125,10 +125,9 @@ public class ActionQueue implements IActionQueue { return this; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Schedulers - @Override public IAction schedule(IAction action) { Action act = (Action) action; @@ -203,10 +202,9 @@ public class ActionQueue implements IActionQueue { return action; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Queue Data - @Override public boolean hasActiveTasks() { return !queue.isEmpty(); @@ -276,10 +274,9 @@ public class ActionQueue implements IActionQueue { return false; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Handling - public boolean finish(Action a) { if (a.isDone()) { if (a.onDone != null) diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index c30f15f57..16389f74d 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -67,14 +67,13 @@ import net.minecraft.util.ResourceLocation; import net.minecraft.util.Vec3; import net.minecraft.world.World; import net.minecraft.world.WorldServer; -import net.minecraftforge.common.ForgeHooks; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.event.ServerChatEvent; import noppes.npcs.CustomItems; import noppes.npcs.CustomNpcs; -import noppes.npcs.DataAbilities; import noppes.npcs.DataAI; +import noppes.npcs.DataAbilities; import noppes.npcs.DataAdvanced; import noppes.npcs.DataDisplay; import noppes.npcs.DataInventory; @@ -96,12 +95,12 @@ import noppes.npcs.ai.EntityAIDodgeShoot; import noppes.npcs.ai.EntityAIFindShade; import noppes.npcs.ai.EntityAIFollow; import noppes.npcs.ai.EntityAIJob; +import noppes.npcs.ai.EntityAILeapAtTargetNpc; import noppes.npcs.ai.EntityAILook; import noppes.npcs.ai.EntityAIMoveIndoors; import noppes.npcs.ai.EntityAIMovingPath; import noppes.npcs.ai.EntityAIOrbitTarget; import noppes.npcs.ai.EntityAIPanic; -import noppes.npcs.ai.EntityAILeapAtTargetNpc; import noppes.npcs.ai.EntityAIPounceTarget; import noppes.npcs.ai.EntityAIRangedAttack; import noppes.npcs.ai.EntityAIReturn; @@ -155,8 +154,8 @@ import noppes.npcs.roles.JobFollower; import noppes.npcs.roles.JobInterface; import noppes.npcs.roles.RoleCompanion; import noppes.npcs.roles.RoleFollower; -import noppes.npcs.roles.RoleMount; import noppes.npcs.roles.RoleInterface; +import noppes.npcs.roles.RoleMount; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.entity.ScriptNpc; import noppes.npcs.scripted.event.NpcEvent; @@ -482,6 +481,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti super.onLivingUpdate(); + // Apply locked rotation after super.onLivingUpdate() to override look helper changes + abilities.applyLockedRotation(); + handleMountRiderState(); if (worldObj.isRemote) { @@ -1932,6 +1934,13 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti if (NPCMountUtil.isFlyingMountWithFlightEnabled(this)) { return; } + // Skip fall damage during abilities that control NPC movement (e.g., Slam leap) + if (this.abilities != null && this.abilities.isExecutingAbility() + && this.abilities.getCurrentAbility() != null + && this.abilities.getCurrentAbility().hasAbilityMovement()) { + this.fallDistance = 0; + return; + } if (!this.stats.noFallDamage) super.fall(distance); } @@ -2150,7 +2159,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti protected boolean handleMountedMovement(float strafe, float forward) { return NPCMountUtil.handleMountedMovement(this, mountState, strafe, forward); } - + public void performMountedMovement(float strafe, float forward, float moveSpeed) { this.moveStrafing = strafe; this.moveForward = forward; diff --git a/src/main/java/noppes/npcs/items/ItemElementalStaff.java b/src/main/java/noppes/npcs/items/ItemElementalStaff.java index 45f45c17f..d786222b1 100644 --- a/src/main/java/noppes/npcs/items/ItemElementalStaff.java +++ b/src/main/java/noppes/npcs/items/ItemElementalStaff.java @@ -9,7 +9,7 @@ import noppes.npcs.CustomItems; import noppes.npcs.CustomNpcs; import noppes.npcs.constants.EnumNpcToolMaterial; -import java.awt.*; +import java.awt.Color; import java.util.List; public class ItemElementalStaff extends ItemStaff { diff --git a/src/main/java/noppes/npcs/items/ItemNpcScripter.java b/src/main/java/noppes/npcs/items/ItemNpcScripter.java index 5c5734140..4d503d97a 100644 --- a/src/main/java/noppes/npcs/items/ItemNpcScripter.java +++ b/src/main/java/noppes/npcs/items/ItemNpcScripter.java @@ -41,6 +41,7 @@ public class ItemNpcScripter extends Item { } return itemStack; } + CustomNpcs.proxy.openGui(0, 0, 0, EnumGuiType.ScriptEvent, player); return itemStack; } diff --git a/src/main/java/noppes/npcs/items/ItemOrb.java b/src/main/java/noppes/npcs/items/ItemOrb.java index 04df0e16c..b43ccaef4 100644 --- a/src/main/java/noppes/npcs/items/ItemOrb.java +++ b/src/main/java/noppes/npcs/items/ItemOrb.java @@ -5,7 +5,7 @@ import net.minecraft.entity.passive.EntitySheep; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; -import java.awt.*; +import java.awt.Color; import java.util.List; public class ItemOrb extends ItemNpcInterface { diff --git a/src/main/java/noppes/npcs/janino/EventJaninoScript.java b/src/main/java/noppes/npcs/janino/EventJaninoScript.java new file mode 100644 index 000000000..2ac7dd218 --- /dev/null +++ b/src/main/java/noppes/npcs/janino/EventJaninoScript.java @@ -0,0 +1,11 @@ +package noppes.npcs.janino; + +import noppes.npcs.constants.ScriptContext; + +public class EventJaninoScript extends JaninoScript { + + public EventJaninoScript(ScriptContext context) { + super(Object.class, null, false); + this.context = context; + } +} diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java new file mode 100644 index 000000000..fc0c63f6d --- /dev/null +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -0,0 +1,208 @@ +package noppes.npcs.janino; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; + +/** + * Resolves script hook methods for compiled script classes. + * Optimized for high-frequency hooks like TICK with fast O(1) lookups. + */ +public final class JaninoHookResolver { + + /** + * Primary cache: hookName -> CachedHandle + * Fast O(1) lookup without string building for non-overloaded hooks. + */ + private final Map handleCache = new HashMap<>(); + + /** + * Track which compiled class we've cached methods for. + * If the class changes (recompilation), we clear the cache. + */ + private Class lastCompiledClass; + + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + /** + * Cached handle with expected parameter count for quick validation. + */ + private static final class CachedHandle { + final MethodHandle handle; + final int paramCount; + + CachedHandle(MethodHandle handle, int paramCount) { + this.handle = handle; + this.paramCount = paramCount; + } + } + + public JaninoHookResolver() { + } + + /** + * Clears internal resolution caches. + */ + public void clearResolutionCaches() { + handleCache.clear(); + lastCompiledClass = null; + } + + /** + * Resolve a MethodHandle for the given hook. + * Optimized for repeated calls with same hook name - O(1) after first resolution. + */ + public MethodHandle resolveHookHandle(String hookName, Object[] args, Object compiledInstance) { + if (hookName == null || hookName.isEmpty() || compiledInstance == null) + return null; + + Class compiledClass = compiledInstance.getClass(); + + // Clear cache if compiled class changed (recompilation) + if (lastCompiledClass != compiledClass) { + handleCache.clear(); + lastCompiledClass = compiledClass; + } + + int argCount = args == null ? 0 : args.length; + + // Fast path: check primary cache by hookName only + CachedHandle cached = handleCache.get(hookName); + if (cached != null) { + // Verify arg count matches (handles most cases without full signature check) + if (cached.paramCount == argCount) { + return cached.handle; + } + // Arg count mismatch - need full resolution for potential overload + } + + // Slow path: resolve method and cache + Method method = resolveMethod(compiledClass, hookName, args); + if (method == null) + return null; + + try { + MethodHandle handle = LOOKUP.unreflect(method); + handleCache.put(hookName, new CachedHandle(handle, method.getParameterCount())); + return handle; + } catch (IllegalAccessException e) { + return null; + } + } + + /** + * Find the best matching method on the compiled class. + */ + private Method resolveMethod(Class targetClass, String hookName, Object[] args) { + Object[] safeArgs = args == null ? new Object[0] : args; + + Method best = null; + int bestScore = Integer.MAX_VALUE; + + for (Method method : targetClass.getMethods()) { + if (!method.getName().equals(hookName)) + continue; + + // Skip final methods and Object methods + if (Modifier.isFinal(method.getModifiers())) + continue; + if (method.getDeclaringClass() == Object.class) + continue; + + Class[] paramTypes = method.getParameterTypes(); + + // Try exact arg count match first + if (paramTypes.length == safeArgs.length) { + int score = scoreMethodMatch(paramTypes, safeArgs); + if (score != Integer.MAX_VALUE && score < bestScore) { + bestScore = score; + best = method; + if (score == 0) + return best; // Perfect match + } + } + } + + // If no match with args, try no-arg version + if (best == null && safeArgs.length > 0) { + for (Method method : targetClass.getMethods()) { + if (!method.getName().equals(hookName)) + continue; + if (Modifier.isFinal(method.getModifiers())) + continue; + if (method.getDeclaringClass() == Object.class) + continue; + if (method.getParameterCount() == 0) { + return method; + } + } + } + + return best; + } + + /** + * Score how well parameter types match runtime arguments. + * Lower is better; MAX_VALUE means incompatible. + */ + private static int scoreMethodMatch(Class[] paramTypes, Object[] args) { + int score = 0; + for (int i = 0; i < paramTypes.length; i++) { + Class paramType = paramTypes[i]; + Object arg = args[i]; + + if (arg == null) { + if (paramType.isPrimitive()) + return Integer.MAX_VALUE; + score += 1; + continue; + } + + Class argType = arg.getClass(); + Class boxedParam = boxType(paramType); + + if (boxedParam.equals(argType)) + continue; + + if (boxedParam.isAssignableFrom(argType)) { + score += inheritanceDistance(argType, boxedParam); + continue; + } + + return Integer.MAX_VALUE; + } + return score; + } + + private static Class boxType(Class type) { + if (!type.isPrimitive()) + return type; + if (type == boolean.class) return Boolean.class; + if (type == byte.class) return Byte.class; + if (type == short.class) return Short.class; + if (type == int.class) return Integer.class; + if (type == long.class) return Long.class; + if (type == float.class) return Float.class; + if (type == double.class) return Double.class; + if (type == char.class) return Character.class; + return type; + } + + private static int inheritanceDistance(Class child, Class parent) { + if (child.equals(parent)) + return 0; + if (!parent.isAssignableFrom(child)) + return 1000; + + int distance = 0; + Class current = child; + while (current != null && !current.equals(parent)) { + current = current.getSuperclass(); + distance++; + } + return distance; + } +} diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java new file mode 100644 index 000000000..1800cc895 --- /dev/null +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -0,0 +1,503 @@ +package noppes.npcs.janino; + +import cpw.mods.fml.common.eventhandler.Event; +import io.github.somehussar.janinoloader.api.script.IScriptBodyBuilder; +import io.github.somehussar.janinoloader.api.script.IScriptClassBody; +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; +import noppes.npcs.controllers.ScriptHookController; +import noppes.npcs.controllers.data.IScriptUnit; +import noppes.npcs.janino.annotations.ParamName; +import org.codehaus.commons.compiler.InternalCompilerException; +import org.codehaus.commons.compiler.Sandbox; + +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.security.Permissions; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public abstract class JaninoScript implements IScriptUnit { + + public ScriptContext context = ScriptContext.GLOBAL; + public boolean errored = false; + public String script = ""; + public List externalScripts = new ArrayList<>(); + public TreeMap console = new TreeMap<>(); + public boolean evaluated; + + public final Class type; + public final IScriptBodyBuilder builder; + protected final Sandbox sandbox; + protected IScriptClassBody scriptBody; + + private final JaninoHookResolver hookResolver = new JaninoHookResolver(); + private final String[] defaultImports; + + private Map hookDefCache; + private int lastHookRevision = -1; + private int lastSeenGlobalRevision; + + + protected JaninoScript(Class type, String[] defaultImports, boolean isClient) { + this.type = type; + this.defaultImports = defaultImports != null ? defaultImports : new String[0]; + this.builder = IScriptBodyBuilder.getBuilder(type, + isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) + .setDefaultImports(this.defaultImports); + + Permissions permissions = new Permissions(); + permissions.setReadOnly(); + this.sandbox = new Sandbox(permissions); + this.scriptBody = builder.build(); + } + + protected JaninoScript(Class type, String[] defaultImports) { + this(type, defaultImports, false); + } + + protected String getHookContext() { + return this.context.hookContext; + } + + protected T getUnsafe() { + return scriptBody.get(); + } + + public R call(Function fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return null; + + // CodeSource cs = t.getClass().getProtectionDomain().getCodeSource(); + // ProtectionDomain pd = new ProtectionDomain(cs, new Permissions()); + + try { + return sandbox.confine((PrivilegedAction) () -> fn.apply(t)); + // return AccessController.doPrivileged((PrivilegedAction) () -> fn.apply(t), new AccessControlContext( + // new ProtectionDomain[] { + // pd + // } + // )); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + return null; + } + } + + public void run(Consumer fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return; + + try { + sandbox.confine((PrivilegedAction) () -> { + fn.accept(t); + return null; + }); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + } + } + + public void unload() { + } + + + // ==================== COMPILATION ==================== + + public void compileScript(String code) { + try { + String[] imports = collectImportsForCode(code); + builder.setDefaultImports(imports); + this.scriptBody = builder.build(); + scriptBody.setScript(code); + } catch (InternalCompilerException e) { + appendConsole("Compilation error: " + e.getMessage()); + Throwable cause = e; + while (cause.getCause() != null) cause = cause.getCause(); + if (e != cause) appendConsole(cause.getMessage()); + hookResolver.clearResolutionCaches(); + } catch (Exception e) { + appendConsole("Error: " + e.getMessage()); + hookResolver.clearResolutionCaches(); + } + } + + public void compileScript() { + compileScript(getFullCode()); + } + + public void ensureCompiled() { + int global = ScriptController.Instance.globalRevision; + if (!evaluated || global != lastSeenGlobalRevision) { + compileScript(); + lastSeenGlobalRevision = global; + evaluated = true; + } + } + + private String getFullCode() { + StringBuilder sb = new StringBuilder(); + if (ConfigScript.RunLoadedScriptsFirst) appendExternalScripts(sb); + if (script != null && !script.isEmpty()) sb.append(script).append("\n"); + if (!ConfigScript.RunLoadedScriptsFirst) appendExternalScripts(sb); + return sb.toString(); + } + + private void appendExternalScripts(StringBuilder sb) { + for (String name : externalScripts) { + String code = ScriptController.Instance.scripts.get(name); + if (code != null && !code.isEmpty()) sb.append(code).append("\n"); + } + } + + // ==================== SMART IMPORTS ==================== + + private String[] collectImportsForCode(String code) { + Set imports = new LinkedHashSet<>(); + Collections.addAll(imports, defaultImports); + + if (ScriptHookController.Instance == null) { + return imports.toArray(new String[0]); + } + + String context = getHookContext(); + if (context == null || context.isEmpty()) { + return imports.toArray(new String[0]); + } + + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + if (code.contains(def.hookName() + "(")) { + String[] hookImports = def.requiredImports(); + if (hookImports != null) { + Collections.addAll(imports, hookImports); + } + } + } + + return imports.toArray(new String[0]); + } + + // ==================== HOOK EXECUTION ==================== + + @Override + public Object callFunction(String hookName, Object... args) { + if (hookName == null || hookName.isEmpty()) return null; + + ensureCompiled(); + T instance = scriptBody.get(); + if (instance == null) return null; + + Object[] invokeArgs = args == null ? new Object[0] : args; + MethodHandle handle = hookResolver.resolveHookHandle(hookName, invokeArgs, instance); + if (handle == null) return null; + + try { + return sandbox.confine((PrivilegedAction) () -> { + try { + Object[] fullArgs = new Object[invokeArgs.length + 1]; + fullArgs[0] = instance; + System.arraycopy(invokeArgs, 0, fullArgs, 1, invokeArgs.length); + return handle.invokeWithArguments(fullArgs); + } catch (Throwable e) { + appendConsole("Error in " + hookName + ": " + e.getMessage()); + return null; + } + }); + } catch (Exception e) { + appendConsole("Runtime error in " + hookName + ": " + e.getMessage()); + return null; + } + } + + @Override + public void run(EnumScriptType type, Event event) { + if (type != null) callFunction(type.function, event); + } + + @Override + public void run(String hookName, Object event) { + callFunction(hookName, event); + } + + // ==================== HOOK DEFINITIONS ==================== + + protected IHookDefinition getHookDefinition(String hookName) { + if (ScriptHookController.Instance == null) return null; + + int rev = ScriptHookController.Instance.getHookRevision(); + if (hookDefCache == null || rev != lastHookRevision) { + rebuildHookDefCache(); + lastHookRevision = rev; + } + return hookDefCache.get(hookName); + } + + private void rebuildHookDefCache() { + hookDefCache = new HashMap<>(); + String context = getHookContext(); + if (context == null || context.isEmpty() || ScriptHookController.Instance == null) return; + + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + hookDefCache.put(def.hookName(), def); + } + } + + public List getHookList() { + Set hooks = new LinkedHashSet<>(); + + // 1. Add hooks from ScriptHookController (new system) + String context = getHookContext(); + if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { + hooks.addAll(ScriptHookController.Instance.getAllHooks(context)); + } + + // 2. Add methods from interface type (backward compatibility with @ParamName) + if (type != null) { + for (Method m : type.getDeclaredMethods()) { + if (!Modifier.isFinal(m.getModifiers()) && !hooks.contains(m.getName())) { + hooks.add(m.getName()); + } + } + } + + return new ArrayList<>(hooks); + } + + @Override + public String generateHookStub(String hookName, Object hookData) { + // 1. Try HookDefinition from registry (new system) + IHookDefinition def = getHookDefinition(hookName); + if (def != null) { + String typeName = def.getUsableTypeName(); + String[] params = def.paramNames(); + String paramName = (params != null && params.length > 0) ? params[0] : "event"; + + if (typeName != null && !typeName.isEmpty()) { + return String.format("public void %s(%s %s) {\n \n}\n", + def.hookName(), typeName, paramName); + } + } + + // 2. Fallback: Try interface method with @ParamName (backward compatibility) + Method method = findInterfaceMethod(hookName); + if (method != null) { + return generateMethodStub(method); + } + + // 3. Final fallback: generic stub + return String.format("public void %s(Object event) {\n \n}\n", hookName); + } + + /** + * Find a method in the type interface by name. + */ + private Method findInterfaceMethod(String methodName) { + if (type == null || methodName == null) return null; + + for (Method m : type.getDeclaredMethods()) { + if (m.getName().equals(methodName) && !Modifier.isFinal(m.getModifiers())) { + return m; + } + } + return null; + } + + /** + * Generate a method stub from a Method, supporting @ParamName annotations. + * Provides backward compatibility with interface-based hook definitions. + */ + public static String generateMethodStub(Method method) { + String mods = Modifier.toString(method.getModifiers()); + mods = mods.replace("abstract ", "").replace("abstract", "") + .replace("default ", "").replace("default", "").trim(); + if (!mods.isEmpty()) mods += " "; + + String returnTypeStr = getUsableTypeName(method.getReturnType()); + String name = method.getName(); + + Map typeCount = new HashMap<>(); + StringBuilder params = new StringBuilder(); + for (int i = 0; i < method.getParameters().length; i++) { + java.lang.reflect.Parameter p = method.getParameters()[i]; + String typeName = getUsableTypeName(p.getType()); + + // Check for @ParamName annotation first + ParamName annotation = p.getAnnotation(ParamName.class); + String paramName; + if (annotation != null) { + paramName = annotation.value(); + } else { + // Fallback to generated name + String baseName = Character.toLowerCase(p.getType().getSimpleName().charAt(0)) + + p.getType().getSimpleName().substring(1); + int count = typeCount.getOrDefault(baseName, 0) + 1; + typeCount.put(baseName, count); + paramName = count == 1 ? baseName : baseName + (count - 1); + } + + if (i > 0) params.append(", "); + params.append(typeName).append(" ").append(paramName); + } + + String body; + Class returnType = method.getReturnType(); + if (returnType == void.class) { + body = ""; + } else if (returnType.isPrimitive()) { + if (returnType == boolean.class) body = " return false;"; + else if (returnType == char.class) body = " return '\\0';"; + else body = " return 0;"; + } else { + body = " return null;"; + } + + return String.format("%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); + } + + /** + * Gets a usable type name for stub generation. + * For nested classes, returns "OuterClass.InnerClass" format. + */ + private static String getUsableTypeName(Class type) { + if (type.isPrimitive() || type.getEnclosingClass() == null) { + return type.getSimpleName(); + } + + // Build the nested class chain + StringBuilder sb = new StringBuilder(); + Class current = type; + while (current != null) { + if (sb.length() > 0) sb.insert(0, "."); + sb.insert(0, current.getSimpleName()); + current = current.getEnclosingClass(); + } + return sb.toString(); + } + + // ==================== IScriptUnit ==================== + + @Override + public String getScript() { + return script; + } + + @Override + public void setScript(String script) { + this.script = script; + this.evaluated = false; + hookResolver.clearResolutionCaches(); + } + + @Override + public List getExternalScripts() { + return externalScripts; + } + + @Override + public void setExternalScripts(List scripts) { + this.externalScripts = scripts; + this.evaluated = false; + hookResolver.clearResolutionCaches(); + } + + @Override + public TreeMap getConsole() { + return console; + } + + @Override + public void clearConsole() { + console.clear(); + } + + @Override + public void appendConsole(String message) { + if (message == null || message.isEmpty()) return; + long time = System.currentTimeMillis(); + if (console.containsKey(time)) { + message = console.get(time) + "\n" + message; + } + console.put(time, message); + while (console.size() > 40) { + console.remove(console.firstKey()); + } + } + + @Override + public String getLanguage() { + return "Java"; + } + + @Override + public void setLanguage(String language) { + } + + @Override + public boolean hasCode() { + return !externalScripts.isEmpty() || (script != null && !script.isEmpty()); + } + + @Override + public boolean hasErrored() { + return errored; + } + + @Override + public void setErrored(boolean errored) { + this.errored = errored; + } + + // ==================== NBT ==================== + + @Override + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString(IScriptUnit.NBT_TYPE_KEY, IScriptUnit.TYPE_JANINO); + compound.setTag("console", NBTTags.NBTLongStringMap(console)); + compound.setTag("externalScripts", NBTTags.nbtStringList(externalScripts)); + compound.setString("script", script); + return compound; + } + + @Override + public void readFromNBT(NBTTagCompound compound) { + this.console = NBTTags.GetLongStringMap(compound.getTagList("console", 10)); + setExternalScripts(NBTTags.getStringList(compound.getTagList("externalScripts", 10))); + setScript(compound.getString("script")); + } + + public static > S readFromNBT(NBTTagCompound compound, S script, Supplier factory) { + if (compound.hasKey("Script")) { + if (script == null) script = factory.get(); + script.readFromNBT(compound.getCompoundTag("Script")); + } + return script; + } + + public static NBTTagCompound writeToNBT(NBTTagCompound compound, JaninoScript script) { + if (script != null) compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); + return compound; + } +} diff --git a/src/main/java/noppes/npcs/janino/annotations/ParamName.java b/src/main/java/noppes/npcs/janino/annotations/ParamName.java new file mode 100644 index 000000000..deb8ed85c --- /dev/null +++ b/src/main/java/noppes/npcs/janino/annotations/ParamName.java @@ -0,0 +1,24 @@ +package noppes.npcs.janino.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies the parameter name and type to use when generating method stubs. + * Applied to interface method parameters to preserve their names and types in generated code. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ParamName { + /** + * The parameter name (e.g., "event") + */ + String value(); + + /** + * The fully qualified type name (e.g., "noppes.npcs.api.event.INpcEvent.InitEvent") + */ + String type() default ""; +} diff --git a/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java b/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java index 9b09759c0..c997729ca 100644 --- a/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java +++ b/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java @@ -56,6 +56,7 @@ public class CustomNPCsMixinPlugin implements IMixinConfigPlugin { } mixins.add("MixinItemStack"); + mixins.add("MixinItem"); } return mixins; } diff --git a/src/main/java/noppes/npcs/mixin/MixinItem.java b/src/main/java/noppes/npcs/mixin/MixinItem.java new file mode 100644 index 000000000..7f2156919 --- /dev/null +++ b/src/main/java/noppes/npcs/mixin/MixinItem.java @@ -0,0 +1,27 @@ +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 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); + } +} diff --git a/src/main/java/noppes/npcs/mixin/MixinItemStack.java b/src/main/java/noppes/npcs/mixin/MixinItemStack.java index 8615573c0..054269e23 100644 --- a/src/main/java/noppes/npcs/mixin/MixinItemStack.java +++ b/src/main/java/noppes/npcs/mixin/MixinItemStack.java @@ -1,9 +1,20 @@ package noppes.npcs.mixin; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.util.AttributeItemUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.player.GuiAuctionInterface; +import noppes.npcs.client.gui.player.GuiAuctionListing; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.containers.ContainerAuctionListing; +import noppes.npcs.controllers.data.AuctionListing; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; diff --git a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java new file mode 100644 index 000000000..727e72378 --- /dev/null +++ b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java @@ -0,0 +1,41 @@ +package noppes.npcs.roles; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.entity.EntityNPCInterface; + +public class RoleAuctioneer extends RoleInterface { + + public RoleAuctioneer(EntityNPCInterface npc) { + super(npc); + } + + @Override + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + // Simple role - no custom data needed + return compound; + } + + @Override + public void readFromNBT(NBTTagCompound compound) { + // Simple role - no custom data needed + } + + @Override + public void interact(EntityPlayer player) { + if (!ConfigMarket.AuctionEnabled) { + return; + } + + // Open the auction house GUI + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); + } + + @Override + public void delete() { + // No cleanup needed + } +} diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 107076a44..6aa69d357 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -238,9 +238,9 @@ public class RoleTrader extends RoleInterface { * Consume stock after purchase. * For shared stock, syncs all viewers (market-wide for linked markets, per-NPC otherwise). * - * @param slot The slot purchased from + * @param slot The slot purchased from * @param playerName The player making the purchase - * @param amount Amount to consume + * @param amount Amount to consume * @return true if stock was consumed successfully */ public boolean consumeStock(int slot, String playerName, int amount) { @@ -284,6 +284,7 @@ public class RoleTrader extends RoleInterface { /** * Get time until stock reset in real-time milliseconds. * Converts MC ticks to approximate real-time for client countdown display. + * * @return Remaining time in milliseconds, or -1 if no reset scheduled */ public long getResetTimeRemainingMillis() { @@ -425,7 +426,7 @@ public class RoleTrader extends RoleInterface { // Player balance PlayerData data = PlayerData.get(player); - compound.setLong("Balance", data.currencyData.getBalance()); + compound.setLong("Balance", data.tradeData.getBalance()); // Stock info compound.setBoolean("StockEnabled", stock.enableStock); diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index 26a0eb89a..c80c18d28 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -5,6 +5,8 @@ import cpw.mods.fml.common.eventhandler.EventBus; import foxz.command.ScriptedCommand; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; +import kamkeel.npcs.controllers.TelegraphController; +import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.block.Block; import net.minecraft.command.CommandHandler; import net.minecraft.entity.Entity; @@ -51,6 +53,7 @@ import noppes.npcs.api.IContainer; import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; import noppes.npcs.api.IParticle; +import noppes.npcs.api.ITelegraph; import noppes.npcs.api.IPos; import noppes.npcs.api.ISkinOverlay; import noppes.npcs.api.ITileEntity; @@ -59,9 +62,10 @@ import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.entity.IEntity; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.gui.ICustomGui; -import noppes.npcs.api.handler.IActionManager; import noppes.npcs.api.handler.IAbilityHandler; +import noppes.npcs.api.handler.IActionManager; import noppes.npcs.api.handler.IAnimationHandler; +import noppes.npcs.api.handler.IAuctionHandler; import noppes.npcs.api.handler.IAttributeHandler; import noppes.npcs.api.handler.ICloneHandler; import noppes.npcs.api.handler.ICustomEffectHandler; @@ -74,6 +78,7 @@ import noppes.npcs.api.handler.IProfileHandler; import noppes.npcs.api.handler.IQuestHandler; import noppes.npcs.api.handler.IRecipeHandler; import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.api.handler.ITelegraphHandler; import noppes.npcs.api.handler.ITransportHandler; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.api.handler.data.IFrame; @@ -85,8 +90,8 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumAnimationPart; import noppes.npcs.containers.ContainerNpcInterface; -import kamkeel.npcs.controllers.data.ability.AbilityController; import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.ChunkController; import noppes.npcs.controllers.CustomEffectController; import noppes.npcs.controllers.DialogController; @@ -96,8 +101,8 @@ import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.ScriptEntityData; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.ServerCloneController; import noppes.npcs.controllers.SpawnController; import noppes.npcs.controllers.TransportController; @@ -370,6 +375,27 @@ public class NpcAPI extends AbstractNpcAPI { return AbilityController.Instance; } + @Override + public ITelegraphHandler getTelegraphs() { + this.checkWorld(); + return TelegraphController.Instance; + } + + @Override + public ITelegraph createTelegraph(String type) { + this.checkWorld(); + return TelegraphController.Instance.create(type); + } + + @Override + public IAuctionHandler getAuctions() { + this.checkWorld(); + if (!noppes.npcs.config.ConfigMarket.AuctionEnabled) { + return null; + } + return AuctionController.getInstance(); + } + @Override public String[] getAllBiomeNames() { List biomes = new ArrayList<>(); diff --git a/src/main/java/noppes/npcs/scripted/ScriptTelegraph.java b/src/main/java/noppes/npcs/scripted/ScriptTelegraph.java new file mode 100644 index 000000000..f03be5033 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/ScriptTelegraph.java @@ -0,0 +1,210 @@ +package noppes.npcs.scripted; + +import kamkeel.npcs.controllers.TelegraphController; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import net.minecraft.entity.Entity; +import net.minecraft.world.World; +import noppes.npcs.api.ITelegraph; +import noppes.npcs.api.ITelegraphInstance; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntity; + +/** + * Script wrapper for Telegraph configuration. + * Implements ITelegraph API interface. + */ +public class ScriptTelegraph implements ITelegraph { + + private final Telegraph telegraph; + + public ScriptTelegraph(Telegraph telegraph) { + this.telegraph = telegraph; + } + + /** + * Get the underlying Telegraph object. + */ + public Telegraph getMCTelegraph() { + return telegraph; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TYPE + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public String getType() { + return telegraph.getType().name(); + } + + @Override + public void setType(String type) { + if (type == null) return; + try { + telegraph.setType(TelegraphType.valueOf(type.toUpperCase())); + } catch (IllegalArgumentException ignored) { + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // SHAPE PARAMETERS + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public float getRadius() { + return telegraph.getRadius(); + } + + @Override + public void setRadius(float radius) { + telegraph.setRadius(radius); + } + + @Override + public float getInnerRadius() { + return telegraph.getInnerRadius(); + } + + @Override + public void setInnerRadius(float innerRadius) { + telegraph.setInnerRadius(innerRadius); + } + + @Override + public float getLength() { + return telegraph.getLength(); + } + + @Override + public void setLength(float length) { + telegraph.setLength(length); + } + + @Override + public float getWidth() { + return telegraph.getWidth(); + } + + @Override + public void setWidth(float width) { + telegraph.setWidth(width); + } + + @Override + public float getAngle() { + return telegraph.getAngle(); + } + + @Override + public void setAngle(float angle) { + telegraph.setAngle(angle); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TIMING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public int getDuration() { + return telegraph.getDurationTicks(); + } + + @Override + public void setDuration(int ticks) { + telegraph.setDurationTicks(ticks); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // COLORS + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public int getColor() { + return telegraph.getColor(); + } + + @Override + public void setColor(int argb) { + telegraph.setColor(argb); + } + + @Override + public int getWarningColor() { + return telegraph.getWarningColor(); + } + + @Override + public void setWarningColor(int argb) { + telegraph.setWarningColor(argb); + } + + @Override + public int getWarningStartTick() { + return telegraph.getWarningStartTick(); + } + + @Override + public void setWarningStartTick(int tick) { + telegraph.setWarningStartTick(tick); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ANIMATION + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public boolean isAnimated() { + return telegraph.isAnimated(); + } + + @Override + public void setAnimated(boolean animated) { + telegraph.setAnimated(animated); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // POSITIONING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public float getHeightOffset() { + return telegraph.getHeightOffset(); + } + + @Override + public void setHeightOffset(float offset) { + telegraph.setHeightOffset(offset); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // SPAWN METHODS + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public ITelegraphInstance spawn(IWorld world, double x, double y, double z) { + return spawn(world, x, y, z, 0); + } + + @Override + public ITelegraphInstance spawn(IWorld world, double x, double y, double z, float yaw) { + if (world == null || world.getMCWorld() == null) return null; + World mcWorld = (World) world.getMCWorld(); + TelegraphInstance instance = TelegraphController.Instance.spawn(telegraph, mcWorld, x, y, z, yaw); + return new ScriptTelegraphInstance(instance); + } + + @Override + public ITelegraphInstance spawn(IEntity entity) { + return spawn(entity, 0); + } + + @Override + public ITelegraphInstance spawn(IEntity entity, float yaw) { + if (entity == null || entity.getMCEntity() == null) return null; + Entity mcEntity = (Entity) entity.getMCEntity(); + TelegraphInstance instance = TelegraphController.Instance.spawn(telegraph, mcEntity, yaw); + return new ScriptTelegraphInstance(instance); + } +} diff --git a/src/main/java/noppes/npcs/scripted/ScriptTelegraphInstance.java b/src/main/java/noppes/npcs/scripted/ScriptTelegraphInstance.java new file mode 100644 index 000000000..b307a6dc7 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/ScriptTelegraphInstance.java @@ -0,0 +1,122 @@ +package noppes.npcs.scripted; + +import kamkeel.npcs.controllers.TelegraphController; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import net.minecraft.entity.Entity; +import noppes.npcs.api.ITelegraphInstance; +import noppes.npcs.api.entity.IEntity; + +/** + * Script wrapper for TelegraphInstance. + * Implements ITelegraphInstance API interface. + */ +public class ScriptTelegraphInstance implements ITelegraphInstance { + + private final TelegraphInstance instance; + + public ScriptTelegraphInstance(TelegraphInstance instance) { + this.instance = instance; + } + + /** + * Get the underlying TelegraphInstance object. + */ + public TelegraphInstance getMCInstance() { + return instance; + } + + @Override + public String getInstanceId() { + return instance.getInstanceId(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // POSITION + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public double getX() { + return instance.getX(); + } + + @Override + public double getY() { + return instance.getY(); + } + + @Override + public double getZ() { + return instance.getZ(); + } + + @Override + public float getYaw() { + return instance.getYaw(); + } + + @Override + public void setPosition(double x, double y, double z) { + instance.setX(x); + instance.setY(y); + instance.setZ(z); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ENTITY FOLLOWING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public void followEntity(IEntity entity) { + if (entity == null || entity.getMCEntity() == null) return; + Entity mcEntity = (Entity) entity.getMCEntity(); + instance.setEntityIdToFollow(mcEntity.getEntityId()); + } + + @Override + public void stopFollowing() { + instance.lockPosition(); + } + + @Override + public boolean isFollowing() { + return instance.getEntityIdToFollow() >= 0; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TIMING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public int getRemainingTicks() { + return instance.getRemainingTicks(); + } + + @Override + public int getTotalTicks() { + return instance.getTotalTicks(); + } + + @Override + public float getProgress() { + return instance.getProgress(); + } + + @Override + public boolean isWarning() { + return instance.isWarning(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CONTROL + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public void remove() { + TelegraphController.Instance.remove(instance); + } + + @Override + public void lockPosition() { + instance.lockPosition(); + } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java index 8b632f7ce..f534f9dbc 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java @@ -5,6 +5,7 @@ import net.minecraft.item.ItemStack; import noppes.npcs.NoppesUtilServer; import noppes.npcs.api.IPos; import noppes.npcs.api.ITimers; +import noppes.npcs.api.ability.IDataAbilities; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.entity.IEntityLivingBase; import noppes.npcs.api.entity.IPlayer; @@ -51,6 +52,7 @@ import noppes.npcs.scripted.roles.ScriptRoleInterface; import noppes.npcs.scripted.roles.ScriptRoleMailman; import noppes.npcs.scripted.roles.ScriptRoleTrader; import noppes.npcs.scripted.roles.ScriptRoleTransporter; +import noppes.npcs.scripted.wrapper.ScriptDataAbilities; import noppes.npcs.util.ValueUtil; import java.util.ArrayList; @@ -1457,4 +1459,8 @@ public class ScriptNpc extends ScriptLiving imp public IMagicData getMagicData() { return npc.stats.magicData; } + + public IDataAbilities getAbilityData() { + return new ScriptDataAbilities(npc); + } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 85a15041e..a1b91e1db 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -39,7 +39,6 @@ import noppes.npcs.api.handler.IOverlayHandler; import noppes.npcs.api.handler.data.IAnimationData; import noppes.npcs.api.handler.data.IDialog; import noppes.npcs.api.handler.data.IMagicData; -import noppes.npcs.api.handler.data.IParty; import noppes.npcs.api.handler.data.IPlayerAttributes; import noppes.npcs.api.handler.data.IQuest; import noppes.npcs.api.handler.data.ISound; @@ -882,13 +881,13 @@ public class ScriptPlayer extends ScriptLivingBase @Override - public IPlayer[] getPartyMembers(){ + public IPlayer[] getPartyMembers() { Party party = this.getData().getPlayerParty(); if (party == null) { throw new CustomNPCsException("Player is not in party"); } - List list = new ArrayList<>(); - for(int i = 0; i < party.getPlayerNamesList().size();i++){ + List list = new ArrayList<>(); + for (int i = 0; i < party.getPlayerNamesList().size(); i++) { IPlayer player = NpcAPI.Instance().getPlayer(party.getPlayerNamesList().get(i)); if (player == null) continue; list.add(player); @@ -902,36 +901,36 @@ public class ScriptPlayer extends ScriptLivingBase @Override public long getCurrencyBalance() { - return getData().currencyData.getBalance(); + return getData().tradeData.getBalance(); } @Override public void setCurrencyBalance(long amount) { - getData().currencyData.setBalance(amount); + getData().tradeData.setBalance(amount); } @Override public boolean depositCurrency(long amount) { - return getData().currencyData.deposit(amount); + return getData().tradeData.deposit(amount); } @Override public boolean withdrawCurrency(long amount) { - return getData().currencyData.withdraw(amount); + return getData().tradeData.withdraw(amount); } @Override public boolean canAffordCurrency(long amount) { - return getData().currencyData.canAfford(amount); + return getData().tradeData.canAfford(amount); } @Override public boolean isUsingVaultCurrency() { - return getData().currencyData.isUsingVault(); + return getData().tradeData.isUsingVault(); } @Override public String getFormattedCurrencyBalance() { - return getData().currencyData.formatBalance(); + return getData().tradeData.formatBalance(); } } diff --git a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java index 9baf245a2..9bf7719d5 100644 --- a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java @@ -125,7 +125,7 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { private float knockbackUp; public HitEvent(ICustomNpc npc, Ability ability, EntityLivingBase target, - EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { + EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { super(npc, ability, target); this.hitEntity = hitEntity != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(hitEntity) : null; this.damage = damage; diff --git a/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java b/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java index a5ebfc1ff..a6291cac0 100644 --- a/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java @@ -8,24 +8,36 @@ import noppes.npcs.constants.EnumScriptType; public class DialogEvent extends PlayerEvent implements IDialogEvent { public final IDialog dialog; + public final int dialogId; + public final int optionId; - public DialogEvent(IPlayer player, IDialog dialog) { + public DialogEvent(IPlayer player, int id, int optionId, IDialog dialog) { super(player); this.dialog = dialog; + this.dialogId = id; + this.optionId = optionId; } public IDialog getDialog() { return dialog; } + public int getDialogId() { + return dialogId; + } + + public int getOptionId() { + return optionId; + } + public String getHookName() { return EnumScriptType.DIALOG_EVENT.function; } @Cancelable public static class DialogOpen extends DialogEvent implements IDialogEvent.DialogOpen { - public DialogOpen(IPlayer player, IDialog dialog) { - super(player, dialog); + public DialogOpen(IPlayer player, int id, int optionId, IDialog dialog) { + super(player, id, optionId, dialog); } public String getHookName() { @@ -35,8 +47,8 @@ public class DialogEvent extends PlayerEvent implements IDialogEvent { @Cancelable public static class DialogOption extends DialogEvent implements IDialogEvent.DialogOption { - public DialogOption(IPlayer player, IDialog dialog) { - super(player, dialog); + public DialogOption(IPlayer player, int id, int optionId, IDialog dialog) { + super(player, id, optionId, dialog); } public String getHookName() { @@ -46,13 +58,12 @@ public class DialogEvent extends PlayerEvent implements IDialogEvent { } public static class DialogClosed extends DialogEvent implements IDialogEvent.DialogClosed { - public DialogClosed(IPlayer player, IDialog dialog) { - super(player, dialog); + public DialogClosed(IPlayer player, int id, int optionId, IDialog dialog) { + super(player, id, optionId, dialog); } public String getHookName() { return EnumScriptType.DIALOG_CLOSE.function; } - } } diff --git a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java new file mode 100644 index 000000000..cb7def405 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java @@ -0,0 +1,101 @@ +package noppes.npcs.scripted.event.player; + +import cpw.mods.fml.common.eventhandler.Cancelable; +import kamkeel.npcs.controllers.data.ability.Ability; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.event.IPlayerAbilityEvent; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.scripted.NpcAPI; + +/** + * Events fired during player ability execution lifecycle. + */ +public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEvent { + protected final Ability ability; + protected final IEntityLivingBase target; + + public PlayerAbilityEvent(IPlayer player, Ability ability, EntityLivingBase target) { + super(player); + this.ability = ability; + this.target = target != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(target) : null; + } + + @Override + public IAbility getAbility() { + return ability; + } + + @Override + public IEntityLivingBase getTarget() { + return target; + } + + @Override + public String getHookName() { + return "playerAbilityEvent"; + } + + /** + * Fired when a player's ability hits an entity with damage. + * Canceling prevents the damage from being applied. + */ + @Cancelable + public static class HitEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.HitEvent { + private final IEntityLivingBase hitEntity; + private float damage; + private float knockback; + private float knockbackUp; + + public HitEvent(IPlayer player, Ability ability, EntityLivingBase target, + EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { + super(player, ability, target); + this.hitEntity = hitEntity != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(hitEntity) : null; + this.damage = damage; + this.knockback = knockback; + this.knockbackUp = knockbackUp; + } + + @Override + public IEntityLivingBase getHitEntity() { + return hitEntity; + } + + @Override + public float getDamage() { + return damage; + } + + @Override + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getKnockback() { + return knockback; + } + + @Override + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + @Override + public float getKnockbackUp() { + return knockbackUp; + } + + @Override + public void setKnockbackUp(float knockbackUp) { + this.knockbackUp = knockbackUp; + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_HIT.function; + } + } +} diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 6e85e1555..e3641570e 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -1,15 +1,19 @@ package noppes.npcs.scripted.item; import cpw.mods.fml.common.eventhandler.Event; +import kamkeel.npcs.network.packets.request.script.item.ItemScriptErrorPacket; +import kamkeel.npcs.network.packets.request.script.item.ItemScriptPacket; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemCustom; import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.controllers.data.INpcScriptHandler; +import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptHandlerPacket; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.CustomNPCsException; import java.util.ArrayList; @@ -18,13 +22,15 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; -public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCustom, INpcScriptHandler { - public List scripts = new ArrayList(); +public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCustom, IScriptHandlerPacket { + public List scripts = new ArrayList(); public List errored = new ArrayList(); public String scriptLanguage = "ECMAScript"; public boolean enabled = false; public boolean loaded = false; + private Map clientConsoleText = new TreeMap<>(); + public double durabilityValue = 1.0D; public int stackSize = 64; @@ -60,6 +66,43 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } } + @Override + public ScriptContext getContext() { + return ScriptContext.ITEM; + } + + @Override + public void requestData() { + ItemScriptErrorPacket.Get(); + ItemScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + ItemScriptPacket.Save(nbt); + } + + @Override + public GuiDataResult setGuiData(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + return new GuiDataResult(GuiDataKind.LOAD_COMPLETE, -1); + } + + if (compound.hasKey("ItemScriptConsole")) { + clientConsoleText = NBTTags.GetLongStringMap(compound.getTagList("ItemScriptConsole", 10)); + return new GuiDataResult(GuiDataKind.TAB, -1); + } + + setMCNbt(compound); + loadScriptData(); + return new GuiDataResult(GuiDataKind.METADATA, -1); + } + + @Override + public void sync() { + sendSavePacket(-1, getScripts().size(), getMCNbt()); + } + public int getType() { return 6; } @@ -69,7 +112,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } @Override - public INpcScriptHandler getScriptHandler() { + public IScriptHandler getScriptHandler() { return this; } @@ -95,14 +138,14 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } for (int i = 0; i < this.scripts.size(); i++) { - ScriptContainer script = this.scripts.get(i); + IScriptUnit script = this.scripts.get(i); if (!this.errored.contains(i)) { - if (script == null || script.errored || !script.hasCode()) + if (script == null || script.hasErrored() || !script.hasCode()) continue; script.run(hookName, event); - if (script.errored) { + if (script.hasErrored()) { this.errored.add(i); } } @@ -129,11 +172,11 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.scripts = list; } - public List getScripts() { + public List getScripts() { return this.scripts; } @@ -142,10 +185,12 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } public Map getConsoleText() { - return new TreeMap<>(); + return clientConsoleText; } public void clearConsole() { + clientConsoleText.clear(); + ItemScriptErrorPacket.Clear(); } public int getMaxStackSize() { @@ -234,7 +279,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } public void setAttackSpeed(int speed) { - if(speed <= 0) { + if (speed <= 0) { speed = 10; } this.attackSpeed = speed; diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java index 3a777134c..1c4a2ac91 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java @@ -4,7 +4,7 @@ import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.api.item.IItemLinked; import noppes.npcs.controllers.LinkedItemController; -import noppes.npcs.controllers.data.INpcScriptHandler; +import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.LinkedItem; public class ScriptLinkedItem extends ScriptCustomizableItem implements IItemLinked { @@ -51,7 +51,7 @@ public class ScriptLinkedItem extends ScriptCustomizableItem implements IItemLin } @Override - public INpcScriptHandler getScriptHandler() { + public IScriptHandler getScriptHandler() { if (this.getLinkedItem() == null) return null; return this.getLinkedItem().getScriptHandler(); diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java new file mode 100644 index 000000000..d0b5259ea --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -0,0 +1,195 @@ +package noppes.npcs.scripted.wrapper; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilitySlot; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.DataAbilities; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.api.ability.IDataAbilities; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.List; + +/** + * Script wrapper for DataAbilities providing API access to NPC abilities. + */ +public class ScriptDataAbilities implements IDataAbilities { + + private final DataAbilities data; + private final EntityNPCInterface npc; + + public ScriptDataAbilities(EntityNPCInterface npc) { + this.npc = npc; + this.data = npc.abilities; + } + + @Override + public boolean isEnabled() { + return data.enabled; + } + + @Override + public void setEnabled(boolean enabled) { + data.enabled = enabled; + } + + @Override + public IAbility[] getAbilities() { + return data.getAbilities().toArray(new IAbility[0]); + } + + @Override + public void addAbility(IAbility ability) { + if (ability instanceof Ability) { + data.addAbility((Ability) ability); + } + } + + @Override + public void addAbilityReference(String key) { + data.addAbilityReference(key); + } + + @Override + public void removeAbility(String abilityId) { + data.removeAbility(abilityId); + } + + @Override + public IAbility getAbility(String abilityId) { + return data.getAbility(abilityId); + } + + @Override + public boolean hasAbility(String abilityId) { + return data.getAbility(abilityId) != null; + } + + @Override + public boolean isAbilityReference(String abilityId) { + List slots = data.getAbilitySlots(); + for (int i = 0; i < slots.size(); i++) { + kamkeel.npcs.controllers.data.ability.AbilitySlot slot = slots.get(i); + if (slot.isReference()) { + if (slot.getReferenceId().equals(abilityId)) return true; + } else { + Ability a = slot.getAbility(); + if (a != null && a.getId().equals(abilityId)) return false; + } + } + return false; + } + + @Override + public boolean convertToInline(String abilityId) { + List slots = data.getAbilitySlots(); + for (int i = 0; i < slots.size(); i++) { + kamkeel.npcs.controllers.data.ability.AbilitySlot slot = slots.get(i); + if (slot.isReference() && slot.getReferenceId().equals(abilityId)) { + return data.convertToInline(i); + } + } + return false; + } + + @Override + public void clearAbilities() { + data.clearAbilities(); + } + + @Override + public IAbility getCurrentAbility() { + return data.getCurrentAbility(); + } + + @Override + public boolean isExecutingAbility() { + return data.isExecutingAbility(); + } + + @Override + public void interruptCurrentAbility() { + data.interruptCurrentAbility(null, 0); + } + + @Override + public int getGlobalCooldown() { + return (int) data.getRemainingCooldown(); + } + + @Override + public void setGlobalCooldown(int ticks) { + // Set the cooldown by manipulating the internal cooldown end time + // Not directly exposed, but we can reset and let it tick down + data.resetCooldown(); + // This is a best-effort implementation + } + + @Override + public void resetCooldowns() { + data.resetCooldown(); + } + + @Override + public boolean forceStartAbility(String abilityId) { + return forceStartAbility(abilityId, null); + } + + @Override + public boolean forceStartAbility(String abilityId, Object target) { + // Find the ability + Ability ability = data.getAbility(abilityId); + if (ability == null) { + return false; + } + + // Resolve target + EntityLivingBase targetEntity = resolveTarget(target); + + // Use the direct force start method + return data.forceStartAbility(ability, targetEntity); + } + + @Override + public boolean executeAbility(String key) { + return executeAbility(key, null); + } + + @Override + public boolean executeAbility(String key, Object target) { + EntityLivingBase targetEntity = resolveTarget(target); + return data.executeAbility(key, targetEntity); + } + + @Override + public IAbility createAbility(String typeId) { + if (AbilityController.Instance == null) { + return null; + } + return AbilityController.Instance.create(typeId); + } + + /** + * Resolve a target object to an EntityLivingBase. + */ + private EntityLivingBase resolveTarget(Object target) { + if (target == null) { + // Try to use the NPC's current attack target + return npc.getAttackTarget(); + } + + if (target instanceof EntityLivingBase) { + return (EntityLivingBase) target; + } + + if (target instanceof noppes.npcs.api.entity.IEntityLivingBase) { + Object mcEntity = ((noppes.npcs.api.entity.IEntityLivingBase) target).getMCEntity(); + if (mcEntity instanceof EntityLivingBase) { + return (EntityLivingBase) mcEntity; + } + } + + return null; + } +} diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java new file mode 100644 index 000000000..a36c65d51 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -0,0 +1,122 @@ +package noppes.npcs.scripted.wrapper; + +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.api.ability.IPlayerAbilityData; +import noppes.npcs.controllers.data.PlayerAbilityData; +import noppes.npcs.controllers.data.PlayerData; + +/** + * Script wrapper for PlayerAbilityData providing API access to player abilities. + */ +public class ScriptPlayerAbilityData implements IPlayerAbilityData { + + private final PlayerAbilityData data; + private final PlayerData playerData; + + public ScriptPlayerAbilityData(PlayerData playerData) { + this.playerData = playerData; + this.data = playerData.abilityData; + } + + @Override + public String[] getUnlockedAbilities() { + return data.getUnlockedAbilities(); + } + + @Override + public void unlockAbility(String key) { + data.unlockAbility(key); + } + + @Override + public void lockAbility(String key) { + data.lockAbility(key); + } + + @Override + public boolean hasUnlockedAbility(String key) { + return data.hasUnlockedAbility(key); + } + + @Override + public int getSelectedIndex() { + return data.getSelectedIndex(); + } + + @Override + public void setSelectedIndex(int index) { + data.setSelectedIndex(index); + } + + @Override + public String getSelectedAbilityKey() { + return data.getSelectedAbilityKey(); + } + + @Override + public void selectNext() { + data.selectNext(); + } + + @Override + public void selectPrevious() { + data.selectPrevious(); + } + + @Override + public boolean isExecutingAbility() { + return data.isExecutingAbility(); + } + + @Override + public IAbility getCurrentAbility() { + return data.getCurrentAbility(); + } + + @Override + public void interruptCurrentAbility() { + data.interruptCurrentAbility(); + } + + @Override + public boolean isOnCooldown() { + return data.isOnCooldown(); + } + + @Override + public boolean isOnCooldown(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.isOnCooldown(key, player); + } + + @Override + public void resetCooldown() { + data.resetCooldown(); + } + + @Override + public void resetCooldown(String key) { + data.resetCooldown(key); + } + + @Override + public void resetAllCooldowns() { + data.resetAllCooldowns(); + } + + @Override + public boolean activateAbility() { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.activateAbility(player); + } + + @Override + public boolean activateAbility(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.activateAbility(player, key); + } +} diff --git a/src/main/java/noppes/npcs/util/NPCMountUtil.java b/src/main/java/noppes/npcs/util/NPCMountUtil.java index 6303f67b9..9c3cce57f 100644 --- a/src/main/java/noppes/npcs/util/NPCMountUtil.java +++ b/src/main/java/noppes/npcs/util/NPCMountUtil.java @@ -5,10 +5,10 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.MathHelper; import noppes.npcs.CustomNpcs; +import noppes.npcs.constants.EnumRoleType; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.constants.EnumRoleType; import noppes.npcs.roles.RoleMount; public final class NPCMountUtil { diff --git a/src/main/java/noppes/npcs/util/ScriptToStringHelper.java b/src/main/java/noppes/npcs/util/ScriptToStringHelper.java new file mode 100644 index 000000000..d34e5c1cd --- /dev/null +++ b/src/main/java/noppes/npcs/util/ScriptToStringHelper.java @@ -0,0 +1,210 @@ +package noppes.npcs.util; + +import com.google.common.base.Preconditions; + +import javax.annotation.CheckForNull; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +/** + * Utility class for building toString representations of script handlers. + * Used by GlobalNPCDataScript and PlayerDataScript for notice strings. + */ +public final class ScriptToStringHelper { + private final String className; + private final ValueHolder holderHead; + private ValueHolder holderTail; + private boolean omitNullValues; + private boolean omitEmptyValues; + + private ScriptToStringHelper(String className) { + this.holderHead = new ValueHolder(); + this.holderTail = this.holderHead; + this.omitNullValues = false; + this.omitEmptyValues = false; + this.className = Preconditions.checkNotNull(className); + } + + public static ScriptToStringHelper toStringHelper(Object self) { + return new ScriptToStringHelper(self.getClass().getSimpleName()); + } + + public ScriptToStringHelper omitNullValues() { + this.omitNullValues = true; + return this; + } + + public ScriptToStringHelper add(String name, @CheckForNull Object value) { + return this.addHolder(name, value); + } + + public ScriptToStringHelper add(String name, boolean value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, char value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, double value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, float value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, int value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, long value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper addValue(@CheckForNull Object value) { + return this.addHolder(value); + } + + public ScriptToStringHelper addValue(boolean value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(char value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(double value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(float value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(int value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(long value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + private static boolean isEmpty(Object value) { + if (value instanceof CharSequence) { + return ((CharSequence) value).length() == 0; + } else if (value instanceof Collection) { + return ((Collection) value).isEmpty(); + } else if (value instanceof Map) { + return ((Map) value).isEmpty(); + } else if (value instanceof Optional) { + return !((Optional) value).isPresent(); + } else if (value instanceof OptionalInt) { + return !((OptionalInt) value).isPresent(); + } else if (value instanceof OptionalLong) { + return !((OptionalLong) value).isPresent(); + } else if (value instanceof OptionalDouble) { + return !((OptionalDouble) value).isPresent(); + } else if (value instanceof com.google.common.base.Optional) { + return !((com.google.common.base.Optional) value).isPresent(); + } else if (value.getClass().isArray()) { + return Array.getLength(value) == 0; + } else { + return false; + } + } + + public String toString() { + boolean omitNullValuesSnapshot = this.omitNullValues; + boolean omitEmptyValuesSnapshot = this.omitEmptyValues; + String nextSeparator = ""; + StringBuilder builder = (new StringBuilder(32)).append(this.className).append('{'); + for (ValueHolder valueHolder = this.holderHead.next; valueHolder != null; valueHolder = valueHolder.next) { + Object value = valueHolder.value; + if (!(valueHolder instanceof UnconditionalValueHolder)) { + if (value == null) { + if (omitNullValuesSnapshot) { + continue; + } + } else if (omitEmptyValuesSnapshot && isEmpty(value)) { + continue; + } + } + builder.append(nextSeparator); + nextSeparator = ", "; + if (valueHolder.name != null) { + builder.append(valueHolder.name).append('='); + } + if (value != null && value.getClass().isArray()) { + Object[] objectArray = new Object[]{value}; + String arrayString = Arrays.deepToString(objectArray); + builder.append(arrayString, 1, arrayString.length() - 1); + } else { + builder.append(value); + } + } + return builder.append('}').toString(); + } + + private ValueHolder addHolder() { + ValueHolder valueHolder = new ValueHolder(); + this.holderTail = this.holderTail.next = valueHolder; + return valueHolder; + } + + private ScriptToStringHelper addHolder(@CheckForNull Object value) { + ValueHolder valueHolder = this.addHolder(); + valueHolder.value = value; + return this; + } + + private ScriptToStringHelper addHolder(String name, @CheckForNull Object value) { + ValueHolder valueHolder = this.addHolder(); + valueHolder.value = value; + valueHolder.name = Preconditions.checkNotNull(name); + return this; + } + + private UnconditionalValueHolder addUnconditionalHolder() { + UnconditionalValueHolder valueHolder = new UnconditionalValueHolder(); + this.holderTail = this.holderTail.next = valueHolder; + return valueHolder; + } + + private ScriptToStringHelper addUnconditionalHolder(Object value) { + UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); + valueHolder.value = value; + return this; + } + + private ScriptToStringHelper addUnconditionalHolder(String name, Object value) { + UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); + valueHolder.value = value; + valueHolder.name = Preconditions.checkNotNull(name); + return this; + } + + private static final class UnconditionalValueHolder extends ValueHolder { + private UnconditionalValueHolder() { + super(); + } + } + + private static class ValueHolder { + @CheckForNull + String name; + @CheckForNull + Object value; + @CheckForNull + ValueHolder next; + + private ValueHolder() { + } + } +} diff --git a/src/main/java/noppes/npcs/util/ValueUtil.java b/src/main/java/noppes/npcs/util/ValueUtil.java index 48cd0b1a9..71846afcc 100644 --- a/src/main/java/noppes/npcs/util/ValueUtil.java +++ b/src/main/java/noppes/npcs/util/ValueUtil.java @@ -1,6 +1,15 @@ package noppes.npcs.util; public class ValueUtil { + + public static float lerp(float start, float end, float delta) { + return start + delta * (end - start); + } + + public static double lerp(double start, double end, double delta) { + return start + delta * (end - start); + } + public static float clamp(float given, float min, float max) { if (given < min) return min; @@ -48,4 +57,16 @@ public class ValueUtil { return max; return given; } + + public static String format(double value) { + return (value % 1 == 0) ? String.format("%.0f", value) : String.format("%.2f", value); + } + + public static String format(String precision, float value) { + return (value % 1 == 0) ? String.format("%.0f", value) : String.format(precision, value); + } + + public static String format(String precision, double value) { + return (value % 1 == 0) ? String.format("%.0f", value) : String.format(precision, value); + } } diff --git a/src/main/java/somehussar/gui/AnnotatedGui.java b/src/main/java/somehussar/gui/AnnotatedGui.java new file mode 100644 index 000000000..8cdf224d5 --- /dev/null +++ b/src/main/java/somehussar/gui/AnnotatedGui.java @@ -0,0 +1,44 @@ +package somehussar.gui; + +import net.minecraft.client.gui.GuiScreen; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; +import somehussar.gui.annotationHandling.GuiFieldHandler; +import somehussar.gui.annotationHandling.field.EditableField; + +public class AnnotatedGui extends SubGuiInterface { + private final GuiFieldHandler.ClassMetadata metadata; + + public AnnotatedGui(T object, String[] groups, GuiScreen parent) { + this(object, groups, parent, 256, 216); + } + + public AnnotatedGui(T object, String[] groups, GuiScreen parent, int xSize, int ySize) { + if (object != null) + metadata = GuiFieldHandler.getMetadata(object.getClass()); + else + metadata = null; + setBackground("menubg.png"); + this.xSize = xSize; + this.ySize = ySize; + closeOnEsc = true; + this.parent = parent; + } + + @Override + public void initGui() { + super.initGui(); + if (metadata == null) + return; + + int i = 0; + for (EditableField field : metadata.getParent().getDeclaredFields()) { + addLabel(new GuiNpcLabel(i++, field.getName(), guiLeft+8, guiTop+i*15+5)); + } + } + + @Override + public void save() { + + } +} diff --git a/src/main/java/somehussar/gui/ImmediateTextField.java b/src/main/java/somehussar/gui/ImmediateTextField.java new file mode 100644 index 000000000..5f49a9d84 --- /dev/null +++ b/src/main/java/somehussar/gui/ImmediateTextField.java @@ -0,0 +1,32 @@ +package somehussar.gui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import somehussar.gui.annotationHandling.field.EditableField; + +public class ImmediateTextField extends GuiNpcTextField{ + private final EditableField editableField; +// +// private final GuiNpcTextField encapsulatedField; + + public ImmediateTextField(int id, EditableField field, int xPos, int yPos) { + this(id, field, Minecraft.getMinecraft().fontRenderer, xPos, yPos); + } + public ImmediateTextField(int id, EditableField field, FontRenderer fontRenderer, int xPos, int yPos) { + super(id, null, fontRenderer, xPos, yPos, 0, 0, null); + + this.editableField = field; + } + + @Override + public void drawTextBox(int mousX, int mousY) { + + } + + @Override + public void mouseClicked(int xPos, int yPos, int k) { + xPos -= this.xPosition; + yPos -= this.yPosition; + } +} diff --git a/src/main/java/somehussar/gui/annotationHandling/FieldConstraint.java b/src/main/java/somehussar/gui/annotationHandling/FieldConstraint.java new file mode 100644 index 000000000..7a9b124e4 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/FieldConstraint.java @@ -0,0 +1,5 @@ +package somehussar.gui.annotationHandling; + +public interface FieldConstraint { + void validate(Object value); +} diff --git a/src/main/java/somehussar/gui/annotationHandling/FieldConstraints.java b/src/main/java/somehussar/gui/annotationHandling/FieldConstraints.java new file mode 100644 index 000000000..9a48fd1e3 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/FieldConstraints.java @@ -0,0 +1,27 @@ +package somehussar.gui.annotationHandling; + +public final class FieldConstraints { + + private static final FieldConstraints EMPTY = + new FieldConstraints(new FieldConstraint[0]); + + private final FieldConstraint[] constraints; + + public FieldConstraints(FieldConstraint[] constraints) { + this.constraints = constraints; + } + + public static FieldConstraints empty() { + return EMPTY; + } + + public void validate(Object value) { + for (FieldConstraint c : constraints) { + c.validate(value); + } + } + + public boolean isEmpty() { + return constraints.length == 0; + } +} diff --git a/src/main/java/somehussar/gui/annotationHandling/GuiEditable.java b/src/main/java/somehussar/gui/annotationHandling/GuiEditable.java new file mode 100644 index 000000000..596f3ab2f --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/GuiEditable.java @@ -0,0 +1,46 @@ +package somehussar.gui.annotationHandling; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface GuiEditable { + + @Retention(RetentionPolicy.RUNTIME) + @interface Field { + String value(); + + /** + * Display order + * Higher value = higher priority. + */ + int order() default 0; + + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Group { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Size { + int labelWidth() default 60; + int fieldWidth() default 50; + + GuiEditable.Size DEFAULT_SIZE = new GuiEditable.Size() { + @Override + public int labelWidth() { + return 60; + } + + @Override + public int fieldWidth() { + return 50; + } + @Override public Class annotationType() { return GuiEditable.Size.class; } + }; + + } +} diff --git a/src/main/java/somehussar/gui/annotationHandling/GuiFieldHandler.java b/src/main/java/somehussar/gui/annotationHandling/GuiFieldHandler.java new file mode 100644 index 000000000..c6b7f3625 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/GuiFieldHandler.java @@ -0,0 +1,140 @@ +package somehussar.gui.annotationHandling; + +import somehussar.gui.annotationHandling.constraints.ConstraintFactory; +import somehussar.gui.annotationHandling.constraints.NumericConstraint; +import somehussar.gui.annotationHandling.constraints.NumericConstraintFactory; +import somehussar.gui.annotationHandling.field.EditableField; + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class GuiFieldHandler { + + private static final Map, ClassMetadata> CACHE = new HashMap<>(); + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + public static final Map, ConstraintFactory> registry = new HashMap<>(); + + static { + registry.put(NumericConstraint.class, new NumericConstraintFactory()); + } + + + private GuiFieldHandler() {} + + public static ClassMetadata getMetadata(Class type) { + return CACHE.computeIfAbsent(type, GuiFieldHandler::scanClass); + } + + @SuppressWarnings({"RawUseOfParameterized"}) + private static ClassMetadata scanClass(Class type) { + if (!type.isAnnotationPresent(GuiEditable.class)) { + return null; + } + + ClassMetadata parent = null; + + Class superCls = type.getSuperclass(); + if (superCls != null && superCls != Object.class) { + parent = getMetadata(superCls); + } + + List list = new ArrayList<>(); + for (Field field : type.getDeclaredFields()) { + EditableField editableField = createEditableField(field); + + if (editableField != null) { + list.add(editableField); + } + } + + return new ClassMetadata( + type, + list.toArray(list.toArray(new EditableField[0])), + parent + ); + } + + @SuppressWarnings({"RawUseOfParameterized", "unchecked"}) + private static EditableField createEditableField(Field field) { + try { + GuiEditable.Field fieldConfig = field.getAnnotation(GuiEditable.Field.class); + if (fieldConfig == null) return null; + if (Modifier.isFinal(field.getModifiers())) return null; + + GuiEditable.Size size = field.getAnnotation(GuiEditable.Size.class); + if (size == null) size = GuiEditable.Size.DEFAULT_SIZE;; + + field.setAccessible(true); + EditableField.Builder builder = new EditableField.Builder() + .name(fieldConfig.value()) + .type(field.getType()) + .getter(LOOKUP.unreflectGetter(field)) + .setter(LOOKUP.unreflectSetter(field)) + .constraints(buildConstraints(field)) + .order(fieldConfig.order()) + .size(size); + + + GuiEditable.Group group = field.getAnnotation(GuiEditable.Group.class); + if (group != null) + builder.group(group.value()); + + + return builder.build(); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Failed to bind field: " + field, e); + } + } + + @SuppressWarnings({"RawUseOfParameterized", "unchecked"}) + private static FieldConstraints buildConstraints(Field field) { + List list = new ArrayList<>(); + + for (Annotation ann : field.getAnnotations()) { + ConstraintFactory factory = registry.get(ann.annotationType()); + if (factory != null) { + FieldConstraint c = factory.create(field, ann); + list.add(c); + } + } + + if (list.isEmpty()) return FieldConstraints.empty(); + return new FieldConstraints(list.toArray(new FieldConstraint[0])); + } + + + @SuppressWarnings({"RawUseOfParameterized"}) + public static final class ClassMetadata { + final Class type; + final EditableField[] fields; // only declared fields + final ClassMetadata parent; // superclass metadata (nullable) + + ClassMetadata(Class type, EditableField[] fields, ClassMetadata parent) { + this.type = type; + this.fields = fields; + this.parent = parent; + } + + public ClassMetadata getParent() { + return parent; + } + + public EditableField[] getDeclaredFields() { + return fields; + } + + public Class getType() { + return type; + } + } + + +} diff --git a/src/main/java/somehussar/gui/annotationHandling/constraints/ConstraintFactory.java b/src/main/java/somehussar/gui/annotationHandling/constraints/ConstraintFactory.java new file mode 100644 index 000000000..d62de94b4 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/ConstraintFactory.java @@ -0,0 +1,10 @@ +package somehussar.gui.annotationHandling.constraints; + +import somehussar.gui.annotationHandling.FieldConstraint; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +public interface ConstraintFactory { + FieldConstraint create(Field field, A annotation); +} diff --git a/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraint.java b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraint.java new file mode 100644 index 000000000..3b9268a30 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraint.java @@ -0,0 +1,13 @@ +package somehussar.gui.annotationHandling.constraints; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(value = RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface NumericConstraint { + double min() default Double.NEGATIVE_INFINITY; + double max() default Double.POSITIVE_INFINITY; +} diff --git a/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraintFactory.java b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraintFactory.java new file mode 100644 index 000000000..24b08ceb9 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraintFactory.java @@ -0,0 +1,13 @@ +package somehussar.gui.annotationHandling.constraints; + +import somehussar.gui.annotationHandling.FieldConstraint; + +import java.lang.reflect.Field; + +public class NumericConstraintFactory implements ConstraintFactory { + + @Override + public FieldConstraint create(Field field, NumericConstraint ann) { + return new NumericRangeConstraint(ann.min(), ann.max()); + } +} diff --git a/src/main/java/somehussar/gui/annotationHandling/constraints/NumericRangeConstraint.java b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericRangeConstraint.java new file mode 100644 index 000000000..198888d25 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericRangeConstraint.java @@ -0,0 +1,35 @@ +package somehussar.gui.annotationHandling.constraints; + +import somehussar.gui.annotationHandling.FieldConstraint; + +public final class NumericRangeConstraint implements FieldConstraint { + + private final double min; + private final double max; + + public NumericRangeConstraint(double min, double max) { + this.min = min; + this.max = max; + } + + @Override + public void validate(Object value) { + if (!(value instanceof Number)) { + throw new ConstraintViolationException("Expected number, got " + value); + } + + double v = ((Number) value).doubleValue(); + if (v < min || v > max) { + throw new ConstraintViolationException( + "Value " + v + " must be between " + min + " and " + max + ); + } + } + + public static final class ConstraintViolationException extends RuntimeException { + public ConstraintViolationException(String message) { + super(message); + } + } + +} diff --git a/src/main/java/somehussar/gui/annotationHandling/field/EditableField.java b/src/main/java/somehussar/gui/annotationHandling/field/EditableField.java new file mode 100644 index 000000000..73172ef42 --- /dev/null +++ b/src/main/java/somehussar/gui/annotationHandling/field/EditableField.java @@ -0,0 +1,85 @@ +package somehussar.gui.annotationHandling.field; + +import somehussar.gui.annotationHandling.FieldConstraints; +import somehussar.gui.annotationHandling.GuiEditable; + +import java.lang.invoke.MethodHandle; + +public final class EditableField { + + private final String name; + private final Class type; + private final MethodHandle getter; + private final MethodHandle setter; + private final FieldConstraints constraints; + private final int order; + private final String group; + private final GuiEditable.Size size; + + public String getName() { + return name; + } + + public Class getType() { + return type; + } + + @SuppressWarnings({"unchecked"}) + public T read(T instance) { + try { + return (T) getter.invoke(instance); + } catch (Throwable t) { + throw new RuntimeException("Failed to read field " + name, t); + } + } + + public void write(T instance, T value) { + constraints.validate(value); + try { + setter.invoke(instance, value); + } catch (Throwable t) { + throw new RuntimeException("Failed to write field " + name, t); + } + } + + public FieldConstraints getConstraints() { + return this.constraints; + } + + public int getOrder() { return this.order; } + + EditableField(Builder b) { + this.name = b.name; + this.type = b.type; + this.getter = b.getter; + this.setter = b.setter; + this.constraints = b.constraints; + this.order = b.order; + this.group = b.group; + this.size = b.size; + } + + public static class Builder { + private String name; + private Class type; + private MethodHandle getter; + private MethodHandle setter; + private FieldConstraints constraints; + private int order; + private String group = null; + private GuiEditable.Size size; + + public Builder name(String v) { this.name = v; return this; } + public Builder type(Class v) { this.type = v; return this; } + public Builder getter(MethodHandle v) { this.getter = v; return this; } + public Builder setter(MethodHandle v) { this.setter = v; return this; } + public Builder constraints(FieldConstraints v) { this.constraints = v; return this; } + public Builder order(int order) { this.order = order; return this; } + public Builder group(String value) { this.group = value; return this; } + public Builder size(GuiEditable.Size size) { this.size = size; return this; } + + public EditableField build() { + return new EditableField<>(this); + } + } +} diff --git a/src/main/java/somehussar/janino/AdvancedClassFilter.java b/src/main/java/somehussar/janino/AdvancedClassFilter.java new file mode 100644 index 000000000..7c2d51834 --- /dev/null +++ b/src/main/java/somehussar/janino/AdvancedClassFilter.java @@ -0,0 +1,57 @@ +package somehussar.janino; + +import io.github.somehussar.janinoloader.api.delegates.LoadClassCondition; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class AdvancedClassFilter implements LoadClassCondition { + + private final Set allowedClasses = new HashSet<>(); + private final Set allowedWildCards = new HashSet<>(); + private final Set bannedClasses = new HashSet<>(); + private final Set bannedWildCards = new HashSet<>(); + + public AdvancedClassFilter() { + addClasses("java.io.Serializable", "java.util.Iterator"); + addRegexes("java\\.lang\\..*"); + } + + public AdvancedClassFilter addRegexes(String... classRegexes) { + allowedWildCards.addAll(Arrays.stream(classRegexes).map(Pattern::compile).collect(Collectors.toList())); + return this; + } + + public AdvancedClassFilter banRegexes(String... classRegexes) { + bannedWildCards.addAll(Arrays.stream(classRegexes).map(Pattern::compile).collect(Collectors.toList())); + return this; + } + + public AdvancedClassFilter addClasses(String... classPath) { + allowedClasses.addAll(Arrays.asList(classPath)); + return this; + } + + public AdvancedClassFilter banClasses(String... classPath) { + bannedClasses.addAll(Arrays.asList(classPath)); + return this; + } + + + @Override + public boolean isValid(String name) { + if (bannedClasses.contains(name)) + return false; + if (bannedWildCards.stream().anyMatch(pattern -> pattern.matcher(name).matches())) + return false; + + if (name.startsWith("java.lang")) return true; + + if (allowedClasses.contains(name)) return true; + + return allowedWildCards.stream().anyMatch(pattern -> pattern.matcher(name).matches()); + } +} diff --git a/src/main/java/somehussar/janino/EntityUnloadListener.java b/src/main/java/somehussar/janino/EntityUnloadListener.java new file mode 100644 index 000000000..b4777e712 --- /dev/null +++ b/src/main/java/somehussar/janino/EntityUnloadListener.java @@ -0,0 +1,76 @@ +package somehussar.janino; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.world.IWorldAccess; +import noppes.npcs.entity.EntityNPCInterface; + +public class EntityUnloadListener implements IWorldAccess { + @Override + public void markBlockForUpdate(int p_147586_1_, int p_147586_2_, int p_147586_3_) { + + } + + @Override + public void markBlockForRenderUpdate(int p_147588_1_, int p_147588_2_, int p_147588_3_) { + + } + + @Override + public void markBlockRangeForRenderUpdate(int p_147585_1_, int p_147585_2_, int p_147585_3_, int p_147585_4_, int p_147585_5_, int p_147585_6_) { + + } + + @Override + public void playSound(String soundName, double x, double y, double z, float volume, float pitch) { + + } + + @Override + public void playSoundToNearExcept(EntityPlayer p_85102_1_, String p_85102_2_, double p_85102_3_, double p_85102_5_, double p_85102_7_, float p_85102_9_, float p_85102_10_) { + + } + + @Override + public void spawnParticle(String p_72708_1_, double p_72708_2_, double p_72708_4_, double p_72708_6_, double p_72708_8_, double p_72708_10_, double p_72708_12_) { + + } + + @Override + public void onEntityCreate(Entity p_72703_1_) { + + } + + @Override + public void onEntityDestroy(Entity entity) { + if (entity instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) entity; + // TODO: REMOVE SCRIPTED OBJECT AND CLASSLOADER FOR IT + } + } + + @Override + public void playRecord(String p_72702_1_, int p_72702_2_, int p_72702_3_, int p_72702_4_) { + + } + + @Override + public void broadcastSound(int p_82746_1_, int p_82746_2_, int p_82746_3_, int p_82746_4_, int p_82746_5_) { + + } + + @Override + public void playAuxSFX(EntityPlayer p_72706_1_, int p_72706_2_, int p_72706_3_, int p_72706_4_, int p_72706_5_, int p_72706_6_) { + + } + + @Override + public void destroyBlockPartially(int p_147587_1_, int p_147587_2_, int p_147587_3_, int p_147587_4_, int p_147587_5_) { + + } + + @Override + public void onStaticEntitiesChanged() { + + } +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Active.json new file mode 100644 index 000000000..c98f89236 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Active.json @@ -0,0 +1,203 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -85.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": 20.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -85.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -20.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -85.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": -20.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -85.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 20.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 3, + "Smooth": 0b, + "ID": 12, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_BeamDual_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Windup.json new file mode 100644 index 000000000..d793e3f22 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Windup.json @@ -0,0 +1,201 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": -90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Duration": 60 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 75.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -75.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 25.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -31.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 5 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 15 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 2, + "Smooth": 0b, + "ID": 11, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_BeamDual_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json new file mode 100644 index 000000000..e285bc679 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json @@ -0,0 +1,86 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 12.0f, + "Rotation0": -75.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 50.0f + }, + { + "Pivot2": -4.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 18.0f, + "Pivot0": -4.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": -18.0f, + "Rotation0": -107.0f, + "Pivot0": -1.0f, + "Pivot1": 2.0f, + "Rotation2": 50.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 30 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 2, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Beam_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json new file mode 100644 index 000000000..62e54167a --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json @@ -0,0 +1,230 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 50.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 94.0f, + "Rotation0": -94.0f, + "Pivot0": -4.0f, + "Pivot1": 3.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 75.0f, + "Rotation0": -56.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + } + ], + "Smooth": 0b, + "Duration": 5 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.25f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 25.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 107.0f, + "Rotation0": -94.0f, + "Pivot0": -5.0f, + "Pivot1": 3.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 12.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 88.0f, + "Rotation0": -56.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + } + ], + "Smooth": 0b, + "Duration": 30 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -4.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 18.0f, + "Pivot0": -4.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 12.0f, + "Rotation0": -75.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 50.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": -18.0f, + "Rotation0": -107.0f, + "Pivot0": -1.0f, + "Pivot1": 2.0f, + "Rotation2": 50.0f + } + ], + "Smooth": 0b, + "Duration": 5 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 1, + "Smooth": 0b, + "ID": 1, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Beam_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Active.json new file mode 100644 index 000000000..1fdf06c24 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Active.json @@ -0,0 +1,86 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 18.0f, + "Rotation0": 88.0f, + "Pivot0": -10.0f, + "Pivot1": 0.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 6.0f, + "Rotation0": 82.0f, + "Pivot0": 3.0f, + "Pivot1": 3.0f, + "Rotation2": 88.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_LEG", + "Rotation1": 107.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 56.0f, + "Rotation0": 0.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 20 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 14, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Cutter_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Windup.json new file mode 100644 index 000000000..153d90f12 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Windup.json @@ -0,0 +1,156 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 1.0f, + "Part": "LEFT_ARM", + "Rotation1": -31.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": -31.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": -31.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": -75.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": -50.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 88.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 18.0f, + "Rotation0": 12.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 20 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 18.0f, + "Rotation0": 88.0f, + "Pivot0": -10.0f, + "Pivot1": 0.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 6.0f, + "Rotation0": 82.0f, + "Pivot0": 3.0f, + "Pivot1": 3.0f, + "Rotation2": 88.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_LEG", + "Rotation1": 107.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 56.0f, + "Rotation0": 0.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 13, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Cutter_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json new file mode 100644 index 000000000..8fbaa73da --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json @@ -0,0 +1,392 @@ +{ + "Loop": 5, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.5f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 75.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 37.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -12.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 75.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 37.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -12.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 75.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 37.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -12.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 18, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_DiscDual_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json new file mode 100644 index 000000000..080306c95 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json @@ -0,0 +1,293 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": -8.0f, + "Part": "BODY", + "Rotation1": 25.0f, + "Rotation0": 31.0f, + "Pivot0": -2.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 50.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": -8.0f, + "Part": "LEFT_ARM", + "Rotation1": 31.0f, + "Rotation0": -56.0f, + "Pivot0": -2.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_LEG", + "Rotation1": 25.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 360.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -8.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 2.0f, + "Rotation2": -12.0f + } + ], + "Smooth": 0b, + "Duration": 10 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.35f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 50.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 50.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 170.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 75.0f, + "Rotation0": -107.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -6.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 50.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": -4.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 360.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 46 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 50.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 50.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 170.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 75.0f, + "Rotation0": -107.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -6.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 50.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": -4.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 2b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 1, + "Smooth": 0b, + "ID": 17, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_DiscDual_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json new file mode 100644 index 000000000..13fe07485 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": -44.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 4.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_ARM", + "Rotation1": 31.0f, + "Rotation0": 44.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": -82.0f + }, + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 12.0f + } + ], + "Smooth": 0b, + "Duration": 10 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 9, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Disc_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json new file mode 100644 index 000000000..a8231ff50 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json @@ -0,0 +1,896 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.8f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + } + ], + "Smooth": 0b, + "Duration": 12 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.2f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 25.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -2.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.25f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 110.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -12.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.4f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 120.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": -2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 1.0f, + "Part": "LEFT_ARM", + "Rotation1": -25.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.6f, + "FrameParts": [ + { + "Pivot2": -2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 25.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -2.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 110.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -12.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.2f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 120.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.25f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.5f, + "FrameParts": [ + { + "Pivot2": -2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 1.0f, + "Part": "LEFT_ARM", + "Rotation1": -25.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + }, + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.5f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": -44.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 4.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_ARM", + "Rotation1": 31.0f, + "Rotation0": 44.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": -82.0f + }, + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 12.0f + } + ], + "Smooth": 0b, + "Duration": 4 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 11, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Disc_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Active.json new file mode 100644 index 000000000..fa6b44543 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 6.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -63.0f, + "Rotation0": -31.0f, + "Pivot0": -2.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 20 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 19, + "Smooth": 0b, + "ID": 16, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_HeavyHit_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json new file mode 100644 index 000000000..15d49bce6 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json @@ -0,0 +1,1274 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.8f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 31.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -37.0f, + "Rotation0": -37.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 10 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 6.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -63.0f, + "Rotation0": -31.0f, + "Pivot0": -2.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 2 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 19, + "Smooth": 0b, + "ID": 15, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_HeavyHit_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Laser_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Active.json new file mode 100644 index 000000000..5cb4fc3be --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -195.0f, + "Pivot0": -1.0f, + "Pivot1": -1.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": -1.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -37.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 3, + "Smooth": 0b, + "ID": 10, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Laser_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json new file mode 100644 index 000000000..c445faca9 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json @@ -0,0 +1,329 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 2.0f, + "Rotation0": -88.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 86.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -92.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 94.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": -2.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 80.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -37.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -195.0f, + "Pivot0": -1.0f, + "Pivot1": -1.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": -1.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 3, + "Smooth": 0b, + "ID": 12, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Laser_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Orb_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Active.json new file mode 100644 index 000000000..994e2190c --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -37.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_LEG", + "Rotation1": -75.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -50.0f, + "Rotation0": 50.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": -6.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -44.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 30 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 13, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Orb_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json new file mode 100644 index 000000000..1be8ef71c --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json @@ -0,0 +1,129 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 3.0f, + "Part": "RIGHT_LEG", + "Rotation1": 69.0f, + "Rotation0": 12.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -88.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -90.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 70.0f, + "Rotation0": -125.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -50.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 31.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 26 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -37.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_LEG", + "Rotation1": -75.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -50.0f, + "Rotation0": 50.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": -6.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -44.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 3, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Orb_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json new file mode 100644 index 000000000..da3e8d61f --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json @@ -0,0 +1,84 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 107.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -208.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -208.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 14, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Slam_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json new file mode 100644 index 000000000..0c22ac017 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json @@ -0,0 +1,147 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.25f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 82.0f, + "Pivot0": 0.0f, + "Pivot1": 7.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 27 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -88.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 15, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Slam_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 715ca4884..8f5af1340 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -115,10 +115,15 @@ gui.play=Play gui.selectSound=Select Sound gui.selectnpc=Select NPC gui.select=Select +gui.sync=Sync gui.range=Range gui.partly=Partially gui.copy=Copy gui.clear=Clear +gui.builtin=Built-in +gui.custom=Custom +gui.builtinAnimations=Built-in Animations +gui.customAnimations=Custom Animations gui.done=Done gui.paste=Paste gui.test=Test @@ -157,6 +162,9 @@ gui.create=Create gui.rename=Rename gui.playerMap=Regen PlayerMap gui.unused=Unused +gui.apply=Apply +gui.cancel=Cancel +gui.yes=Yes gui.lineColor1=Upper line color gui.lineColor2=Middle line color @@ -681,6 +689,8 @@ global.naturalspawn=Spawns global.linked=Linked global.customeffects=Effects global.magic=Magic +global.abilities=Abilities +global.auction=Auction bank.slot=Slot bank.unlock=Unlock @@ -1501,6 +1511,8 @@ menu.abilities=Abilities ability.availableTypes=Available Types ability.npcAbilities=NPC Abilities ability.globalCooldown=Global Cooldown +ability.minCooldown=Min CD +ability.maxCooldown=Max CD ability.edit=Edit Ability ability.weight=Weight ability.targeting=Targeting @@ -1509,11 +1521,15 @@ ability.maxRange=Max Range ability.timing=Timing (ticks) ability.cooldown=Cooldown ability.windup=Wind Up -ability.active=Active -ability.recovery=Recovery +ability.syncOn=Sync: On +ability.syncOff=Sync: Off +ability.dazed=Dazed Ticks ability.interruptible=Interruptible ability.threshold=Threshold ability.lockMove=Lock Move +ability.lockMove.windup=Windup +ability.lockMove.active=Active +ability.lockMove.both=Both ability.telegraph=Telegraph ability.telegraphType=Telegraph Type @@ -1521,6 +1537,48 @@ ability.target.aggro_target=Target ability.target.self=Self ability.target.aoe_self=AOE Self ability.target.aoe_target=AOE Target +ability.syncWindup=Sync Windup + +# Anchor Point +ability.anchor.front=Front +ability.anchor.center=Center +ability.anchor.rightHand=Right Hand +ability.anchor.leftHand=Left Hand +ability.anchor.aboveHead=Above Head +ability.anchor.chest=Chest + +# Sweep Mode +ability.sweep.swipe=Swipe +ability.sweep.spin=Spin + +# Dash Mode +ability.dash.aggressive=Aggressive +ability.dash.defensive=Defensive + +# Counter Type +ability.counter.flat=Flat +ability.counter.percent=Percent + +# Hazard Shape +ability.shape.circle=Circle +ability.shape.ring=Ring +ability.shape.cone=Cone + +# Placement Mode +ability.placement.atCaster=At Caster +ability.placement.atTarget=At Target +ability.placement.followCaster=Follow Caster +ability.placement.followTarget=Follow Target + +# Teleport Mode +ability.teleport.blink=Blink +ability.teleport.behind=Behind +ability.teleport.single=Single + +# Trap Placement +ability.trapPlace.atCaster=At Caster +ability.trapPlace.atTarget=At Target +ability.trapPlace.aheadOfCaster=Ahead of Caster telegraph.circle=Circle telegraph.ring=Ring @@ -1531,6 +1589,7 @@ telegraph.point=Point ability.tab.timing=Timing ability.tab.effects=Effects ability.tab.telegraph=Telegraph +ability.tab.visual=Visual ability.showTelegraph=Show Telegraph ability.telegraphColor=Color ability.telegraphSizeNote=Size is based on ability parameters @@ -1545,8 +1604,10 @@ ability.cnpc.slam=Slam ability.cnpc.heavy_hit=Heavy Hit ability.cnpc.cutter=Cutter ability.cnpc.beam=Beam +ability.cnpc.beam_dual=Beam Dual ability.cnpc.projectile=Projectile ability.cnpc.orb=Orb +ability.cnpc.orb_dual=Orb Dual ability.cnpc.charge=Charge ability.cnpc.dash=Dash ability.cnpc.teleport=Teleport @@ -1556,113 +1617,198 @@ ability.cnpc.guard=Guard ability.cnpc.heal=Heal ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap +ability.cnpc.disc=Disc +ability.cnpc.disc_dual=Disc Dual +ability.cnpc.laser_shot=Laser Shot +ability.cnpc.sweeper=Sweeper -# Ability GUI - Type Tab +# ═══════════════════════════════════════════════════════════════ +# Ability Config GUI +# ═══════════════════════════════════════════════════════════════ + +# General Tab +ability.weight=Weight +ability.windUpTicks=Wind Up +ability.cooldownTicks=Cooldown +ability.lockMovement=Lock Movement +ability.interruptible=Interruptible +ability.dazedTicks=Dazed Ticks +ability.targetingMode=Targeting + +# Target Tab +ability.minRange=Min Range +ability.maxRange=Max Range +ability.conditions=Conditions + +# Effects Tab +ability.windUpSound=Wind Up Sound +ability.activeSound=Active Sound +ability.windUpAnimation=Wind Up Animation +ability.activeAnimation=Active Animation +ability.showTelegraph=Show Telegraph +ability.windUpColor=Wind Up Color +ability.activeColor=Active Color + +# Section Headers +ability.section.timing=Timing +ability.section.movement=Movement +ability.section.sounds=Sounds +ability.section.animations=Animations +ability.section.telegraph=Telegraph +ability.section.beam=Beam +ability.section.sweep=Sweep +ability.section.push=Push +ability.section.leap=Leap +ability.section.disc=Disc +ability.section.homing=Homing +ability.section.boomerang=Boomerang +ability.section.explosive=Explosive +ability.section.colors=Colors +ability.section.effects=Effects +ability.section.dual=Dual +ability.section.beam1=Beam 1 +ability.section.beam2=Beam 2 +ability.section.disc1=Disc 1 +ability.section.disc2=Disc 2 +ability.section.orb1=Orb 1 +ability.section.orb2=Orb 2 +ability.section.counter=Counter +ability.section.area=Area +ability.section.damage=Damage +ability.section.debuffs=Debuffs +ability.section.healing=Healing +ability.section.trigger=Trigger +ability.section.aoe=AOE +ability.section.pullDamage=Pull Damage + +# Shared Sub-Section Labels (used under section headers) +gui.radius=Radius +gui.thickness=Thickness +gui.rotationSpeed=Rotation Speed +gui.strength=Strength +gui.range=Range +gui.delay=Delay +gui.density=Density +gui.length=Length +gui.width=Width +gui.height=Height +gui.value=Value +gui.chance=Chance +gui.sound=Sound +gui.animation=Animation +gui.duration=Duration +gui.amp=Amp # Common Type Settings ability.radius=Radius ability.knockback=Knockback -ability.knockbackUp=KB Up -ability.maxDist=Max Dist -ability.minDist=Min Dist +ability.knockbackUp=Knockback Up ability.distance=Distance +ability.maxDistance=Max Distance ability.lifetime=Lifetime -ability.stunTicks=Stun -ability.stunDur=Stun Dur -ability.rootDur=Root Dur -ability.rootDuration=Root Duration -ability.stunDuration=Stun Duration -ability.leapHeight=Leap Height +ability.stunTicks=Stun Ticks ability.hitWidth=Hit Width -ability.potionTime=Potion Time (s) -ability.weaknessLvl=Weakness Lvl -ability.poisonTime=Poison Time (s) -ability.slownessLvl=Slowness Lvl -ability.poisonLvl=Poison Lvl +ability.effects=Effects +ability.mode=Mode +ability.duration=Duration +ability.anchorPoint=Anchor Point +ability.rotationSpeed=Rotation Speed -# Slam Settings +# Slam ability.leapSpeed=Leap Speed +ability.leapHeight=Leap Height -# Charge Settings -ability.hitRadius=Hit Radius - -# Cutter Settings +# Cutter +ability.range=Range +ability.sweepMode=Sweep Mode +ability.spinDuration=Spin Duration ability.arcAngle=Arc Angle -ability.sweepWaves=Waves -ability.rotSpeed=Rot Speed -ability.piercing=Piercing +ability.innerRadius=Inner Radius + +# Projectile +ability.projectileType=Projectile Type # Orb Settings ability.homing=Homing ability.explosive=Explosive +ability.firingSpeed=Firing Speed +ability.amount=Amount +ability.dualCharge=Dual Charging -# Vortex Settings -ability.pullRadius=Radius -ability.pullStrength=Strength -ability.pullTo=Pull To -ability.aoe=AOE +# Charge (no unique keys) + +# Dash (no unique keys) + +# Teleport +ability.blinkRadius=Blink Radius +ability.blinkCount=Blink Count +ability.blinkDelay=Blink Delay +ability.behindDistance=Behind Distance +ability.lineOfSight=Line of Sight +ability.damageAtStart=Damage At Start +ability.damageAtEnd=Damage At End + +# Vortex +ability.pullRadius=Pull Radius +ability.pullStrength=Pull Strength ability.maxTargets=Max Targets -# Dash Settings -ability.dash.aggressive=Aggressive -ability.dash.defensive=Defensive -ability.mode=Mode -ability.behindDistance=Behind Dist +# Shockwave +ability.pushStrength=Push Strength -# Teleport Settings -ability.blinkCount=Blinks -ability.blinkDelay=Delay -ability.blinkRadius=Radius -ability.minRadius=Min Rad -ability.dmgRadius=Dmg Radius -ability.lineOfSight=Line Sight -ability.dmgAtStart=Dmg Start +# Guard +ability.damageReduction=Damage Reduction -# Projectile Settings -ability.explRadius=Expl Rad - -# Beam Settings -ability.beamLength=Length -ability.beamWidth=Width -ability.dmgInterval=Interval -ability.sweepAngle=Sweep Ang -ability.sweepSpeed=Sweep Spd -ability.lockTarget=Lock Tgt - -# Guard Settings -ability.dmgReduce=Dmg Reduce -ability.counterDmg=Counter Dmg -ability.canCounter=Counter -ability.counterChance=Counter % -ability.counterType=Counter Type -ability.counterValue=Counter Value -ability.counterSound=Counter Sound -ability.counterAnim=Counter Anim - -# Heal Settings -ability.healAmount=Amount -ability.healPercent=Percent -ability.healRadius=Radius +# Heal +ability.instantHeal=Instant Heal +ability.healAmount=Heal Amount +ability.healPercent=Heal Percent +ability.healRadius=Heal Radius ability.healSelf=Heal Self ability.healAllies=Heal Allies -# Hazard Settings -ability.dmgPerTick=Dmg/Tick -ability.debuffDur=Debuff Dur +# Hazard ability.shape=Shape ability.placement=Placement -ability.affectsCaster=Hit Caster -ability.ignoreInvuln=Ign Invuln -ability.place.caster=At Caster -ability.place.target=At Target -ability.place.followCaster=Follow Caster -ability.place.followTarget=Follow Target -ability.place.ahead=Ahead +ability.damagePerSecond=Damage/Sec +ability.damageInterval=Damage Interval +ability.slownessLevel=Slowness Level +ability.debuffDuration=Debuff Duration +ability.poisonLevel=Poison Level +ability.affectsCaster=Affects Caster -# Trap Settings -ability.triggerRadius=Trig Rad +# Trap ability.armTime=Arm Time -ability.maxTriggers=Max Trig +ability.maxTriggers=Max Triggers +ability.triggerCooldown=Trigger Cooldown + +# Sweeper +ability.sweepSpeed=Sweep Speed +ability.rotations=Rotations +ability.piercing=Piercing +ability.lockTarget=Lock Target + +# Beam / Laser +ability.beamWidth=Beam Width +ability.headSize=Head Size +ability.attachedToOwner=Attached to Owner +ability.laserWidth=Laser Width +ability.expansionSpeed=Expansion Speed +ability.lingerTicks=Linger Ticks + +# Visual Settings +ability.innerColor=Inner Color +ability.outerColor=Outer Color +ability.outerEnabled=Outer Glow +ability.outerWidth=Outer Width +ability.outerAlpha=Outer Alpha +ability.lightning=Lightning +ability.lightningDensity=Density +ability.lightningRadius=Radius + +# Dual Energy Projectile Settings +ability.dualFire=Dual Fire +ability.dualFireDelay=Fire Delay # Ability Save/Load ability.save.confirm=Save ability preset @@ -1672,29 +1818,67 @@ ability.delete.confirm=Delete ability preset gui.load=Load # Global Abilities Manager -global.abilities=Abilities ability.manager.title=Ability Manager # Explosive Hover Text ability.explosive.noBlockDamage=Explosions do not destroy blocks -# Additional Orb Settings -ability.explRadius=Expl Radius - -# Telegraph Colors (for abilities that support separate colors) -ability.windUpColor=Wind Up Color -ability.activeColor=Active Color - -# Shockwave Settings -ability.pushRadius=Push Radius -ability.pushStrength=Push Str +# Shockwave Settings (dev) ability.shockwaveFalloff=Damage/knockback drop off with distance -# Ability Config GUI +# Ability Config GUI (dev) ability.typeId=Type ID -ability.conditions=Conditions ability.noTypeSettings=No type-specific settings +# ═══════════════════════════════════════════════════════════════ +# Ability Hover Tooltips +# ═══════════════════════════════════════════════════════════════ + +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.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) +ability.hover.cooldown=Time before ability can be used again (20 ticks = 1 second) +ability.hover.sync=Set windup ticks to match animation duration +ability.hover.minRange=Target must be at least this far away +ability.hover.maxRange=Target must be within this distance +ability.hover.targeting=How the ability selects its target +ability.hover.conditions=Requirements that must be met for ability to activate +ability.hover.showTelegraph=Show visual warning indicator during windup +ability.hover.windUpColor=Color of telegraph during charge-up phase +ability.hover.activeColor=Color of telegraph when ability is active +ability.hover.homing=Projectile tracks toward target +ability.hover.homingStr=How aggressively the projectile tracks (higher = tighter turns) +ability.hover.explosive=Creates explosion on impact +ability.hover.explosionRad=Size of explosion radius +ability.hover.piercing=Hits multiple targets instead of stopping on first hit +ability.hover.lightning=Add electrical effect to visuals +ability.hover.outerGlow=Enable outer glow layer around projectile +ability.hover.anchorPoint=Where on the NPC the ability spawns from +ability.hover.dualCharge=Whether or not should create two anchor points +ability.hover.damage=Base damage dealt to targets +ability.hover.speed=Movement speed of projectile +ability.hover.lifetime=Maximum ticks before despawning +ability.hover.knockback=Push force applied to targets +ability.hover.boomerang=Projectile returns to caster after reaching max distance +ability.hover.dashMode=Aggressive dashes toward target, Defensive dashes away +ability.hover.aoe=Affects all valid targets in range instead of just one +ability.hover.dmgOnPull=Deal damage continuously while pulling targets +ability.hover.lineOfSight=Requires clear line of sight to the target +ability.hover.dmgAtStart=Deal damage at the teleport origin location +ability.hover.dmgAtEnd=Deal damage at the teleport destination +ability.hover.instant=Heals instantly instead of over time +ability.hover.healSelf=Ability can heal the casting NPC +ability.hover.healAllies=Ability can heal nearby allied NPCs +ability.hover.placement=Where the ability effect is placed (Caster, Target, etc.) +ability.hover.lockTarget=Sweeper beam tracks the target's position +ability.hover.canCounter=Enable counterattack when blocking damage +ability.hover.counterType=Flat = fixed damage, Percent = % of blocked damage +ability.hover.shape=Shape of the hazard zone (Circle, Ring, Cone) +ability.hover.affectsCaster=Hazard can damage the NPC that created it +ability.hover.sweepMode=Swipe = single arc attack, Spin = continuous rotation + # Conditions condition.edit=Edit Condition condition.hp_above=NPC HP Above @@ -1705,3 +1889,179 @@ condition.hit_count=Hit Count condition.percent=Threshold condition.hits=Required Hits condition.within=Within (ticks) + +# Condition Hover Tooltips +condition.hover.type=Condition type that must be met for ability to activate +condition.hover.hpAbove=Activates when NPC health is above this percentage +condition.hover.hpBelow=Activates when NPC health is below this percentage +condition.hover.targetHpAbove=Activates when target's health is above this percentage +condition.hover.targetHpBelow=Activates when target's health is below this percentage +condition.hover.hitCount=Activates after receiving X hits within the time window +condition.hover.hits=Number of hits required to trigger condition +condition.hover.within=Time window in ticks (20 ticks = 1 second) +condition.hover.threshold=Health percentage threshold (1-99%) + +# Effect Hover Tooltips +effect.hover.type=Potion effect type to apply to targets +effect.hover.duration=Effect duration in ticks (20 ticks = 1 second) +effect.hover.amplifier=Effect strength level (higher = stronger) + +# ========================================= +# Auction House +# ========================================= + +# Auction Status +auction.status.active=Active +auction.status.ended=Ended +auction.status.cancelled=Cancelled +auction.status.claimed=Claimed + +# Auction Sort +auction.sort.newest=Newest +auction.sort.endingSoon=Ending Soon +auction.sort.priceLow=Price: Low to High +auction.sort.priceHigh=Price: High to Low +auction.sort.mostBids=Most Bids + +# Claim Types +auction.claim.item=Item +auction.claim.currency=Sale Proceeds +auction.claim.refund=Bid Refund + +# Notifications +auction.notify.won=You won an auction! +auction.notify.outbid=You were outbid! +auction.notify.sold=Your item sold! +auction.notify.expired=Your auction expired. +auction.notify.claim=You have items to claim. + +# Main GUI +auction.title=Auction House + +# Listing Display +auction.seller=Seller: %s +auction.currentBid=Current Bid: %s +auction.buyout=Buyout: %s +auction.timeLeft=Time Left: %s +auction.bids=%d bids +auction.noBids=No bids +auction.startingPrice=Starting: %s + +# Sell Page +auction.sell.startingPrice=Starting Price +auction.sell.buyoutPrice=Buyout Price +auction.sell.fee=Listing Fee: %s +auction.sell.noItem=Place an item to sell + +# Filter +auction.filter.search=Search + +# Bid Dialog +auction.bid.minimum=Minimum Bid: %s +auction.bid.yourBid=Your Bid: +auction.bid.place=Place Bid +auction.bid.placeBid=Place Bid +auction.bid.buyout=Buyout Now + +# Errors +auction.error.notEnoughCurrency=Not enough currency! +auction.error.bidTooLow=Bid must be at least %s. +auction.error.blacklisted=This item cannot be listed on the Auction House. +auction.error.boundItem=Bound items cannot be listed on the Auction House. + +# Navigation +auction.nav.listings=Browse Auctions +auction.nav.sell=Sell Item +auction.nav.trades=My Trades +auction.nav.filter=Filter + +# Claim Details +auction.claim.amount=Amount: %s +auction.claim.expires=Expires in %s days +auction.claim.expiresSoon=Expires soon! +auction.claim.clickToClaim=Click to claim + +# Filter +auction.filter.sort=Sort By +auction.filter.none=(none) +auction.filter.leftClick=Left-click: Search +auction.filter.rightClick=Right-click: Cycle Sort + +# Pagination +auction.page.prev=Previous Page +auction.page.next=Next Page + +# Sell Page +auction.sell.confirm=Confirm Listing +auction.sell.invalidPrice=Enter a valid starting price +auction.sell.buyoutTooLow=Buyout must be higher than starting price +auction.sell.placeItem=Place item here +auction.sell.slotTitle=Item to Sell +auction.sell.leftClear=Left-click to clear +auction.sell.rightRemove=Right-click to remove 1 +auction.sell.leftAdd=Left-click to add stack +auction.sell.rightAddOne=Right-click to add 1 + +# Trades Page +auction.trades.activeListing=Active Listing +auction.trades.activeBid=Active Bid +auction.trades.unavailable=Slot Unavailable +auction.trades.rightClickToCancel=Right-click to cancel +auction.trades.leftClickToClaim=Left-click to claim +auction.trades.leftClickToIncrease=Left-click to increase bid +auction.trades.winningBid=You are the highest bidder +auction.trades.soldClaim=Item Sold! +auction.trades.wonClaim=Auction Won! +auction.trades.expiredClaim=Auction Expired +auction.trades.refundClaim=Outbid - Refund Available +auction.trades.confirmCancel=Cancel Listing? +auction.trades.confirmClaim=Claim Item? +auction.trades.rightClickConfirm=Right-click again to confirm +auction.trades.leftClickConfirm=Left-click again to confirm +auction.trades.otherClickCancel=Any other click to cancel +auction.trades.buyer=Buyer +auction.trades.outbidBy=Outbid by +auction.trades.selling=Selling +auction.trades.bidding=Bidding +auction.trades.sold=Sold +auction.trades.won=Won +auction.trades.expired=Expired +auction.trades.outbid=Outbid +auction.trades.clickToRebid=Click to increase bid +auction.trades.leftClickRebid=Left-click to rebid +auction.trades.rightClickRefund=Right-click to claim refund +auction.claim.soldItem=Item: %s +auction.claim.buyer=Buyer: %s +auction.claim.outbidItem=Item: %s +auction.claim.outbidBy=Outbid by: %s + +# Sell Page - Buyout Toggle +auction.sell.allowBuyout=Allow Buy Now +auction.sell.buyoutEnabled=Enabled +auction.sell.buyoutEnabledDesc=Buyers can purchase immediately +auction.sell.buyoutEnabledDesc2=at the buyout price +auction.sell.buyoutDisabled=Disabled +auction.sell.buyoutDisabledDesc=Auction only - no instant buy +auction.sell.clickToToggle=Click to toggle +auction.sell.notEnoughItems=Not enough items in inventory + +# Auction Bidding Info +auction.info.currentBid=Current Bid +auction.info.highBidder=High Bidder +auction.info.startingPrice=Starting Price +auction.info.buyoutPrice=Buyout Price +auction.info.timeLeft=Time Left +auction.info.bidCount=Total Bids + +# Auction Bid SubGui +auction.bid.yourBalance=Your Balance: %s + +# Auction Buyout SubGui +auction.buyout.title=Buy Now +auction.buyout.confirm=Purchase this item for: + +# Auction Status +auction.ended=Ended + +# Role +role.auctioneer=Auctioneer diff --git a/src/main/resources/assets/customnpcs/textures/entity/white.png b/src/main/resources/assets/customnpcs/textures/entity/white.png new file mode 100644 index 000000000..32a20fb7c Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/entity/white.png differ diff --git a/src/main/resources/assets/customnpcs/textures/entity/whiteSmall.png b/src/main/resources/assets/customnpcs/textures/entity/whiteSmall.png new file mode 100644 index 000000000..0cdc3313a Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/entity/whiteSmall.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/auction.png b/src/main/resources/assets/customnpcs/textures/gui/auction/auction.png new file mode 100644 index 000000000..e7b299093 Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/auction.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/auction_slot.png b/src/main/resources/assets/customnpcs/textures/gui/auction/auction_slot.png new file mode 100644 index 000000000..76833ca40 Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/auction_slot.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/button.png b/src/main/resources/assets/customnpcs/textures/gui/auction/button.png new file mode 100644 index 000000000..275629c32 Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/button.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/button_press.png b/src/main/resources/assets/customnpcs/textures/gui/auction/button_press.png new file mode 100644 index 000000000..7152e278e Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/button_press.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/check_icon.png b/src/main/resources/assets/customnpcs/textures/gui/auction/check_icon.png new file mode 100644 index 000000000..07c9335ff Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/check_icon.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/down.png b/src/main/resources/assets/customnpcs/textures/gui/auction/down.png new file mode 100644 index 000000000..f76c57e1b Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/down.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/full_concept.png b/src/main/resources/assets/customnpcs/textures/gui/auction/full_concept.png new file mode 100644 index 000000000..917c7474e Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/full_concept.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/my_auctions.png b/src/main/resources/assets/customnpcs/textures/gui/auction/my_auctions.png new file mode 100644 index 000000000..67d0faa4c Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/my_auctions.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/search.png b/src/main/resources/assets/customnpcs/textures/gui/auction/search.png new file mode 100644 index 000000000..4887af00c Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/search.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/sell.png b/src/main/resources/assets/customnpcs/textures/gui/auction/sell.png new file mode 100644 index 000000000..9f4a1eecd Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/sell.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/small_button.png b/src/main/resources/assets/customnpcs/textures/gui/auction/small_button.png new file mode 100644 index 000000000..4ca4aedfe Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/small_button.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/small_button_press.png b/src/main/resources/assets/customnpcs/textures/gui/auction/small_button_press.png new file mode 100644 index 000000000..aca760502 Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/small_button_press.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/up.png b/src/main/resources/assets/customnpcs/textures/gui/auction/up.png new file mode 100644 index 000000000..f81ba084f Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/up.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon.png b/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon.png new file mode 100644 index 000000000..a801d21ae Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/icons.png b/src/main/resources/assets/customnpcs/textures/gui/icons.png index 86bf094c4..6f2993cd1 100644 Binary files a/src/main/resources/assets/customnpcs/textures/gui/icons.png and b/src/main/resources/assets/customnpcs/textures/gui/icons.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/keypreset_highres.png b/src/main/resources/assets/customnpcs/textures/gui/keypreset_highres.png new file mode 100644 index 000000000..bd393937b Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/keypreset_highres.png differ diff --git a/tools/standalone-JaninoLoader-1.0.1-ALPHA.jar b/tools/standalone-JaninoLoader-1.0.1-ALPHA.jar new file mode 100644 index 000000000..cc5b1c843 Binary files /dev/null and b/tools/standalone-JaninoLoader-1.0.1-ALPHA.jar differ