Skin TradeUps

Combine duplicate skins into a roll for one a rarity bracket higher.

groovyminecraft
On this page 6 sections
  1. Filling a contract
  2. The ladder
  3. Before you commit
  4. Who it’s for
  5. Architecture
  6. Trade-offs

Trade Ups let you combine duplicate skins you’ll never use into a roll for a skin one rarity bracket higher. Instead of waiting on crate RNG hoping for a new skin, you spend the inventory you already have… your hoard becomes the fuel.

Filling a contract

  1. Open the trade-up menu and pick a tier (e.g. Common → Rare).
  2. Fill the contract by clicking eligible skins on the right, they slot into the contract panel on the left.
  3. Click PROCEED when the contract is full. The system rolls success or failure, consumes your inputs, and if you succeed… it awards a single random skin one rarity higher!

You can only slot skins of the contract’s input rarity, and each duplicate you own can only be used once per contract.

The menu shows your balance, what’s in the contract, and how many copies you have left available before you commit.

The ladder

Trade UpInputs RequiredSuccess Chance
Common → Rare1590%
Rare → Epic1075%
Epic → Legendary1060%
Legendary → Mythical1045%
Mythical → Godly510%

Success rates shown are placeholder values and may have changed since writing this.

The output skin is rolled from the registered pool of the target rarity.

Before you commit

  • Inputs are consumed on every roll… success or failure. Failed trade-ups don’t refund anything.
  • The output is random within its rarity. You’re rolling for a skin of the next bracket, not a specific one.
  • Higher tiers carry lower odds. The early climb is forgiving (90%), the final Mythical → Godly jump is intentionally brutal (10%).
  • No coins or keys required. Trade Ups are pure skin-for-skin exchanges.
  • Skins of both omnitool and armor types share the same trade-up pools at each rarity.

Who it’s for

  • Clearing duplicate clutter. If you’ve opened enough Skin Crates, you have 100s of Bronze tools and basic armor pieces you’ll never equip. Trade Ups give those a purpose.
  • Targeted progression. Skin Crates give you whatever the RNG decides. Trade Ups let you aim… if you want a Rare skin, you grind Commons.
  • Long-term path to top-rarity cosmetics. Godly skins almost never drop from crates. Trade Ups give you a deterministic (if expensive) road to them: stockpile Mythical’s, gamble at 10%, and a Godly drops directly into your inventory.
  • Inventory pressure relief. Trading up keeps your skin balances trim by collapsing many low-rarity duplicates into fewer high-rarity ones.

Architecture

Pluggable skin sources

The trade-up logic doesn’t know what skins are. Each cosmetic system implements TradeUpSource:

interface TradeUpSource {
    String getKey()

    SkinCrateOption resolve(String skinId)

    String getRarity(String skinId)

    List<String> listSkinIdsOfRarity(String rarityName)

    CompletableFuture<Map<String, Integer>> getOwnedSkins(UUID player)

    CompletableFuture<Boolean> consumeOne(UUID player, String skinId)

    CompletableFuture<Void> award(UUID player, String skinId)
}

There are two sources today (atlantic.omnitool is for omnitool skins, atlantic.itemskins is for armor/tool skins). They share the same trade-up ladder but back onto completely different MySQL tables. This as well allows extendability for other gamemodes using this system, and or building off of it more.

Session-based contracts

A trade-up contract is an in-memory session keyed by player UUID, not an inventory item or a placed entity:

  • No item handling. Players never drag stacks around; the system reads the users balances and writes balances directly. This eliminates an entire class of dupe and desync bugs that would come with intermediate item state.
  • Resumable. Closing and re-opening the menu for the same tier picks up the existing contract. Since we all know that one time we accidentally closed a menu after spending quite a bit of time in it…
  • Cancelable. Walking away releases the session, nothing was ever committed.

Atomic single-flight execution

When PROCEED fires, we acquire an exclusive lock for the player via TradeUp.tryStartExecuting(uuid). A second click during execution is silently dropped (with a feedback sound). Execution then does:

  1. Consume each selected skin sequentially against its source.
  2. If any consumption fails, refund every skin already consumed and abort. (This happens when the player’s balance changed between contract-open and proceed, most commonly because they’re on a separate server that already deducted the skin.)
  3. Roll success/failure.
  4. On failure: publish the result, send the loss message.
  5. On success: roll an output skin. If no source has any skin of the target rarity, refund everything and tell the player to contact staff. This is the only graceful-degradation path.
  6. Award the output skin! Publish.

Refund-on-partial-failure is the load-bearing detail. Without it, a cross-server race during the consume phase would silently eat part of the player’s inputs with no output. Refunds keep the player whole when the system can’t deliver, at the cost of slightly more complex bookkeeping.

Output roll: two-stage, equal-weighted

rollOutput(toRarity) is intentionally not a flat-weight roll across every registered skin of that rarity. It’s two stages:

  1. Pick a source uniformly at random from sources that have ≥1 skin of the target rarity.
  2. Pick a skin uniformly at random from that source’s list.

The consequence: an omnitool Legendary and an itemskins Legendary are equally likely as categories, not weighted by how many skins exist in each category. We did this is deliberately, it keeps the source distribution stable even as we add or remove skins from each catalog. If we’d flat-weighted by total count, adding 20 new itemskins Legendaries would have quietly cut every omnitool Legendary’s drop rate roughly in half. The two-stage roll insulates each catalog from the other’s growth.

Result publication

Every consumed trade-up (success or failure, refund paths skip this) publishes a JSON line to Redis channel tradeup:executed:

{
  "type": "tradeup",
  "username": "...",
  "uuid": "...",
  "server": "atlantic34",
  "fromRarity": "MYTHICAL",
  "toRarity": "GODLY",
  "successChance": 0.10,
  "success": true,
  "submitted": [
    ...
  ],
  "awarded": {
    "sourceKey": "atlantic.omnitool",
    "skinId": "..."
  }
}

Consumers can use this for global broadcasts, leaderboards, analytics, etc. The trade-up flow is decoupled from any subscriber, publish failures are best-effort and never block the player’s roll. Using this is good for stuff like a discord bot monitoring the channel and publishing an internal or public message for when someone wins them.

Trade-offs

  • No coin cost. We had a skin recycler in the past and it got removed with the implementation of this system due to the sheer amount of skins being recycled compared to the coins rewarded. We wanted the Trade Ups to reward skin hoarders, not coin hoarders.
  • Equal-weight output within a rarity. Players will occasionally trade up to a Legendary they didn’t want. Acceptable cost. Per-skin output weights would have made every roll politically charged (“why is X favored over Y?”) for marginal gameplay payoff, and would have needed a separate weight table to maintain alongside every skin catalog.
  • Inputs consumed on failure. The alternative, partial refunds… these would have flattened the risk curve and made the top-tier 10% roll meaningless. The Godly gamble is only meaningful because failure is total.
  • Two-stage source roll instead of flat-weight. Discussed above. We took the source-stability benefit over the “all skins equally likely” intuition.
Skin TradeUp demo video