Skip to main content

Overview

The case opening system provides an engaging slot-machine style animation that displays items scrolling across the screen before landing on the winning item. The system integrates with the provably fair API to ensure transparent and verifiable randomness.

Architecture

1

User Initiates Spin

Player clicks the “Abrir Caja” button, triggering the startSpin() function
2

Server Determines Winner

Backend uses provably fair algorithm to calculate the winning item using cryptographic seeds
3

Build Animation Reel

Frontend constructs a reel of 110 items with the winner placed at index 75
4

Animate to Winner

CSS transition scrolls the reel over 8 seconds with easing function to land on winner
5

Display Winner Modal

Modal appears showing the won item with rarity glow effects and sell option

Component Structure

The case opener is built using the CaseOpener component located at:
// ~/workspace/source/components/case-opener.tsx

interface CaseOpenerProps {
  items: Item[]        // All possible items in this case
  casePrice: number    // Cost to open the case
  caseName: string     // Display name
  caseId: string       // Database ID for API call
}

Key Configuration Constants

components/case-opener.tsx
const CARD_WIDTH = 180           // Width of each item card in pixels
const GAP = 2                    // Gap between cards
const TOTAL_ITEM_WIDTH = 182     // Card width + gap
const SPIN_DURATION = 8000       // Animation duration (8 seconds)
const WINNER_INDEX = 75          // Position where winner is placed in reel

Animation Mechanics

Reel Construction

When a spin starts, the component builds a reel array:
components/case-opener.tsx
const startSpin = async () => {
  // 1. Request Winner from Server
  const response = await fetch('/api/cases/open', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ caseId: caseId })
  })
  
  const data = await response.json()
  const wonItem = data.winner
  
  // 2. Build the Reel around the Winner
  const filler = shuffle([...items])
  const newReel: Item[] = []
  
  for (let i = 0; i < 110; i++) {
    if (i === WINNER_INDEX) {
      newReel.push(wonItem)  // Winner at position 75
    } else {
      newReel.push(filler[i % filler.length])
    }
  }
  setReel(newReel)
}
The winner is always placed at index 75 in a 110-item reel. The animation then scrolls to center this item in the viewport.

Transform Calculation

The animation uses CSS transforms to scroll the reel:
components/case-opener.tsx
// Calculate End position to center the winner
const windowWidth = windowRef.current.clientWidth
const winningCardCenter = (WINNER_INDEX * TOTAL_ITEM_WIDTH) + (CARD_WIDTH / 2)
const finalTranslate = (windowWidth / 2) - winningCardCenter

// Apply smooth easing animation
reelContainerRef.current.style.transition = 
  `transform ${SPIN_DURATION}ms cubic-bezier(0.15, 0, 0.10, 1)`
reelContainerRef.current.style.transform = 
  `translateX(${finalTranslate}px)`
The cubic-bezier easing (0.15, 0, 0.10, 1) creates a realistic “slow down” effect, mimicking physical slot machines.

Sound Effects

The system includes two audio cues:
  1. Tick Sound: Plays each time the reel crosses to a new item
  2. Finish Sound: Plays when winner modal appears
components/case-opener.tsx
// Track position during animation
const checkPosition = () => {
  const currentIndex = Math.round(rawIndex)
  
  if (currentIndex !== lastIndex) {
    if (!isMuted) {
      const soundClone = audio.cloneNode() as HTMLAudioElement
      soundClone.volume = 0.2
      soundClone.play().catch(() => { })
    }
    lastIndex = currentIndex
  }
  
  requestAnimationFrame(checkPosition)
}

Rarity System

Items have color-coded rarity indicators:
components/case-opener.tsx
const getRarityColor = (rarity: string) => {
  switch (rarity.toLowerCase()) {
    case 'legendary': return 'border-b-yellow-500'
    case 'epic':      return 'border-b-purple-500'
    case 'rare':      return 'border-b-blue-500'
    default:          return 'border-b-white/20'
  }
}

Legendary

Yellow border and glow - Highest value items

Epic

Purple border and glow - High value items

Rare

Blue border and glow - Medium value items

Common

White/gray border - Standard items

Winner Modal

After the animation completes, a modal displays:
components/case-opener.tsx
setTimeout(() => {
  setIsSpinning(false)
  setShowWinnerModal(true)
}, SPIN_DURATION + 500)
The modal includes:
  • Rarity-based background glow
  • Item image with drop shadow
  • Item name and value
  • “Sell” button (currently displays sell price)

API Integration

The component communicates with the backend API:
POST /api/cases/open
{
  "caseId": "uuid-string"
}

Response:
{
  "winner": {
    "id": "item-uuid",
    "name": "Item Name",
    "image_url": "https://...",
    "rarity": "legendary",
    "price": 5000,
    "probability": 2.5
  },
  "fairness": {
    "server_seed_hash": "sha256-hash",
    "client_seed": "client-seed",
    "nonce": 42,
    "roll_value": 0.98234
  }
}
The API returns a 401 Unauthorized if the user is not logged in, triggering the auth modal.

Performance Optimizations

The reel container uses willChange: 'transform' to hint the browser to optimize for transform animations.
Sound effects are triggered using requestAnimationFrame to sync with browser paint cycles, avoiding jank.
Each tick sound is cloned from the base Audio object to allow rapid successive plays without interruption.

User Experience Features

Mute Toggle

Players can toggle sound effects:
components/case-opener.tsx
const [isMuted, setIsMuted] = useState(false)

<button onClick={() => setIsMuted(!isMuted)}>
  {isMuted ? <Volume2 className="text-red-500/50" /> : <Volume2 />}
</button>

Visual Indicators

  • Top/Bottom Arrows: Mark the center line where the winner will land
  • Center Line: Vertical line shows exact selection point
  • Side Fades: Gradient overlays create depth and focus attention on center
components/case-opener.tsx
{/* Center Line Verification */}
<div className="absolute top-0 bottom-0 left-1/2 w-[2px] 
              bg-primary/30 -ml-px z-20 pointer-events-none" />

{/* Side Fades */}
<div className="absolute inset-y-0 left-0 w-40 
              bg-gradient-to-r from-[#050505] to-transparent 
              z-20 pointer-events-none" />

Next Steps

Provably Fair

Learn how cryptographic verification ensures fair outcomes

Wallet System

Understand balance management and transactions

Build docs developers (and LLMs) love