π΄ 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
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)
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 onrpg-apiandrpg-core. SalvageGuiβ 54-slot inventory;Map<UUID, SalvageSession>tracks open sessions.SalvageSessionβ holds theInventory,Player, and the current calculated yield (recalculated on every grid change).- Grid change detection:
InventoryClickEvent+InventoryDragEventfiltered to the salvage inventory instance; both re-runrecalculate()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βPlayerInteractEventon 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)andupgradeItem(id)torpg-api(or look up fromItemRegistryif reforge stones are registered RPG items). BuiltinSkill.SALVAGINGβ add torpg-api;CoreSkillRegistrypicks it up automatically.- Add
Salvageable: truefield toRpgItemYAML (defaulttrue); loaded inItemLoader, stored onCoreRpgItem, checked bySalvageGui.
β
Main Menu Item (rpg-core) β shipped in 1.10.0¶
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-adminalongside 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-coreto avoid a new plugin dependency chain; can be split torpg-achievementslater
β
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
DataStoreso it's only given once) /kitcommand 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
SetYAML 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) /statsGUI 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.)/topwith 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β &rprefix 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-smeltingmodule (same pattern asrpg-cooking/rpg-alchemy) - Move
SmeltingLoaderand any smelting-specific YAML content out ofrpg-coreinto the new plugin - The
smelting: truevanilla-suppression flag stays inrpg-core/config.ymlβ it's a world-toggle, not addon-specific rpg-coresoft-depends onrpg-smelting; if the plugin isn't loaded, smelting suppression still applies but no custom recipes are registered- Stub out a
config.ymlandrecipes/example.ymlthe same way cooking does - Build timed crafting in from the start β
rpg-smeltingshould support aCraftTimefield 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-craftingmodule - Move
RecipeLoaderand crafting YAML content out ofrpg-coreinto the new plugin - The
crafting: truevanilla-suppression flag stays inrpg-core/config.yml - Stub out
config.ymlandrecipes/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):
- PAPI β RPGCORE (consume external placeholders):
- Add
softdepend: [PlaceholderAPI]torpg-hud/plugin.ymlandrpg-core/plugin.yml -
In
PlaceholderResolver.resolve(), if PAPI is loaded, runPlaceholderAPI.setPlaceholders(player, template)as a second pass after RPGCORE's own{...}substitution (so both syntaxes work in the same template line) -
RPGCORE β PAPI (expose RPG stats to other plugins):
- Register a
PlaceholderExpansionclass inrpg-core(e.g.,RpgCorePlaceholders) with identifierrpgcore - Exposes:
%rpgcore_stat_<id>%,%rpgcore_skill_<id>_level%,%rpgcore_skill_<id>_xp%,%rpgcore_balance%,%rpgcore_health%,%rpgcore_mana%,%rpgcore_guild%,%rpgcore_party_size% - 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!) /mailor/inboxcommand β 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.urlandresource-pack.hashtorpg-core/config.yml - On
PlayerJoinEvent, callplayer.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 throughserver.propertiesinstead)
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
EnchantTriggerListenerinrpg-enchantingthat 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
/ahcommand: 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.mdfor 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
/bazaarcommand- Admin commands:
/bazaar reload,/bazaar add,/bazaar remove,/bazaar stock <item> <amount> - Non-tradeable items blocked
- See
docs/planned/bazaar.mdfor 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 idwaypoint, fireWaypointInteractHandler. WaypointInteractHandlerreads cost fromWaypointsConfig, deducts it viaRpgServices.economy()(coins) orplayer.setLevel/getLevel()(XP), then callsplayer.setBedSpawnLocation(block.getLocation(), true).- Particle emission: a repeating
BukkitTaskper placed waypoint. Spawned inWaypointBlockListeneronBlockPlaceEventfor the custom block; cancelled onBlockBreakEvent. Tasks stored inMap<Location, BukkitTask>inWaypointParticleManager. On reload, all tasks are cancelled and restarted. - Holograms: use
BlockHologramService(already exists inrpg-core) β each waypoint block registers a hologram on place, unregisters on break. Or build atoprpg-hologramsif loaded (soft-dep). - Achievement trigger:
WaypointInteractHandlercallsRpgServices.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 numbern(defaults to 1). Tab-completes to the player's unlocked vault numbers.- Players can see all vaults via a vault selector GUI (opened with
/vaultor/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.
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β loadsvaults.yml. Holds aMap<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 inrpg-api. Methods:isUnlocked(Player, int vault),unlock(Player, int vault),openVault(Player, int vault),openSelector(Player).CoreVaultServiceβ implementation. Stores per-player unlock state inDataStoreunder key"vaults"(map of vault number βtrue). Vault 1 always marked unlocked on first access. Item contents stored under"vault_contents_<n>"(serializedItemStack[]).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
InventoryCloseEventand onPlayerQuitEventas serializedItemStack[]. 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
DataStoreuntil access is re-granted. - Add
vaultcommand toplugin.ymlwith permissionrpg.core.vault(default: true) andrpg.core.vault.other(default: op).