Skip to content

πŸ”΄ New Features β€” Not Started

Full features or systems that don't exist at all yet.

Difficulty scale: 🟒 Easy (< 1 day) Β· 🟑 Medium (1–2 days) Β· πŸ”΄ Hard (several days) Β· ⚫ Very Hard (week+)


Friends System (rpg-core) β€” πŸ”΄ Hard

A persistent social graph between players. All management is done through the Friends GUI (see todo-gui.md). Chat commands (/friend add <name>, /friend remove <name>, /friend list, /friend accept <name>) are optional convenience aliases.

Data model (per player, saved in PlayerState): - List<UUID> friends β€” confirmed friends. - List<FriendRequest> incomingRequests β€” pending incoming requests (sender UUID + timestamp). - List<UUID> outgoingRequests β€” pending outgoing requests (to avoid duplicate sends).

FriendRequest is a simple DTO: { senderUuid, senderName, sentAt (game time) }. Requests expire after a configurable friends.request-expiry-game-hours (default 48 game-hours).

Config keys (in rpg-core config.yml):

friends:
  enabled: true
  max-friends: 50
  request-expiry-game-hours: 48
  allow-teleport: false       # whether the Friends GUI shows a Teleport button
  notify-on-login: true       # send a chat message listing which friends are online on join

Events / hooks: - On player join: send notify-on-login message if any friends are online. - On friend request sent: deliver a chat notification to the target if they are online; store in incomingRequests regardless. - On accept: add to both players' friends lists; remove from request queues; notify both. - On remove: remove from both players' friends lists; no notification. - Request expiry: prune stale requests in PlayerState.Load() and lazily on next GUI open.

Persistence: add friends, incomingRequests, outgoingRequests fields to SaveData. Follow the properties-not-fields rule. Wire into ResetAllData().


Salvaging System (rpg-salvaging) β€” πŸ”΄ Hard

Players feed unwanted items into a Salvager block to recover coins, XP levels, and occasionally the reforges/upgrades that were applied to those items. Yield scales with item rarity and the player's Salvaging skill level.


Block

A new custom RPG block type (salvager, defined in blocks/) that opens the Salvage GUI on right-click. Placed by admins via /rpg item give salvager_block. Hooks into the existing BlockInteractListener dispatch.

# blocks/salvager.yml
id: salvager
DisplayName: "&6Salvager"
Material: GRINDSTONE          # or a resource-pack custom-model-data override
Interactable: true
# no Toughness / RequiredPower β€” can't be broken by players in protected areas

GUI β€” 54-slot inventory

[ 0  1  2  3  4  5  6  7  8 ]   ← row 1 \
[ 9 10 11 12 13 14 15 16 17 ]   ← row 2  |
[18 19 20 21 22 23 24 25 26 ]   ← row 3  | 36 input slots (rows 1–4)
[27 28 29 30 31 32 33 34 35 ]   ← row 4 /
[36 37 38 39 40 41 42 43 44 ]   ← row 5 (border separator)
[45 46 47 48 49 50 51 52 53 ]   ← row 6 (action bar)
                  ↑        ↑
              close(49)  scrap(52)
         yield-preview(46)

Row 5 β€” all border panes (visual separator between input grid and action bar).

Row 6 slots: | Slot | Item | Behaviour | |---|---|---| | 45, 47, 48, 50, 51, 53 | Border pane | Static filler | | 46 | Yield Preview | Updates live as items are added/removed. Shows estimated coin yield, XP levels, and recovery chances in lore. Icon = GOLD_NUGGET; turns EMERALD if the grid is non-empty. | | 49 | Close (standard nav bar) | Returns all items in the grid to the player's inventory (same as closing the GUI normally). | | 52 | Scrap All | Executes salvage of every item in the grid; credits rewards; closes the GUI. LIME_DYE icon, turns GRAY_DYE (with lore "Add items first") when grid is empty. |

On any close (whether via βœ•, Close button, or server/disconnect): every ItemStack in slots 0–35 that is non-null is returned to the player's inventory; overflow drops at their feet.


What can be salvaged

An item is salvageable unless any of the following is true: - Its YAML definition includes Salvageable: false (per-item opt-out). - Its material is in salvaging.blocked-materials config list. - Its RPG item ID is in salvaging.blocked-items config list. - It is a vanilla item (no RPG stats) AND salvaging.block-vanilla-items: true (default true). - It has the PDC key rpg_no_salvage set to 1 (runtime flag, for quest items etc.).

Non-salvageable items placed in the grid are immediately returned with an action-bar warning: "&c<item name> cannot be salvaged." They are never held in the grid.


Yield calculation

Coins:

base_value      = item's ShopValue (defined on RpgItem YAML), or 0 if absent
rarity_mult     = config rarity-multipliers[item.Rarity]   (see config below)
skill_bonus     = 1.0 + (salvagingLevel Γ— config.yield-per-level)
variance        = random in [1.0 - yield-variance, 1.0 + yield-variance]
coin_yield      = base_value Γ— rarity_mult Γ— skill_bonus Γ— variance
Minimum coin yield is 1 (never zero if the item was salvageable).

XP levels (only if the item has at least one vanilla enchantment):

raw_xp = sum of (enchant.level Γ— config.enchant-xp-per-level) for each enchantment
xp_yield = ceil(raw_xp Γ— skill_bonus)
Granted via player.giveExpLevels(xpYield).

Reforge recovery (only if item has a reforge applied):

roll < config.reforge-recovery-chance + (salvagingLevel Γ— config.recovery-per-level)
β†’ give the matching reforge stone ItemStack to the player

Upgrade recovery (for each upgrade scroll applied to the item):

roll < config.upgrade-recovery-chance + (salvagingLevel Γ— config.recovery-per-level)
β†’ give one matching upgrade scroll ItemStack to the player

Recovery items go directly to the player's inventory (overflow drops at feet). Each reforge/upgrade is rolled independently.


Yield Preview item

The preview icon in slot 46 recalculates every time the contents of slots 0–35 change (InventoryClickEvent / InventoryDragEvent). Lore shows the summed estimate across all items currently in the grid:

Β§6Estimated yield
Β§e  Coins:     ~$1,240 – $1,560
Β§b  XP Levels: ~4
Β§a  Reforges:  2 items (15% chance each)
Β§a  Upgrades:  3 items (10% chance each)
Β§7
Β§7Salvaging skill bonus: +12%

Ranges are shown using the variance bounds so the player understands it's not a fixed number.


Config (plugins/rpg-salvaging/config.yml)

salvaging:
  # Fraction of ShopValue returned as coins, by rarity.
  rarity-multipliers:
    COMMON:    0.10
    UNCOMMON:  0.15
    RARE:      0.25
    EPIC:      0.40
    LEGENDARY: 0.60
    MYTHIC:    1.00

  # Vanilla XP levels granted per enchantment level on the salvaged item.
  enchant-xp-per-level: 1

  # Base probability (0.0–1.0) of recovering a reforge stone.
  reforge-recovery-chance: 0.15

  # Base probability (0.0–1.0) of recovering each applied upgrade scroll.
  upgrade-recovery-chance: 0.10

  # Additive yield bonus per Salvaging skill level (e.g. 0.005 = +0.5%/level).
  yield-per-level: 0.005

  # Additive recovery-chance bonus per Salvaging skill level.
  recovery-per-level: 0.005

  # Yield variance: actual coins = calculated Β± this fraction.
  yield-variance: 0.15

  # Prevent vanilla (non-RPG) items from being salvaged.
  block-vanilla-items: true

  # RPG item IDs that cannot be salvaged.
  blocked-items: []

  # Vanilla materials that cannot be salvaged.
  blocked-materials: []

  # Salvaging XP formula: 1 skill-XP per this many coins of yield.
  xp-per-coin: 10.0

Salvaging skill

New BuiltinSkill.SALVAGING entry in rpg-api. XP is awarded at the moment of scrapping, proportional to total coin yield (yield / config.xp-per-coin).

Milestone bonuses (applied passively via SkillsService.snapshot(); stored in stat sheet as salvaging_yield_bonus and salvaging_recovery_bonus virtual stats):

Level Bonus
1–49 +0.5% yield and +0.5% recovery per level (from yield-per-level / recovery-per-level config)
50 Expert Salvager β€” reforge recovery floor raised to 50%; upgrade recovery floor raised to 25%
100 Master Salvager β€” guaranteed at least one reforge or upgrade returned per salvage (if any were applied)

Milestone bonuses at 50 and 100 are additive on top of the base formula, not replacing it.


Implementation notes

  • New Gradle module rpg-salvaging. Depends on rpg-api and rpg-core.
  • SalvageGui β€” 54-slot inventory; Map<UUID, SalvageSession> tracks open sessions.
  • SalvageSession β€” holds the Inventory, Player, and the current calculated yield (recalculated on every grid change).
  • Grid change detection: InventoryClickEvent + InventoryDragEvent filtered to the salvage inventory instance; both re-run recalculate() after the event resolves (scheduled 1-tick-later so item moves have settled).
  • Close handling: InventoryCloseEvent β€” if session was closed without scrapping, return all items and remove session.
  • SalvageBlockListener β€” PlayerInteractEvent on the salvager custom block β†’ open GUI.
  • Reforge/upgrade item lookup: need a way to turn the stored reforge ID / upgrade ID back into an ItemStack. Add EnchantingService.reforgeItem(id) and upgradeItem(id) to rpg-api (or look up from ItemRegistry if reforge stones are registered RPG items).
  • BuiltinSkill.SALVAGING β€” add to rpg-api; CoreSkillRegistry picks it up automatically.
  • Add Salvageable: true field to RpgItem YAML (default true); loaded in ItemLoader, stored on CoreRpgItem, checked by SalvageGui.

A persistent hotbar item that acts as a hub into all major player-facing GUIs. Every player always has it; it cannot be removed, dropped, or moved.

Item behaviour: - Placed in hotbar slot 8 (last slot) on join and on respawn - If missing from slot 8 for any reason (edgecases, plugin restart): restored silently on the next inventory interaction - /menu command re-gives it if somehow lost (no permission required) - The item itself is configurable in config.yml β€” material, name, custom model data (for a resource-pack icon)

Protected interactions β€” all cancelled silently: - Drop (Q key) β†’ cancelled, item stays in slot 8 - Move to offhand (F key / swap key) β†’ cancelled - Drag out of hotbar via inventory screen β†’ cancelled, item snaps back - Death β†’ item is not added to the death drop list; restored to slot 8 on respawn

Right-click β†’ opens the Main Menu GUI (see GUI Redesigns for the full layout spec)

Implementation: PlayerInteractEvent for right-click, InventoryClickEvent + InventoryDragEvent for click-cancel, PlayerDropItemEvent for Q-key cancel, PlayerDeathEvent to strip it from drops, PlayerRespawnEvent to restore. Tag the item with PDC key rpg_menu_item so it can be identified reliably regardless of name/material changes.


Item Browser GUI (rpg-core) β€” 🟑 Medium

A /rpg items command (alias /items) that opens a paginated GUI showing every item registered across all loaded RPG plugins. Primarily an admin-facing tool, but can be made player-visible via permission.

Command: - /rpg items β€” opens the browser - /rpg items <search> β€” opens with the search term pre-applied - Permission: rpg.items.browse (default: op) to open; rpg.items.give (default: op) to click-give items

GUI layout (54 slots): - Row 1: filter toggle buttons (one slot per configured filter; active filter highlighted with enchant glint). Last slot = clear-all-filters button. - Row 2: search button (opens sign-entry prompt; current search shown in button lore), result count, prev/next page arrows - Rows 3–6 (36 slots): item display grid β€” actual ItemStack of each matching RPG item, built via ItemRegistry - Clicking an item with rpg.items.give: gives one copy to the clicker's inventory (or opens a quantity prompt if shift-clicked)

Search: case-insensitive substring match against the item's display name and its YAML id. Combine-able with active filters.

Configurable filters β€” defined in plugins/rpg-core/item-browser.yml. Each filter entry specifies a display name, GUI icon, and one or more match criteria. Admins create, remove, or rename filters in this file; /rpg reload picks up changes without a restart.

filters:
  weapons:
    DisplayName: "&cWeapons"
    Icon: IRON_SWORD
    Match:
      Type: [SWORD, BOW, CROSSBOW, WAND]

  armor:
    DisplayName: "&9Armor"
    Icon: IRON_CHESTPLATE
    Match:
      Type: [HELMET, CHESTPLATE, LEGGINGS, BOOTS, ACCESSORY]

  potions:
    DisplayName: "&dPotions"
    Icon: POTION
    Match:
      Type: [POTION, FOOD]

  tools:
    DisplayName: "&eMining Tools"
    Icon: IRON_PICKAXE
    Match:
      Type: [PICKAXE, AXE, SHOVEL]

  # Tag-based filter β€” matches items with `Tag: rare` in their YAML
  rare:
    DisplayName: "&6Rare+"
    Icon: NETHER_STAR
    Match:
      Tag: rare

Match criteria (all optional, ANDed together within one filter): - Type: [...] β€” matches items whose Type: field is in the list - Tag: <value> β€” matches items with a Tag: field equal to this value (or one of a list) - Stat: { id: DAMAGE, min: 50 } β€” matches items with at least N of a given stat (useful for "high damage" filter) - Plugin: <plugin-name> β€” matches items registered by a specific addon (e.g., rpg-alchemy only)

Multiple filters can be active simultaneously β€” results must satisfy all active filters (AND logic). Search is applied on top of whatever filters are active.

Pagination: 36 items per page; prev/next arrows hidden when not applicable. Page resets to 1 on any filter or search change.

Implementation notes: - Source of truth is RpgServices.items() (ItemRegistry API, backed by CoreItemRegistry). All plugins register their items on enable. No file-scanning at GUI open time β€” just iterate the already-loaded registry. - Filter config loaded by a new ItemBrowserConfig class; reloaded on /rpg reload. - Sign-entry for search requires the SignEntryService (see above β€” build that first if it doesn't exist, or use a book-and-quill fallback in the interim). - Filter slots in row 1 are built dynamically from the YAML; if more than 8 filters are defined, overflow filters are hidden (log a warning on load).


βœ… Player Homes + Server Warps (rpg-homes) β€” shipped in 0.1.0

Conspicuously absent from the suite. Every RPG server needs these.

  • /sethome [name], /home [name], /delhome [name], /homes β€” per-player saved locations, configurable max homes per permission group
  • /setwarp <name>, /warp <name>, /delwarp <name>, /warps β€” admin-defined server-wide teleport points
  • Homes + warps persist via DataStore
  • Teleport delay configurable (cancel on damage or movement)
  • Goes in rpg-admin alongside the existing fly/gmc/heal/tp commands

Achievement System (rpg-core or new rpg-achievements) β€” πŸ”΄ Hard

No achievement tracking exists anywhere in the suite.

Achievement YAML (plugins/rpg-core/achievements/<file>.yml):

first_kill:
  DisplayName: "&cFirst Blood"
  Description: "Kill your first custom mob."
  Category: combat
  Hidden: false          # true = doesn't show until unlocked (secret achievement)
  Rewards:
    Currency: 500
    SkillXp: { combat: 100 }
    Items: [{ Item: strength_potion, Amount: 1 }]
  Trigger:
    Type: kill_count
    Mob: any
    Count: 1

Trigger types: | Type | Fields | Fires when | |---|---|---| | kill_count | Mob: <id or any>, Count: N | Player kills N of that mob type | | skill_level | Skill: <id>, Level: N | Skill reaches level N | | money_earned | Amount: N | Lifetime currency earned reaches N | | quest_complete | Quest: <id> | Specific quest is completed | | item_obtain | Item: <id> | Player picks up that item | | stat_reach | Stat: <id>, Value: N | Player's effective stat reaches N | | manual | β€” | Only unlockable via /achievement give <id> <player> |

  • Per-player progress for count-based triggers tracked in DataStore
  • On unlock: title toast + configurable broadcast (&6<player> unlocked &e<achievement>!), broadcast toggleable in config
  • /achievements β€” opens a 54-slot GUI browser with category tabs (locked achievements shown as gray locked items if not hidden; hidden ones invisible until unlocked)
  • Progress-based achievements show a progress bar in lore (e.g., 47/100 kills)
  • Goes in rpg-core to avoid a new plugin dependency chain; can be split to rpg-achievements later

βœ… Boss Bar System (rpg-bossbar) β€” shipped in 0.1.0

No boss bar support exists. Needed by dungeons, world events, and world boss mobs.

API surface (BossBarService component on the Game object): - show(player, barId, text, progress, color, style) β€” shows or updates a named bar for that player - hide(player, barId) β€” removes the bar - update(player, barId, text, progress) β€” update text/progress without recreating - hideAll(player) β€” removes all RPG-managed bars for that player on disconnect/death/exit

barId is a string key (e.g., "dungeon:instance-uuid", "worldboss:kraken"). Players can have multiple bars simultaneously (e.g., a dungeon timer bar + a world boss HP bar at the same time).

Usage: - Dungeons: "Mobs Remaining: X / Y" bar shown to all players in an instance, updated on each mob kill. Removed on completion or player exit. - World boss: shared HP bar visible to all players in the event area. Title includes boss name + HP %. - Timed events: countdown bar (progress counts down from 1.0 to 0.0 over the event duration)

Reconnect behaviour: boss bars are client-side and lost on reconnect. BossBarService must store the current active bar state per player and re-send them on PlayerJoinEvent / PlayerRespawnEvent.


World Events + World Boss (rpg-core or new rpg-events) β€” ⚫ Very Hard

Periodic server-wide events add communal engagement that solo play can't. Planned:

  • Admin-configurable event schedule (cron-like interval or manual /event start <id>)
  • Event types: world boss spawn (named mob at a configured location, shared HP, loot pool per-player), resource rush (higher drop rates for N minutes), invasion (wave of strong mobs at a location)
  • Boss bar + broadcast announce when event starts/ends
  • Per-player loot rolls on completion

Salvaging System (rpg-enchanting) β€” 🟑 Medium

Players break down unwanted RPG items into materials at a salvage station (custom block type with StationType: salvage). Lives in rpg-enchanting since it's part of the item-modification workflow alongside enchanting and reforging.

GUI (27 slots): - Slot 11: input slot β€” place the RPG item here - Slot 13: preview slot (read-only) β€” shows what materials will be returned. Updates live as the input changes. - Slot 15: Salvage button β€” green when valid, red/gray when not. Shows item name + rarity being salvaged. - Remaining slots: GUI background panes

Yield logic: - Default yield by rarity: Common β†’ 1 common material, Uncommon β†’ 2, Rare β†’ 1 rare + 1 common, etc. Configurable defaults in config.yml - Per-item override: Salvage: [{ Item: iron_chunk, Amount: 3 }] in item YAML - Non-RPG items (vanilla items without PDC tag) can optionally be salvaged if allow-vanilla-salvage: true in config (drops nothing by default) - Items marked Salvageable: false in YAML show an error on the button: Β§cThis item cannot be salvaged - Quest items (Type: QUEST) are always non-salvageable regardless of the YAML field

Admin commands: /salvage reload to reload yield config.


βœ… Starter Kit System (rpg-kits) β€” shipped in 0.1.0

New players joining for the first time should receive a configured starting set of items.

  • Admin defines kit contents in config.yml (list of item ids + amounts)
  • Given automatically on first join (tracked per UUID in DataStore so it's only given once)
  • /kit command to claim manually if they missed it (once per UUID)
  • Optional: multiple kits gated by permission (rpg.kit.starter, rpg.kit.vip, etc.)

βœ… Item Set Bonuses (rpg-core) β€” already shipped

Wearing multiple pieces of the same named set grants additional stat bonuses. Very standard RPG feature.

  • Items in a set share a Set YAML field (e.g., Set: mages_robes)
  • Set definition file in sets/ folder: display name, bonus tiers (2-piece: { intelligence: 30 }, 4-piece: { ability_damage: 50, cooldown_reduction: 10 })
  • Bonuses apply on top of individual item stats during recalculateStats()
  • Active set bonuses shown at the bottom of the item lore (e.g., &5Mage's Robes (2/4): Intelligence +30)
  • /stats GUI shows active set bonuses in a dedicated section

Leaderboards (rpg-core) β€” 🟑 Medium

No /top or leaderboard command exists anywhere in the suite.

  • /top [category] β€” categories: money, level (overall), and one per skill (combat, mining, etc.)
  • /top with no args opens a 54-slot GUI with category tabs across the top row
  • Each entry shows: rank number, player head, player name, value (formatted with Money.Format for currency, or level number for skills)

Performance note: scanning all player records in DataStore on demand is O(n) over all players and will be slow on large servers. Use a cached snapshot approach: - A background task re-builds top-N lists on a configurable interval (leaderboard-refresh-seconds: 300 in config) - The cache stores the top 10 (or configurable) entries per category - /top reads from the cache β€” always fast, potentially slightly stale - Cache is rebuilt on plugin enable and on schedule - New/offline players are included in snapshots since they're in DataStore regardless of online status


Elite / Champion Mob Variants (rpg-core) β€” 🟑 Medium

Randomly enhanced mob spawns that are rarer, stronger, and drop better loot. Standard RPG engagement mechanic.

  • Configurable chance per-mob-spawn that it becomes an "elite" variant (e.g., 5%)
  • Elite mobs get a configurable stat multiplier (HP Γ— 3, damage Γ— 2, etc.)
  • Distinct display name prefix (e.g., &6⚜ &r prefix before the mob name)
  • Separate loot table or loot multiplier defined on the mob YAML (EliteLootTable, EliteLootMultiplier)
  • Particle effect on elite spawn (configurable)
  • Optional: champion tier above elite with even bigger multipliers

βœ… Extract Smelting β†’ rpg-smelting Plugin β€” shipped in 0.1.0

Smelting recipe loading (SmeltingLoader.java) currently lives in rpg-core. It should be its own addon plugin so servers that don't need custom smelting don't load it, and so it can be expanded independently later.

  • Create a new blank rpg-smelting module (same pattern as rpg-cooking / rpg-alchemy)
  • Move SmeltingLoader and any smelting-specific YAML content out of rpg-core into the new plugin
  • The smelting: true vanilla-suppression flag stays in rpg-core/config.yml β€” it's a world-toggle, not addon-specific
  • rpg-core soft-depends on rpg-smelting; if the plugin isn't loaded, smelting suppression still applies but no custom recipes are registered
  • Stub out a config.yml and recipes/example.yml the same way cooking does
  • Build timed crafting in from the start β€” rpg-smelting should support a CraftTime field on recipes (same model as cooking/alchemy). See Timed Smelting in Improvements for the full spec. Don't ship the plugin without it β€” retrofitting timed crafting later is messier than including it upfront.

βœ… Extract Crafting β†’ rpg-crafting Plugin β€” shipped in 0.1.0

Same rationale as smelting. RecipeLoader.java (shaped/shapeless crafting) currently lives in rpg-core.

  • Create a new blank rpg-crafting module
  • Move RecipeLoader and crafting YAML content out of rpg-core into the new plugin
  • The crafting: true vanilla-suppression flag stays in rpg-core/config.yml
  • Stub out config.yml and recipes/example.yml; flesh out further later

βœ… Sign-Entry Number Input (rpg-core) β€” shipped in 1.8.0

Required before: Auction House, Bazaar, Guild Bank GUI, any other GUI that takes a currency or quantity input from the player.

Needed everywhere a player types a numeric value (currency amount, quantity, price) inside a GUI. Build once as a shared SignEntryService in rpg-core so every addon can call it β€” don't re-implement per-plugin.

  • Open a virtual sign via PacketPlayOutOpenSignEditor
  • Line 1 = prompt label (e.g., Enter Price:)
  • Player types the number on the sign and confirms
  • Parse PacketPlayInUpdateSign; on invalid input, re-open the sign with an error hint on line 2
  • Callback-based API: SignEntryService.open(player, prompt, onResult)

Reference implementation: SurvivalCore repo.


βœ… PlaceholderAPI Support (rpg-hud) β€” shipped in 0.4.1

Allow PlaceholderAPI placeholders (e.g., %player_name%, %vault_balance%) anywhere RPGCORE reads a template string β€” scoreboard lines, tablist header/footer, nametag format, action bar format, etc.

Integration (two directions):

  1. PAPI β†’ RPGCORE (consume external placeholders):
  2. Add softdepend: [PlaceholderAPI] to rpg-hud/plugin.yml and rpg-core/plugin.yml
  3. In PlaceholderResolver.resolve(), if PAPI is loaded, run PlaceholderAPI.setPlaceholders(player, template) as a second pass after RPGCORE's own {...} substitution (so both syntaxes work in the same template line)

  4. RPGCORE β†’ PAPI (expose RPG stats to other plugins):

  5. Register a PlaceholderExpansion class in rpg-core (e.g., RpgCorePlaceholders) with identifier rpgcore
  6. Exposes: %rpgcore_stat_<id>%, %rpgcore_skill_<id>_level%, %rpgcore_skill_<id>_xp%, %rpgcore_balance%, %rpgcore_health%, %rpgcore_mana%, %rpgcore_guild%, %rpgcore_party_size%
  7. Register it on enable only if PAPI is present (soft-dep pattern: Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI"))

Offline Mail / Inbox System (rpg-core) β€” πŸ”΄ Hard

Several future systems need to deliver items or messages to players who are offline β€” most notably Auction House sale proceeds, achievement rewards, and admin broadcasts. Without a mail system, these are either silently lost or require the player to be online.

  • Per-player inbox stored in DataStore (list of mail entries: type, sender, message, attached items, timestamp)
  • Mail types: item_delivery (AH sale return / expired listing), currency_delivery (AH sold proceeds), system_message (server announcement), achievement_reward (if player was offline when triggered)
  • On join: notify player if they have unread mail (&eYou have N unread mail items!)
  • /mail or /inbox command β€” opens a GUI showing all messages, click to claim items, mark-as-read on view
  • Admin command: /mail send <player> <message> for system messages
  • Mail entries expire after a configurable duration (default 7 days) to prevent DataStore bloat
  • Items in mail slots are stored serialized (Base64 ItemStack) so they survive server restarts

βœ… Resource Pack Auto-Delivery (rpg-core) β€” shipped in 1.5.2

Server resource packs need to be sent to players on join for custom model data and fonts to work. Currently players must manually apply a resource pack or be sent one through server.properties (which applies to all players with no server-side control).

  • Add resource-pack.url and resource-pack.hash to rpg-core/config.yml
  • On PlayerJoinEvent, call player.setResourcePack(url, hash, true, Component) (the last boolean = required)
  • Configurable prompt message shown to players
  • If resource-pack.enabled: false, skip the send (for servers that manage the pack through server.properties instead)

Custom Enchantment Effects β€” Ability Triggers (rpg-enchanting) β€” πŸ”΄ Hard

Currently enchantments only add stat modifiers (e.g., +5 Strength). A natural extension is ability-trigger enchantments β€” enchants that fire an ability effect when specific conditions are met.

Enchant YAML extension:

vampiric_edge:
  DisplayName: "&4Vampiric Edge"
  Description: "Drain life from every strike."
  Rarity: RARE
  Triggers:
    - Event: on_hit          # on_hit | on_kill | on_hurt | on_use
      Chance: 25.0           # % chance per event
      Ability: drain         # ability effect id from rpg-core
      Level: 3

  • Trigger events: on_hit (melee attack lands), on_kill (killing blow), on_hurt (player takes damage), on_use (right-click with item)
  • Chance per trigger is configurable per-enchant
  • The ability effect fires the same pipeline as regular ability effects β€” uses the same DamageEffect, HealEffect, etc.
  • Multiple triggers allowed per enchant (e.g., on_hit fires drain + on_kill fires a death nova)
  • Needs an EnchantTriggerListener in rpg-enchanting that intercepts the relevant events and checks equipped item enchants

Pets System (rpg-pets) β€” ⚫ Very Hard

Referenced in several other systems (pet_luck stat, pet slot in Stats GUI, companion slot in profile GUI) but no plugin exists yet. This is a large standalone feature β€” placeholder entry to track it.

High-level planned scope: - Egg/capture system: pets obtained via loot drops or special items - Pet entity that follows the player (either a real entity or a passenger entity on the player) - Pets level up separately from the player; gain XP from kills the player makes - Pet abilities: configurable list of abilities that fire on timers or combat events - Pet stats: contribute pet_luck, passive HP/damage bonuses to the owner - Pet storage: /pets GUI to view, equip, and release pets - Multiple pets owned per player, one active at a time - Persisted via DataStore (pet species, level, XP, name, equipped status)

This is a large plugin β€” build in phases. Phase 1: pet item + summon + follow AI. Phase 2: levelling + stats. Phase 3: abilities. Phase 4: GUI.


Auction House (rpg-auction-house) β€” ⚫ Very Hard

New plugin β€” nothing exists yet.

  • Player-posted item listings with custom price (uses sign-entry for price input)
  • Browse GUI: filterable by name, category, or seller
  • /ah command: main browser, my listings, create listing, expired returns
  • Listing expiry β€” unsold listings return the item after a configurable duration (delivered via mail system)
  • Configurable listing fee (% of sale), max listings per player
  • Admin commands: /ah list <player>, /ah remove <id>, /ah wipe
  • Non-tradeable items blocked from listing
  • See docs/planned/auction-house.md for full layout spec

Bazaar (rpg-bazaar) β€” πŸ”΄ Hard

New plugin β€” nothing exists yet.

  • Admin-defined fixed-price buy/sell listings organized in categories
  • Infinite or limited stock with configurable restock intervals
  • Browse GUI with category tabs, stock counts
  • /bazaar command
  • Admin commands: /bazaar reload, /bazaar add, /bazaar remove, /bazaar stock <item> <amount>
  • Non-tradeable items blocked
  • See docs/planned/bazaar.md for full layout spec

Waypoints System (rpg-core) β€” 🟑 Medium

A player-facing fast-travel / spawn-point system using a custom placeable block (not admin warps β€” those stay in /warp). Waypoints are physical blocks placed by admins in the world; players interact with them to bind their respawn point.

Why not use warps: /warp destinations are admin-only lists. Waypoints are world objects players discover and interact with β€” exploration-flavoured spawn binding rather than a menu of teleport commands.


Block

New custom RPG block type (waypoint, defined in blocks/):

# blocks/waypoint.yml
id: waypoint
DisplayName: "&b✦ Waypoint"
Material: BEACON        # or any material; resource-pack can replace it
Interactable: true

The block is placed by admins via /rpg item give waypoint_block and cannot be broken by non-creative players (standard custom-block protection from BlockBreakHandler).


Visual effects

Both configurable in the block's YAML definition or in a waypoints.yml config:

Particles:

waypoint:
  particles:
    enabled: true
    type: END_ROD       # any Particle enum name
    count: 5
    offset-x: 0.3
    offset-y: 0.5
    offset-z: 0.3
    speed: 0.02
    interval-ticks: 10  # how often particles spawn (10 = twice/sec)

Hologram:

waypoint:
  hologram:
    enabled: true
    lines:
      - "&b✦ Waypoint"
      - "&7Right-click to set spawn"
    height-offset: 1.5  # blocks above the block's top face

The hologram line can include the waypoint's Name field (set by the admin via /waypoint setname <id> <name>), so different waypoints display different names: "&b✦ Starting Village" vs "&b✦ The Ruins".


Player interaction

Non-creative players β€” right-click: 1. Check cost (see below). If the player can't afford it, send action-bar message and cancel. 2. Deduct cost. 3. Set the player's spawn point to the waypoint block's location + Y+1 (so they respawn standing on it). 4. Send confirmation message + play a sound (BLOCK_BEACON_ACTIVATE or configurable). 5. Grant an achievement trigger waypoint_set (for discovery-based achievements).

Creative players (or players with rpg.waypoint.free permission): no cost charged.

Setting your spawn at the same waypoint twice is free (idempotent β€” no re-charge).


Cost configuration

Per-waypoint cost override OR a global default in waypoints.yml:

waypoints:
  default-cost:
    type: none            # none | coins | experience | item
    amount: 0             # coins / XP levels; ignored for "item" type
    item: null            # item id for type:item, e.g. "teleport_crystal"
    item-amount: 1
    # "experience" = Minecraft vanilla XP levels (player.getLevel())

  # Per-waypoint overrides (keyed by waypoint block UUID or admin-assigned ID):
  overrides:
    "uuid-or-id-here":
      type: coins
      amount: 500

The cost is charged every time a player sets their spawn, not on each respawn. "Item" type consumes one (or more) of the specified RPG item from the player's inventory.


Admin commands

Command Description
/waypoint list List all placed waypoints (UUID, location, name, cost)
/waypoint setname <id> <name> Give a waypoint a display name (shown in hologram)
/waypoint setcost <id> <type> <amount> Set the interaction cost for a specific waypoint
/waypoint reload Reload waypoints.yml config

Waypoints are identified by the block UUID (stored in BlockPersistence) β€” no separate data file needed.


Implementation notes

  • Hook into BlockInteractListener's existing dispatch β€” when a player right-clicks a block with PDC id waypoint, fire WaypointInteractHandler.
  • WaypointInteractHandler reads cost from WaypointsConfig, deducts it via RpgServices.economy() (coins) or player.setLevel/getLevel() (XP), then calls player.setBedSpawnLocation(block.getLocation(), true).
  • Particle emission: a repeating BukkitTask per placed waypoint. Spawned in WaypointBlockListener on BlockPlaceEvent for the custom block; cancelled on BlockBreakEvent. Tasks stored in Map<Location, BukkitTask> in WaypointParticleManager. On reload, all tasks are cancelled and restarted.
  • Holograms: use BlockHologramService (already exists in rpg-core) β€” each waypoint block registers a hologram on place, unregisters on break. Or build atop rpg-holograms if loaded (soft-dep).
  • Achievement trigger: WaypointInteractHandler calls RpgServices.achievements().grant(player, "waypoint_set") for a manual-type achievement.

Vault / Storage System (rpg-core) β€” πŸ”΄ Hard

Per-player persistent storage vaults β€” extra inventory pages players unlock over time, similar to Hypixel's storage upgrades. Admins configure how many vaults exist, their sizes, and what each one requires to unlock.


Player experience

  • /vault [n] β€” opens vault number n (defaults to 1). Tab-completes to the player's unlocked vault numbers.
  • Players can see all vaults via a vault selector GUI (opened with /vault or /vault list) β€” a 54-slot overview showing each vault slot as a chest icon, with lock status and unlock requirements in lore.
  • Items in vaults persist between sessions (stored in DataStore).
  • Vaults are per-player β€” not shared, not accessible by other players (admins can use /vault <player> [n] to inspect).

Admin configuration (plugins/rpg-core/vaults.yml)

vaults:
  # Default row count for a vault if not overridden per-vault (1-6 rows β†’ 9-54 slots).
  default-rows: 6

  # Vault definitions β€” add as many entries as you want, numbered 1 upward.
  # The vault count is derived from however many entries you define here; there is no cap.
  # Vault 1 is always unlocked (starter vault). All others require the configured unlock.
  unlocks:
    1:
      name: "Vault I"
      rows: 3            # starter vault is smaller
      requires: none     # always unlocked

    2:
      name: "Vault II"
      rows: 6
      requires:
        type: coins
        amount: 10000

    3:
      name: "Vault III"
      rows: 6
      requires:
        type: experience  # Minecraft XP levels
        amount: 30

    4:
      name: "Vault IV"
      rows: 6
      requires:
        type: item
        item: vault_key   # RPG item id
        amount: 1         # consumes this many on unlock

    5:
      name: "Vault V"
      rows: 6
      requires:
        type: permission
        permission: rpg.vault.5  # granted by rank/LuckPerms; no cost deducted

    # Add more entries to create more vaults β€” no limit.
    # 6:
    #   name: "Vault VI"
    #   rows: 6
    #   requires:
    #     type: coins
    #     amount: 500000

Unlock types:

Type Behaviour
none Always unlocked
coins Deducts via RpgServices.economy(). One-time payment.
experience Deducts Minecraft XP levels (player.setLevel). One-time payment.
item Consumes N of the specified RPG item from inventory. One-time.
permission Checks player.hasPermission(...). No deduction β€” grants access while permission holds. If permission is removed, vault is re-locked but contents are preserved (player can't access them until permission is re-granted or they unlock another way).

Vault Selector GUI

54-slot paginated overview. Each vault is represented by a chest (or custom icon). Content area is slots 0–44 (rows 1–5, 45 slots per page). Row 6 is the nav bar with Previous / Next page arrows when there are more than 45 vaults.

Slot 0 β†’ Vault 1    Slot 1 β†’ Vault 2    Slot 2 β†’ Vault 3 ...

Since the vault count is unlimited, pagination is required for large configs. Most servers will have ≀ 10 vaults and never hit the second page.

Unlocked vault icon: - Material: CHEST - Name: "&6Vault I" (or configured name) - Lore: "&7Slots: 54", "&aUnlocked", "&7Click to open"

Locked vault icon: - Material: TRAPPED_CHEST (or BARRIER) - Name: "&7Vault II" (grayed) - Lore: "&cLocked", "&7Requires: &e$10,000" (using currency formatting) - Left-click β†’ shows requirements in chat and plays a deny sound; does NOT auto-deduct

Unlock button: when a player clicks a locked vault they can afford/qualify for, a confirm prompt appears β€” either a second click on the same item ("Click again to unlock") or a confirmation slot that pops up. On confirm, cost is deducted and vault is unlocked.


Implementation notes

  • VaultConfig β€” loads vaults.yml. Holds a Map<Integer, VaultDef> (VaultDef: name, rows, UnlockRequirement) keyed by vault number. The map can have any number of entries β€” no upper limit. maxVault() returns the highest defined key. Reloaded via /rpg reload.
  • VaultService β€” interface in rpg-api. Methods: isUnlocked(Player, int vault), unlock(Player, int vault), openVault(Player, int vault), openSelector(Player).
  • CoreVaultService β€” implementation. Stores per-player unlock state in DataStore under key "vaults" (map of vault number β†’ true). Vault 1 always marked unlocked on first access. Item contents stored under "vault_contents_<n>" (serialized ItemStack[]).
  • VaultGui β€” standard 54-slot (or configured rows Γ— 9) inventory. Nav bar on last row. Items shift up if rows < 6 so the nav bar is always on the visible bottom row.
  • VaultSelectorGui β€” paginated selector (45 vaults per page, nav bar on row 6). Clicking a locked vault shows requirements; clicking an unlocked vault opens it nested (Back β†’ selector).
  • VaultCommand β€” /vault [n], tab-completes to all defined vault numbers (so players can inspect locked ones' requirements).
  • Vault contents are saved on InventoryCloseEvent and on PlayerQuitEvent as serialized ItemStack[]. Loaded lazily on first open.
  • Contents on permission-revoke: vault contents are never wiped on lock. If a permission-gated vault locks again, contents stay in DataStore until access is re-granted.
  • Add vault command to plugin.yml with permission rpg.core.vault (default: true) and rpg.core.vault.other (default: op).