Skip to Content
DocsVXShopGacha System

Loot Box (10-Pull Gacha)

A gacha system where users buy a 10-pull pack and receive random items based on probability.

Step 1: Register Product on VX Shop

Before implementing, register the product on Verse8 VX Shop dashboard:

Product Settings:

  • Product ID: gacha-10pull
  • Product Name: Hero Gacha 10-Pull (or “10x Hero Summon”)
  • Price: 500 VX (adjust as needed)
  • Purchase Constraints:
    • ❌ Lifetime Limit: None (unlimited purchases)
    • ❌ Period Limit: None
    • ❌ Time-Limited Sale: None (unless it’s an event)

Users can purchase this repeatedly to collect more heroes.

👉 Learn how to register products

Step 2: Client Implementation

import { useVXShop } from '@verse8/platform'; import { useGlobalMyState, remoteFunction } from '@agent8/gameserver'; import { useEffect, useState } from 'react'; function GachaShop() { const { getItem, buyItem, refresh, onClose } = useVXShop(); const myState = useGlobalMyState(); const [pullResult, setPullResult] = useState(null); useEffect(() => { const cancel = onClose(async (payload) => { refresh(); // If purchase was successful, open the loot box if (payload.purchased && payload.productId === 'gacha-10pull') { // Call server to open loot box const result = await remoteFunction('openGachaBox', {}); setPullResult(result); } }); return cancel; }, [onClose, refresh]); const gachaItem = getItem('gacha-10pull'); const tickets = myState?.gachaTickets || 0; // Show pull results if (pullResult) { return ( <div className="gacha-result"> <h3>You Got!</h3> <div className="items-grid"> {pullResult.items.map((item, idx) => ( <div key={idx} className={`item rarity-${item.rarity}`}> <img src={item.image} alt={item.name} /> <p>{item.name}</p> <span className="rarity">{item.rarity}</span> </div> ))} </div> <button onClick={() => setPullResult(null)}>Close</button> </div> ); } return ( <div className="gacha-shop"> <h3>Hero Gacha</h3> <p>Get 10 random heroes!</p> <p>Your tickets: {tickets}</p> {/* Buy more tickets */} {gachaItem?.purchasable && ( <button onClick={() => buyItem('gacha-10pull')}> Buy 10-Pull - {gachaItem.price} VX </button> )} {/* Use existing tickets */} {tickets >= 10 && ( <button className="use-tickets" onClick={async () => { const result = await remoteFunction('openGachaBox', {}); setPullResult(result); }} > Use 10 Tickets </button> )} </div> ); }

Step 3: Server Implementation

// server.js // Item pool with rarities const GACHA_POOL = [ // Common (70%) { id: 'hero_1', name: 'Warrior', rarity: 'common', rate: 0.15 }, { id: 'hero_2', name: 'Archer', rarity: 'common', rate: 0.15 }, { id: 'hero_3', name: 'Mage', rarity: 'common', rate: 0.15 }, { id: 'hero_4', name: 'Healer', rarity: 'common', rate: 0.15 }, { id: 'hero_5', name: 'Knight', rarity: 'common', rate: 0.10 }, // Rare (25%) { id: 'hero_6', name: 'Dark Knight', rarity: 'rare', rate: 0.10 }, { id: 'hero_7', name: 'Assassin', rarity: 'rare', rate: 0.10 }, { id: 'hero_8', name: 'Paladin', rarity: 'rare', rate: 0.05 }, // Epic (5%) { id: 'hero_9', name: 'Dragon Rider', rarity: 'epic', rate: 0.03 }, { id: 'hero_10', name: 'Phoenix Mage', rarity: 'epic', rate: 0.02 }, ]; function rollGacha() { const random = Math.random(); let cumulative = 0; for (const item of GACHA_POOL) { cumulative += item.rate; if (random < cumulative) { return item; } } return GACHA_POOL[0]; // Fallback } async $onItemPurchased({ account, purchaseId, productId, quantity, metadata }) { if (productId === 'gacha-10pull') { // Give user 10 gacha tickets instead of opening immediately const userState = await $global.getUserState(account); const currentTickets = userState.gachaTickets || 0; await $global.updateUserState(account, { gachaTickets: currentTickets + 10 }); } return { success: true }; } async openGachaBox() { const userState = await $global.getMyState(); const tickets = userState.gachaTickets || 0; if (tickets < 10) { throw new Error('Not enough tickets'); } // Roll 10 times const results = []; for (let i = 0; i < 10; i++) { results.push(rollGacha()); } // Update user inventory const inventory = userState.inventory || {}; results.forEach(item => { inventory[item.id] = (inventory[item.id] || 0) + 1; }); // Deduct tickets and update inventory await $global.updateMyState({ gachaTickets: tickets - 10, inventory: inventory }); return { items: results }; }

Step 4: How It Works

  1. User clicks “Buy 10-Pull”
  2. VXShop payment dialog opens
  3. User completes payment
  4. Server receives $onItemPurchased and gives 10 tickets
  5. Client detects purchase completion
  6. Client calls openGachaBox() remote function
  7. Server rolls 10 random items based on rates
  8. Server updates inventory and deducts tickets
  9. Client shows animated pull results

Key Points

  • Ticket system: Purchase gives tickets, not direct items
  • Server-side rolling: Prevents client-side manipulation
  • Probability-based: Weighted random selection
  • Inventory tracking: Automatic item counting

Why Tickets Instead of Direct Opening?

Giving tickets first allows users to save them and pull later, creating better UX. Users can also use tickets without additional purchases.

Probability Distribution

The example uses:

  • Common (70%): Most frequent drops
  • Rare (25%): Moderate frequency
  • Epic (5%): Rare drops

Adjust rates in GACHA_POOL to match your game’s economy.

Legal Compliance

Some regions require disclosing gacha rates. Always display probability information to users and comply with local regulations.

Last updated on