Ruins Classes

Split the Ruins gamemode into Slayer, Archer, and Mage. Each with its own inventory, zone progress, and unlock path.

groovyminecraft
On this page 6 sections
  1. Picking a class
  2. The three classes
  3. Unlocking and switching
  4. What classes changed
  5. Architecture
  6. Trade-offs

Ruins used to be one shared playstyle. Everyone swung a sword, everyone leveled the same numbers, cosmetics were the only thing that changed. Classes broke that open into three real playstyles: Slayer (melee), Archer (ranged bow), and Mage (mid-range staff that cleaves everything in a line). Each one gets its own inventory, its own zone progress, and its own unlock gate.

You pick a class on first join. From then on, each class remembers what you had on when you left it: your gear, your held slot, the highest zone you’d reached. Switching back is stepping back into that class’s state, not resetting.

Picking a class

  1. First join opens the class picker. Nothing class-specific is handed to you until you choose. Otherwise any starter loot would be built against a fallback class and burn on the switch.
  2. Pick a class. Your inventory, zone, and progress are seeded for that class.
  3. Play. Each class levels its own tokens, holds its own gear, and clears zones at its own pace.
  4. Unlock another class. Right-click a Class Unlock Token to open the unlock menu and permanently unlock a second (or third) class.
  5. Switch with /class. Your current inventory parks on the class you’re leaving, and the class you’re joining hands you back exactly what it had on last time.

Switching is refused while you’re in combat, and attacks are suppressed for the split second the swap actually runs, so an in-flight sword swing can’t leak into the new class’s inventory.

The three classes

ClassWeaponFeel
SlayerIron SwordClose-range lock-on. Carve through one mob at a time.
ArcherBowRanged, safer, but every shot has to be aimed.
MageStaff (stick)Mid-range channel that hits everything in a line.

Each class gets its own icon and colour in the picker, its own zone HUD, and its own leaderboard once you’re past the first zone.

Unlocking and switching

  • Your first class is free. The picker is a barrier menu until you commit. You can’t close it.
  • Extra classes cost a Class Unlock Token. Tokens come from Battle Pass, milestones, and events; the redeem opens a menu so you never pay for a class you already own.
  • Switching keeps everything. Both classes’ inventories, hotbars, and zone progress are preserved. Nothing gets deleted when you leave a class.
  • You can’t switch mid-fight. Combat lockout blocks the swap until combat drops.
  • Class switching itself is behind its own flag. Initial selection is always allowed; free switching is a separate, admin-toggleable slice so it can be turned off for events without locking players out of the game.

What classes changed

  • Replay in a game that used to be one loop. Same account, same progress, three different playstyles.
  • A real reason to keep multiple weapon families. Bow, sword, staff each level in their own economy.
  • Cheap live-ops content. A new class is a RuinsClass enum entry plus its abilities. The switching, storage, and unlock plumbing already exists.
  • A monetisation-safe unlock. The token is a physical item that goes through the standard reward pipelines. No “buy a class” store button because there doesn’t need to be one.

Architecture

Per-class profile data

Each PlayerProfile holds a ClassProfileData per RuinsClass. Its own serialized inventory, its own unlocked flag, its own maxUnlockedZone. getActiveClass(player) falls back to RuinsClass.DEFAULT while the feature is off or before the player has chosen, so callers can never null-check into a bug. needsClassSelection is what actually blocks class-specific handouts until a real pick lands.

The active class always counts as unlocked. That’s a rule about profiles from before classes could lock: a player who was legitimately playing a class before the lock gate went live should never lose the class they were already on.

One-shot, single-thread swap

switchClass funnels the whole swap through ThreadHelper.ensureAsyncWorldThread so no other tick can observe a player mid-swap. The inventory belonging to the old class and the profile data belonging to the new one is the exact desync that would corrupt a save. A switching set gates re-entry so a double-clicked class button can’t fire two swaps that race each other, and the same set tells the periodic profile saver to leave the profile alone while a swap is in flight.

The apply step is the strict order that matters:

  1. Stop any current attack (combatController.stopAttacking).
  2. Serialize the current inventory into the class we’re leaving.
  3. Mark the leaving class unlocked. Playing it counts as owning it.
  4. Set the active class to the target.
  5. Restore the target class’s inventory.
  6. Invalidate the player-stats cache and teleport them to their maxUnlockedZone in the new class.
  7. Fire PlayerClassChangedEvent so listeners (HUD, tab list, achievements) rebuild.

Any exception inside the try clears the switching flag in finally, so a bad swap doesn’t permanently lock the player out of switching. combatController.setAscending(player) covers the small window with the same attack-suppression mechanism ascension uses.

Locked classes are never in the locked list at edit-time

isUnlocked(player, class) treats the active class as unlocked by construction. You can’t be playing a class that isn’t unlocked, because the switch to it unlocked it. That means the “locked classes” list callers see is always the ones you legitimately don’t own, without special cases for the class you’re currently on.

Unlock token: per-item id, right-click to redeem

ClassUnlockTokenItem is an item with a per-instance UUID and a right-click handler that opens the unlock menu. The menu refuses to spend a token on a class you already own; unlock() returns false when the class was already unlocked, so the item is never consumed for a no-op. Tokens ride the standard item pipelines (crate rewards, Battle Pass, milestones, admin /give) with no class-specific plumbing.

Feature flags gate the pieces, not the whole feature

  • CLASSES. The master flag. When off, getActiveClass returns the default and nothing class-specific fires.
  • CLASS_SWITCHING. Free switching. Off means only the initial pick is allowed; extra classes still unlock via tokens but the swap is gated.
  • CLASS_LOCKING. The lock gate itself. Off means any class is available to any player, useful for events or a rollback if the lock is misbehaving.

Splitting them means a broken switching path doesn’t take character selection down with it, and locking can be disabled without breaking a live progression grant.

Trade-offs

  • Class inventories are stored, not aliased. Two classes have two full serialized inventories. Storage cost is real but bounded (three inventories per player, max), and it beats every alternative for correctness. A shared bank that filtered by class type would break every existing item-lifecycle assumption in the plugin.
  • Progress is per-class, not per-account. An account that maxed Slayer starts Archer over. That’s the whole point of having three classes; a shared account level would collapse them into “a costume for a sword.” Shared cosmetics are separate and stay shared.
  • First pick is a barrier menu, no close button. You can log off and come back, but you can’t skip the pick and walk around as the fallback class. Skipping means nothing class-specific ever fires cleanly, so the simpler rule wins.
  • Combat lock stops the swap outright. No mid-fight class switching. Allowing it would mean every combat system has to handle the class-of-record changing mid-hit, and nothing that follows is worth the extra complexity.