🎮 Games

Parama Legends 2

A Minecraft Paper RPG plugin built with my uni friends, now that we're all graduated and actually know what we're doing. Classes, ability trees, custom models, the works.

May 8, 2026
MinecraftJavaPaperPluginRPGGroup Project

Source on GitHub

What it is

Parama Legends 2 is a Minecraft Paper RPG plugin that turns a vanilla survival server into something closer to an MMO. Classes, ability trees, mana, custom weapons, custom models, custom enemies, parties, quests, runestone gear modifiers, brewing, outposts, the works. Built on Paper 1.21.11 with the BetterModel resource pack for custom 3D models.

It is a sequel. The first Parama Legends was a passion project me and my uni friends ran on a private server during our degrees at our many universities. It was scrappy. Half-finished classes, ideas we never got around to, balancing that nobody had time to do. It was still some of the most fun I had building anything during uni, because it was just for us, played with the same group of friends every weekend. The whole point was making something for the server, not shipping something to strangers.

Parama Legends 2 is what happens when that same group comes back to it after we have all graduated and actually have engineering jobs. Same friends, same private server, same vibe of "let's make a thing for ourselves," but everyone now knows what a clean architecture looks like, what proper event handling is, what a maintainable codebase feels like. The result is an order of magnitude better than the original. It is genuinely the project I am most proud of right now.

The lead is my friend Aaqil, who set up the architecture and runs point on most of the systems. I am one of the contributors building features alongside him.

Feature overview

The plugin is doing a lot of things at once. The big systems:

  • Classes and ability trees. Eight classes so far (Swordsman, Archer, Mage, Healer, Barbarian, Reaper, Necromancer, Techies), each with their own weapon type, casting input, and a full skill-point ability tree across two tiers. Abilities split into passive, active, and active-buff types. Each class progresses from XP earned by using the class' weapon.
  • Spellcasting. Sneak plus a click input (left or right depending on the class) to enter a casting sequence. Different inputs cast different abilities. Costs mana, runs a cooldown.
  • Damage system. Centralized damage modifier pipeline with flat, additive, and multiplicative stacking, so every buff and debuff in the game composes correctly without abilities stomping each other.
  • Custom enemies. Bespoke mob types with their own AI, stats, and loot tables, separate from vanilla mobs.
  • Runestone gear modifiers. Drops with rarity tiers and modifier categories that slot into your gear. Think Path of Exile affixes but in Minecraft.
  • Brewing. Custom recipes and a BAC (drunkenness) system because of course there is.
  • Outposts and regions. Capturable points and region-based behavior with name updates.
  • Parties, quests, shops, summoning, cosmetics. All the connective tissue that makes it feel like a server and not a tech demo.
  • Custom 3D models. Through the BetterModel pack, weapons and items have actual unique geometry instead of just retextured vanilla items.

Everything is data-driven where it matters. Class data, abilities, and ability trees are defined in YAML, so adding a node or rebalancing a cooldown does not need a recompile.

What I actually built

Barbarian (Tier 1 and Tier 2)

I designed and implemented the Barbarian class end to end across both tiers, so the full ability tree, the weapon behavior, and all 14 abilities under it.

The fantasy is "big dumb guy with an axe who wants to be in melee at all times and gets angrier when he is hurt." Mechanically, the Barbarian uses axes and trades the precision of swords for raw damage, sustain, and a bunch of ways to force fights to come to him. Tier 1 establishes the kit (sweep, throw, charge, get angry). Tier 2 leans harder into the identity with the more committal nodes that make a Barbarian build feel finished.

Meat Hook is the headline. You yank an entity to you with a hook, anime-protagonist style. It needed to look like an actual hook, not a recolored fishing rod, so I custom modelled the hook itself and routed it through the BetterModel resource pack. The implementation spawns an invisible Snowball as the projectile carrier, then attaches a non-billboarded ItemDisplay to it as a passenger so the model holds its yaw to face the flight direction. The chain trail is drawn by hand: every two ticks the runnable spawns alternating light-grey and dark-grey dust along the line from the player's offhand grip to the hook, so it reads as actual chain links instead of a glowing rope. Hits travel through three phases: the hook latches and follows the target for 10 ticks via teleport, then yanks them in front of the player, applies a stun status effect, and marks them with a "shredded" PDC tag for 4 seconds (every subsequent ability hit on a shredded target adds a 20% additive damage modifier). It also stamps the player with a "next hit crit" flag, so the very next axe swing or ability hit detonates with extra particles, sound, and a 150% additive damage bonus. Miss and the hook chains break with a sound and electric spark burst. Hit a wall and the hook display sticks in place for two seconds before despawning.

Throw Axes launches three real-feeling spinning axes in sequence, ten ticks apart. Each one is a Snowball entity with its visibility hidden and a custom ItemDisplay riding it, with the display's Transformation rebuilt every tick: the yaw locks to flight direction, then a Z-rotation increments by 30 degrees per tick so the axe cartwheels blade-over-handle exactly the way a real axe throw tumbles through the air. Hits trigger a CRIT particle burst plus a red dust spray. Misses stick the axe in the block face it hit, offset out of the surface so it lands flush.

Mighty Sweep is the AoE cleave. Damages every Enemy in a 5-block radius, slows them, and renders a directional sweep arc in front of the player by sampling 11 points across a 160-degree spread and emitting SWEEP_ATTACK particles along the arc, with red dust at each impact for blood. Ravager step plus axe strip plus breeze wind burst all play layered for the impact sound.

Down Smash is the leap-and-slam. Velocity multiplies the player upward and forward, then a runnable polls every tick: while airborne the player trails GUST particles, and if they take fall damage during the buff it gets reduced by 8 to make the slam survivable. The moment the player hits the ground, it rolls a shockwave: 4.5-block radius AoE damage, a knock-up (Y velocity 0.7) so enemies stay in melee instead of getting yeeted, a 24-point ring of CRIT and EXPLOSION particles, and an "earth shatter" effect that reads the BlockData of whatever the player is standing on and sprays 60 BLOCK particles outward. Stand on stone and you get stone shrapnel, stand on grass and you get grass shards.

Primal Rage is the Tier 2 berserker buff and the most ambitious ability in the kit. On cast: a transient AttributeModifier adds 50% to the player's Attribute.SCALE so they physically grow, a title screen announcement reads "Nothing will stop you.", a layered roar plays (ravager + warden + explosion), and the player gets an 80-tick DARKNESS flash for atmosphere. While active: SPEED, STRENGTH, and HASTE are refreshed every tick, every melee axe hit adds a 30% additive damage modifier, kills extend the buff duration by up to 5 seconds (capped at the 40-second max) and heal 1 heart on a 4-second internal cooldown, and each kill triggers another short DARKNESS pulse so the world dims rhythmically as you rampage. The kicker is the "Relentless Rage" save: an EventPriority.HIGHEST listener catches any damage event with getFinalDamage() >= player.getHealth(), cancels it, and pins the player at 1 HP. It only fires once per Rage activation, and the player gets an action bar reading "YOU REFUSE TO FALL!" The whole ability is gated by a deprecation check that hides it from the equip menu once the player has unlocked the T2 upgrade.

Max Impact is the Tier 2 ground-pound. Three phases. Phase 1 (45 ticks): the player floats up via a level-6 LEVITATION effect while a transient SCALE modifier ramps from 1x up to 3x, growing them visibly into a giant. A flame ring telegraphs the landing target underneath, sized proportionally to current scale and alternating between regular and soul fire flame on each pulse. The title text "IMPACT" reveals letter by letter at 6 ticks per character. Phase 2 (15 ticks): LEVITATION switches to SLOW_FALLING so the giant hovers menacingly. Phase 3: title flashes full-size, the player teleports to the targeted ground location, deals 60 damage in a 10-block AoE, stuns for 80 ticks, and renders a 32-point shockwave ring at full radius with explosion plus crit particles plus a sonic boom plus warden attack impact plus ravager roar plus crit attack plus explosion all layered. The scale lingers 10 ticks past the slam so you actually see the giant landing before they shrink back.

Battle Frenzy is the passive stack engine. Each axe swing on an enemy adds a stack (max 10), each stack adds 2.5% additive damage to that hit, and stacks decay 3 seconds after the last refresh. The cool detail is the audio: the attack-crit sound pitch escalates with stack count, so you can hear yourself building momentum, and at 10 stacks you get a blaze + warden roar burst plus an action bar reading "BATTLE FRENZY MAX!".

Devour is the high-commitment finisher. 2-second channel where a spiral of red dust converges on the player, the warden heartbeat sound escalates (the beat interval shrinks from 16 ticks to 4 ticks as the channel completes), and the action bar shows a textual progress bar [||||......] that fills up. At the end it raycasts to a target up to 5 blocks ahead and hits for 40 damage. If it kills, the player heals for half their max HP, restocks 10 hunger points, and triggers heart particles plus a level-up sound. If there is no target, it fizzles.

Cry More is the Tier 2 taunt. 14-block radius scream that uses the status effect manager to apply TAUNTED to every hostile mob in range for the buff's remaining duration, layers four sounds (ravager + warden + ender dragon growl + explosion), and renders a 32-point ring of GUST and red dust particles plus a center burst of explosion + flame + crit. Forces aggro back to you so the squishies survive.

Battle Scarred is the simple one. Passive damage reduction that scales with class level: 10% base + 0.2% per level, capped at 35%. Mutates the EntityDamageEvent directly.

Belly Fat is its weirder cousin. Damage reduction that conditionally fires only if you are holding an axe and only if you are not wearing heavy armor (light armor counts: leather, chainmail, copper). Unarmored gets 50-70% reduction, light armor gets 25-45%, heavy armor gets nothing. This one routes through the DamageModifier system as an additive negative modifier so it composes correctly with everything else.

Runnn is a +10% movement speed passive that ticks every tick to apply or remove an attribute modifier based on whether the player is currently holding an axe and not in heavy armor. Stays in sync even if they swap weapons or armor mid-combat.

Yearn For Battle is a single-proc bonus: the next axe hit gets 30% additive damage, then goes on an 8-second cooldown that only ticks down while the player is actually holding the weapon (so you can't swap to a pickaxe to mine while it cools). Triggers crit + sweep_attack particles and a sharp crit-attack sound on the proc.

Fight Forever is straight class-level scaling: every axe hit adds level × 0.01 additive damage, so the same axe gets meaner every time you level up.

Only Destruction is the dual-axe passive: hold an axe in your offhand and ability damage gets a flat 20% additive bonus. Encourages a specific cosmetic build.

The whole class slots cleanly into the broader plugin because every ability routes through the same damage(player, target, amount, this) helper, the same DamageModifier pipeline, and the same EntityDamageByWeaponEvent and EntityDamageByAbilityEvent events Aaqil set up. Cross-class interactions (a Reaper buffing a Barbarian, for example) just work as a side effect.

Reaper (Tier 1 and Tier 2)

I also designed and implemented the Reaper across both tiers. This is the class I am most proud of, and the one with the deepest implementation.

The fantasy here is the opposite of Barbarian. Reaper is a glassy, mobile, high-skill assassin with a scythe. The kit is built around dashing in, getting hits off, and getting back out, with bleed as the resource economy and Flow as the rhythm. Tier 2 adds the Archangel Form transformation and the higher commitment abilities like Harakiri, Last Breath, and Apex Predator. There are 24 abilities in the Reaper tree, and they all interact with each other.

The class identity rests on three intertwined systems I wrote: Bleed, Flow, and the Scythe display.

Bleed and Coated Blade

Bleed is the Reaper's resource. Every scythe hit applies a stack via Coated Blade, max 5 stacks per target. Stacks decay after 5 seconds, and the per-second tick deals damage proportional to current stack count, scaled by the applier's class level. The applier identity is stored in the target's PersistentDataContainer as two longs (most significant + least significant bits of the player UUID) so it survives chunk unloads, server reloads, anything. When the bleed ticks, it resolves the applier UUID, attributes the damage back to them so XP and class progression credit go to the right person.

The bleed DoT itself was a fun bug to chase. Periodic damage from a BukkitRunnable cannot use the same damage() helper that gates weapon-charge progression, because it floods the source-credit system. I learned that the hard way and now any DoT in the codebase uses sourceless target.damage(amount) to avoid breaking the weapon-charge gate.

Eight other Reaper abilities consume bleed stacks for bonus damage. The whole rest of the kit is built on top of this resource.

Flow

Flow is the Reaper's rhythm gauge. Every Reaper ability cast and every successful ability hit adds a stack, capped at 10. Plain scythe weapon hits do not build Flow (they build bleed via Coated Blade), which is the design lever that makes Flow specifically a "use your kit" meter rather than a "swing your weapon" meter. Each stack adds 3% movement speed (transient AttributeModifier on Attribute.MOVEMENT_SPEED) and 3% dodge chance, where the dodge cancels the EntityDamageEvent at EventPriority.LOWEST before any other listener runs. Hit 10 stacks and you trigger Flow State for 3 seconds: total damage immunity (every damage event cancels itself with a glow + end_rod particle flash), then a 10-second cooldown before you can re-trigger it. Stacks decay 1 per second after 6 seconds of no activity. The action bar updates with a colored counter (gray at low, dark red, red, gold at max) so you can feel the meter filling as you cast.

Flow II extends State duration from 3 to 5 seconds and layers extra buffs on top. Healing Suzu consumes Flow stacks for AoE healing: 1 HP per stack, 2 HP per stack while in Flow State, 8-block radius, only heals players + animals + villagers, never hostiles.

The Scythe display

The scythes themselves got the deepest treatment. I custom modelled multiple scythe variants (the ScytheType system) so every tier of scythe has its own geometry, and routed them through BetterModel via the setItemModel API on a netherite hoe base item. They are not retextured hoes.

The signature flourish is Reap. It is the Tier 1 capstone, a 12-tick channel that spawns a giant scythe ItemDisplay at the player's right-back, anchored at the bud of the blade. The scythe rises out of the ground via the display's Transformation translation interpolating from -1.7 to 0 over CHARGE_TICKS - 4 ticks, with setBrightness(15, 15) set so it does not go dark while underground. After the wind-up, the display sweeps through a 130-degree arc in front of the player by SLERPing through three poses: spawn (right-back, head up-and-right), midpoint (head up-and-forward, tip pointing right), and end (left-back, tip pointing forward). The midpoint pose is mandatory because Quaternion shortest-path interpolation can pick the wrong hemisphere and route the scythe behind the player instead of through the front, which would look like the swing reversed itself. Forcing the midpoint solves it.

The transform math itself was fun. The local sprite frame has the bud at (-1, -1, 0) and the head at (+1, +1, 0). To rotate the local basis (head-direction, tip-direction) onto arbitrary world directions, the code composes two Quaternionf.rotationTo calls: first map the local head direction onto the world head direction, then re-orthogonalize the tip and map it onto the world tip with a second rotation, then multiply the quaternions. The translation is computed from where the bud lands after rotation, so the scythe's bud always sits exactly where the entity is anchored regardless of the rotation. The whole thing is encapsulated in buildTransform(worldHead, worldTip, budOffset).

The damage payload is a cone hit on every Enemy in front. Bleed stacks on each enemy are individually consumed for bonus damage (3.5 per stack). At full bleed (5 stacks), the base damage gets multiplied 1.5x, and triggers the "max bleed kick": every other Reaper ability cooldown gets sliced by 10% per max-bleed target consumed (compounding multiplicatively, so 3 max-bleed kills = 27% reduction across the board), the Reaper heals 1 heart per max-bleed target, and the title shows "Reap." in dark red bold. Reap's own cooldown is intentionally excluded from the slice because letting it cooldown-reduce itself would degenerate into Reap-spam.

Cross Reap is the Tier 2 capstone that just inherits Reap and overrides four protected getters: range up from 4.5 to 5.5, cone angle up from 130 to 359 degrees (full surround), base damage up from 10 to 28, and a half-mana refund on max-bleed kills. The mirror flag triggers a second scythe ItemDisplay on the left-back and an inverted SLERP path so both scythes sweep through the front and back in tandem. Inheritance over copy-paste, and adding the second scythe was 30 lines.

Mobility and assassination

Shadowstep is the assassination opener. Lenient ray-trace lock with a 0.7-block radius (so you can land it without pixel-perfect aim) up to 20 blocks. On lock: teleport behind the target with a smoke + portal trail drawn between origin and destination, stun the target for 30 ticks, and empower the next scythe hit. The empowered hit gets a 50% additive crit, plus 2.5 flat per bleed stack on the target, plus stamps a "shred" PDC marker on the target so all subsequent damage from the Reaper for the next 4 seconds gets a 20% additive bonus. The same hit also stamps a "shadowstep striker" marker recording the player UUID, so the Tier 2 ability Into Shadow can credit kill-related procs back to the right caster from a death listener.

Dash Slash is the dash-attack. Steps 0.2 blocks at a time along the player's eye direction up to 10 blocks, falling back to flat-ground stepping if the 3D path gets blocked, hitting every Enemy within 2.2 blocks of the path. Damage scales with bleed stacks. The first bleeding target hit triggers a cooldown reset (yellow burst + amethyst chime + warden impact sound), so chaining Dash Slash through bleeding mobs is the bottom-branch duelist's bread and butter.

Blade Dance is the lock-in burst. Lenient ray-trace lock, then a stun-pre-pause (10 ticks) so the target stops moving, then teleport-strike-teleport-strike-teleport-strike from behind, left, and right of the target with portal + reverse_portal + smoke particles at every reposition. Each strike consumes bleed for bonus damage on the locked target and applies Coated Blade stacks to a 2.5-block sweep AoE around the player at the strike location. After three strikes the player teleports back to where they cast it, with setYaw/setPitch calculated to keep them facing the target through the whole sequence.

Shadowstep's teleport, Dash Slash's dash, and Blade Dance's strike-flurry all give Flow stacks on cast and per hit. The economy compounds.

Survival and bleed payoffs

Soul Harvest caps healing-from-kills at 50% max HP, deliberately. The cap exists because Last Breath scales damage up the lower your HP gets, so the Reaper wants to live in the sub-50% window. Soul Harvest keeps you out of true danger without lifting you back into safe territory.

Hemolysis is the bleed-detonator. AoE 6-block burst that reads every bleeding enemy's remaining bleed seconds, computes the full DoT damage they would have taken over the rest of the bleed window using the same per-stack and applier-level formulas as the ticker, and lands all of it instantly. Higher remaining duration = bigger payout, so the optimization is to detonate early in the bleed window. Then the stacks clear.

Harakiri is the sacrifice ability. Costs 8 self-damage (clamped above 1 HP so it cannot suicide), then pulls every nearby Enemy into a forward gather point via a two-stage motion: knock-up on the Y axis first to lift them off the ground (so friction does not eat the pull), then 5 ticks later a horizontal pull applied to their velocity vector. While airborne they get a 20-tick stun so they cannot retaliate against the now-1HP Reaper. Each hit applies 4 Coated Blade stacks (one off max bleed, so the swarm immediately becomes ripe for a Reap or Hemolysis follow-up) and heals the Reaper 2 HP, so you can recover the 8 self-damage if you pull at least 4 mobs. Three concentric blood-red dust rings spawn at the radius for visual punch.

The Tier 2 transformations

Archangel Form is the Tier 2 transformation. It inherits from a base class (Deliver) and overrides scythe range, blade damage, pierce radius, self-cost, and self-heal. The visual identity shifts hard: the dust palette alternates white and black per frame, the player gets setGlowing(true), and there is a per-tick BukkitRunnable that renders elytra-style wings entirely out of dust particles. Each wing is sampled along a parameterized curve where lateral offset grows quadratically and vertical offset grows linearly (so the tip flares wide and lifts above the shoulders), with white dust on one wing and black on the other. A breathing animation modulates the wing span by a small sin(tick * 0.18) factor so the wings flex in and out like they are alive. End-rod particles glint at the wingtips every 6 ticks for celestial flavor. Stays anchored to the player's yaw.

Apex Predator, Assassins Initiative, The First Form, Into Shadow, Kenshibu, Soul Guide, Flicker, Coated Blade II, Featherstep, Armor Of Needles, Deliver, Last Breath, Soul Harvest make up the rest of the kit. Each one slots into one of the three Tier 2 paths: The Living (Flow path), The Inbetween (balance / Archangel path), or The Dead (bleed amplification path). Build identity actually matters, because the paths reward different scythe rotations.

What makes the Reaper work

What ties it all together is that every Reaper ability builds Flow, every weapon hit builds bleed, and most of the kit either applies bleed or consumes it for bonus damage. The cooldowns are tuned so the rotation never has dead air. Cast Shadowstep, get the empowered hit, follow with Dash Slash through a bleeding target to reset its cooldown, Reap to consume the stacks at full bleed, hit Hemolysis on whatever survived, Blade Dance into the next priority target, Flow State pops at 10 stacks and you are invincible for 3 seconds while you set up the next opener. It feels like a fighting-game combo because the kit was designed to interlock that way.

Custom weapon enchantment system

The third thing I built is the custom weapon enchantment system. The problem: vanilla Minecraft is restrictive about which enchantments go on which weapon types, and Parama has weapons that are not vanilla weapons. The Reaper's scythe is implemented on top of a netherite hoe. The Barbarian uses axes, but vanilla refuses to put Looting, Knockback, or Fire Aspect on an axe at the anvil. The Mage and Techies weapons (staff, lantern, rocket launcher) are implemented on top of sticks, lanterns, and leather horse armor respectively, items that have no business showing up at an enchanting table at all under vanilla rules.

The whole point of the system was that it should feel exactly like vanilla. You walk up to an anvil or an enchanting table, you put your Parama weapon in, the offers and costs appear, you pay and you get your enchanted weapon. No new GUI, no plugin-specific commands, no surprising behavior. Three weapon archetypes (SWORD, AXE, BOW) define what enchants are legal on each, and the system routes the right pool to the right weapon.

The anvil side took me about 15 minutes. Intercept PrepareAnvilEvent, check the base item, merge the book's stored enchants in with vanilla's combination logic (same level plus one if both match, otherwise take the higher), filter by the weapon's allowed pool, conflict-detect (Sharpness + Smite cannot coexist), preserve rename text, set the XP cost. Same listener also handles weapon-on-weapon combine: merging enchants from a same-type donor weapon, plus the vanilla repair formula (donor's remaining durability + 12% of max durability, clamped). Easy. The hard part was everything else.

The enchanting table was the actually hard part, because Mojang's enchanting pipeline is a closed loop and there is no public API for half of what it does. Vanilla generates three offer slots based on the item's "enchantability" and the player's per-account enchantment seed, then on click it computes which actual enchants from a hidden weighted pool match the rolled cost level. None of this is exposed. The plugin needs to take over both halves of the loop and produce results the client UI shows correctly.

What I had to build:

  1. Backfill the minecraft:enchantable data component on inventory open. Without it, custom items refuse to even produce table offers. New crafts get it set at construction time, but every Parama weapon that existed before the table support was wired up is silently broken. So an InventoryOpenEvent listener walks the player's inventory whenever they open an enchanting table and patches any Parama weapon that is missing the component.

  2. Replace the offer slots. When a Parama weapon is the input, Paper fires PrepareItemEnchantEvent with vanilla's offer suggestions. For our weapons, those offers can be entirely null because the base material (stick, lantern, leather horse armor) is not in any vanilla enchant's primary_items registry tag, so vanilla's offer-generator decides "no compatible enchants" and bails. But it still computed the slot cost levels and sent them to the client. So my replacement reads the cost levels, rolls our own enchants against them, and writes the result back.

  3. Read the slot costs from a private field via reflection. This was the moment I lost the most time. EnchantmentOffer.getCost() is the public way to read the level numbers shown on the table UI, but those offers can be null. The cost numbers themselves live on the menu's costs[] array, which has no public API. I had to walk event.getView().getHandle() and grab EnchantmentMenu.costs via reflection, with a try/catch fallback in case Paper or Mojang shifts the field name in a future mappings bump.

  4. Reimplement Mojang's EnchantmentHelper.selectEnchantment from scratch. When the player clicks an offer, I need to produce the same set of enchants vanilla would have produced for that cost level, drawn from my custom pool. That meant porting the entire vanilla algorithm: derive a "modified level" from cost + 1 + rand(ench/4 + 1) + rand(ench/4 + 1), fudge it by ±15%, filter the pool by each enchant's getMinModifiedCost(level) and getMaxModifiedCost(level) window, weighted-pick a primary by enchant weight, then run a bonus loop that keeps adding enchants while rand.nextInt(50) <= modifiedLevel and halves modifiedLevel each round. There is a subtle off-by-one to get right: vanilla picks at the current modifiedLevel then halves, not the other way around. I had it the other way around for a while, which biased high-cost rolls toward mid-tier enchants and made the table feel oddly stingy.

  5. Make the rolls deterministic across both events. PrepareItemEnchantEvent (which sets the offer card) and EnchantItemEvent (which applies the click) fire separately. They have to roll the same enchants for the same offer or the displayed level number will not match what gets applied. Solution: a deterministic seed compounded from the player's enchantment seed, their UUID, the item hash, the slot index, and the cost level. Both events feed the same inputs into the same Random, get the same output. Vanilla mitigates this with a per-player enchantment seed that scrambles after each apply, so I scramble it explicitly after EnchantItemEvent (seed ^= slot * 0x9E3779B1 ^ nanoTime()) so the next session re-rolls instead of showing the same offers forever.

  6. Defer the lore refresh by one tick. The scythe shows a damage number in its lore that includes Sharpness bonuses. After EnchantItemEvent returns, vanilla applies the enchant to the item. So if I rebuild the lore inside the handler, I read pre-apply state and the lore is wrong. Bukkit.getScheduler().runTask(plugin, ...) defers the lore rebuild one tick so the enchants are present when I read them.

  7. Handle the grindstone too. Disenchanting a Parama weapon at a grindstone strips its enchants but leaves the lore showing the old damage number. A PrepareGrindstoneEvent listener rebuilds the lore on the result item the same way the enchant path does.

The whole thing is wrapped in a WeaponParama base class with two extension hooks (getCompatibleEnchants() and isTableEnchantable()) so each weapon class declares its own enchant pool and whether it shows up at the table. Adding a new weapon archetype is one method override. The hard work was just behind the curtain.

The result is what I wanted: you put a Parama weapon in an anvil or an enchanting table, and it works exactly like a vanilla weapon would. No menus, no commands, no surprises.

What this project actually is

The original Parama Legends taught me that scope is the enemy of finishing things, and that working with friends on something nobody is paying you for has to be its own reward. Parama Legends 2 is the version of that with five more years of engineering experience baked in. Cleaner architecture, real abstractions, data-driven content, custom models, and a feature list that we are actually going to finish this time.

It is still just a thing me and my friends play on a private server. It is not going on a marketplace. The audience is the same group of friends from uni, just slightly older and slightly better at Java. That is what makes it good. Nobody is optimizing for users we do not have. We are building the game we want to play together, and that is the most fun software engineering gets.