Skip to content

Upgrades - Melhorar Itens

O sistema de Upgrade permite ao jogador tentar converter um item de menor valor em um item de maior valor, baseado em probabilidade calculada.

Como Funciona

Cálculo de Probabilidade

Fórmula Base

typescript
// upgrade.service.ts
calculateUpgradeProbability(
  fromValueCents: bigint,
  toValueCents: bigint,
  houseEdge: number = 0.10, // 10% default
): number {
  // Probabilidade base = valor origem / valor destino
  const baseProbability = Number(fromValueCents) / Number(toValueCents);
  
  // Aplicar house edge
  const adjustedProbability = baseProbability * (1 - houseEdge);
  
  // Limitar entre 1% e 90%
  return Math.max(0.01, Math.min(0.90, adjustedProbability));
}

Exemplos

Item OriginalItem DestinoProbabilidade
R$ 10R$ 5018%
R$ 10R$ 1009%
R$ 50R$ 10045%
R$ 50R$ 20022.5%
R$ 100R$ 50018%

Modelo de Dados

prisma
model Upgrade {
  id                BigInt        @id @default(autoincrement())
  userId            BigInt
  
  fromItemId        BigInt
  fromValueCents    BigInt
  toItemId          BigInt
  toValueCents      BigInt
  
  probability       Float
  roll              Int
  success           Boolean
  
  // Provably Fair
  serverSeed        String
  serverSeedHash    String
  clientSeed        String
  nonce             Int
  
  user              User          @relation(...)
  fromItem          Item          @relation("UpgradeFromItem", ...)
  toItem            Item          @relation("UpgradeToItem", ...)
  
  createdAt         DateTime      @default(now())
  
  @@index([userId])
  @@index([success])
}

Implementação

Execute Upgrade Use Case

typescript
// execute-upgrade.use-case.ts
@Injectable()
export class ExecuteUpgradeUseCase {
  constructor(
    private upgradeRepository: UpgradeRepository,
    private inventoryService: InventoryService,
    private itemRepository: ItemRepository,
    private provablyFairService: ProvablyFairService,
    private redlock: Redlock,
  ) {}
  
  async execute(
    userId: bigint,
    fromItemId: bigint,
    toItemId: bigint,
  ): Promise<UpgradeResult> {
    const lockKey = `user:${userId}:upgrade`;
    
    return await this.redlock.using([lockKey], 30000, async () => {
      // 1. Verificar posse do item
      const inventoryItem = await this.inventoryService.findByUserAndItem(
        userId,
        fromItemId,
      );
      
      if (!inventoryItem || inventoryItem.status !== 'AVAILABLE') {
        throw new ItemNotAvailableException();
      }
      
      // 2. Buscar itens
      const fromItem = await this.itemRepository.findById(fromItemId);
      const toItem = await this.itemRepository.findById(toItemId);
      
      if (!toItem || !toItem.isUpgradable) {
        throw new ItemNotUpgradableException();
      }
      
      // 3. Validar valores
      if (fromItem.valueCents >= toItem.valueCents) {
        throw new InvalidUpgradeException(
          'Item de destino deve ter valor maior',
        );
      }
      
      // 4. Calcular probabilidade
      const probability = this.calculateProbability(
        fromItem.valueCents,
        toItem.valueCents,
      );
      
      // 5. Gerar seeds e roll
      const serverSeed = this.provablyFairService.generateServerSeed();
      const serverSeedHash = this.provablyFairService.hashSeed(serverSeed);
      const clientSeed = await this.getOrCreateClientSeed(userId);
      const nonce = await this.incrementNonce(userId);
      
      const roll = this.provablyFairService.calculateRoll(
        serverSeed,
        clientSeed,
        nonce,
      );
      
      // 6. Determinar resultado
      const probabilityThreshold = Math.floor(probability * 100000);
      const success = roll < probabilityThreshold;
      
      return await this.prisma.$transaction(async (tx) => {
        // 7. Remover item original
        await this.inventoryService.removeItem(tx, userId, fromItemId);
        
        // 8. Registrar upgrade
        const upgrade = await tx.upgrade.create({
          data: {
            userId,
            fromItemId,
            fromValueCents: fromItem.valueCents,
            toItemId,
            toValueCents: toItem.valueCents,
            probability,
            roll,
            success,
            serverSeed,
            serverSeedHash,
            clientSeed,
            nonce,
          },
        });
        
        if (success) {
          // 9a. Sucesso - adicionar item novo
          await this.inventoryService.addItem(tx, userId, toItemId);
          
          console.log(`[Upgrade] SUCCESS: ${fromItem.name} -> ${toItem.name}`);
        } else {
          // 9b. Falha - item perdido
          console.log(`[Upgrade] FAIL: ${fromItem.name} -> LOST`);
        }
        
        return {
          upgradeId: upgrade.id,
          success,
          probability,
          roll,
          serverSeedHash,
          fromItem,
          toItem: success ? toItem : null,
        };
      });
    });
  }
  
  private calculateProbability(
    fromValue: bigint,
    toValue: bigint,
  ): number {
    const HOUSE_EDGE = 0.10;
    const baseProbability = Number(fromValue) / Number(toValue);
    const adjusted = baseProbability * (1 - HOUSE_EDGE);
    return Math.max(0.01, Math.min(0.90, adjusted));
  }
}

Frontend

Tela de Upgrade

tsx
const UpgradePage = () => {
  const [fromItem, setFromItem] = useState<Item | null>(null);
  const [toItem, setToItem] = useState<Item | null>(null);
  const [probability, setProbability] = useState(0);
  const [isSpinning, setIsSpinning] = useState(false);
  const [result, setResult] = useState<UpgradeResult | null>(null);
  
  // Calcular probabilidade quando itens mudam
  useEffect(() => {
    if (fromItem && toItem) {
      const prob = calculateProbability(
        fromItem.valueCents,
        toItem.valueCents,
      );
      setProbability(prob);
    }
  }, [fromItem, toItem]);
  
  const handleUpgrade = async () => {
    if (!fromItem || !toItem) return;
    
    setIsSpinning(true);
    
    try {
      const result = await api.post('/upgrades', {
        fromItemId: fromItem.id,
        toItemId: toItem.id,
      });
      
      // Aguardar animação
      await delay(5000);
      
      setResult(result.data);
    } finally {
      setIsSpinning(false);
    }
  };
  
  return (
    <div className="upgrade-page">
      {/* Seletor de item origem */}
      <div className="from-slot">
        <h3>Seu Item</h3>
        <ItemSelector
          items={inventory}
          selected={fromItem}
          onSelect={setFromItem}
        />
      </div>
      
      {/* Indicador de probabilidade */}
      <div className="probability-indicator">
        <CircularProgress value={probability * 100} />
        <span>{(probability * 100).toFixed(1)}%</span>
      </div>
      
      {/* Seletor de item destino */}
      <div className="to-slot">
        <h3>Item Desejado</h3>
        <ItemSelector
          items={upgradeableItems}
          selected={toItem}
          onSelect={setToItem}
          filter={(item) => item.valueCents > (fromItem?.valueCents || 0)}
        />
      </div>
      
      {/* Botão de upgrade */}
      <Button
        onClick={handleUpgrade}
        disabled={!fromItem || !toItem || isSpinning}
      >
        {isSpinning ? 'Upgrading...' : 'UPGRADE'}
      </Button>
      
      {/* Animação de resultado */}
      {isSpinning && (
        <UpgradeAnimation probability={probability} />
      )}
      
      {/* Resultado */}
      {result && (
        <UpgradeResultModal
          result={result}
          onClose={() => setResult(null)}
        />
      )}
    </div>
  );
};

Animação de Upgrade

tsx
const UpgradeAnimation = ({ probability }: { probability: number }) => {
  return (
    <motion.div className="upgrade-animation">
      {/* Barra de probabilidade animada */}
      <div className="probability-bar">
        <motion.div
          className="success-zone bg-green-500"
          style={{ width: `${probability * 100}%` }}
        />
        <motion.div
          className="fail-zone bg-red-500"
          style={{ width: `${(1 - probability) * 100}%` }}
        />
        
        {/* Ponteiro animado */}
        <motion.div
          className="pointer"
          animate={{
            x: ['0%', '100%', '0%'],
          }}
          transition={{
            duration: 5,
            ease: 'easeInOut',
          }}
        />
      </div>
    </motion.div>
  );
};

Endpoints da API

Calcular Probabilidade

http
GET /api/upgrades/probability?fromItemId=123&toItemId=456

Response:

json
{
  "fromItem": {
    "id": "123",
    "name": "AK-47 | Redline",
    "valueCents": 5000
  },
  "toItem": {
    "id": "456",
    "name": "AWP | Asiimov",
    "valueCents": 20000
  },
  "probability": 0.225,
  "probabilityPercent": "22.5%"
}

Executar Upgrade

http
POST /api/upgrades
Authorization: Bearer {sessionId}
Content-Type: application/json

{
  "fromItemId": "123",
  "toItemId": "456"
}

Response (Sucesso):

json
{
  "upgradeId": "789",
  "success": true,
  "probability": 0.225,
  "roll": 15234,
  "serverSeedHash": "abc123...",
  "fromItem": {
    "id": "123",
    "name": "AK-47 | Redline"
  },
  "toItem": {
    "id": "456",
    "name": "AWP | Asiimov"
  }
}

Response (Falha):

json
{
  "upgradeId": "789",
  "success": false,
  "probability": 0.225,
  "roll": 78543,
  "serverSeedHash": "abc123...",
  "fromItem": {
    "id": "123",
    "name": "AK-47 | Redline"
  },
  "toItem": null
}

Verificar Fairness

http
GET /api/upgrades/:id/verify

Response:

json
{
  "upgradeId": "789",
  "serverSeed": "original-seed",
  "serverSeedHash": "abc123...",
  "clientSeed": "user-seed",
  "nonce": 42,
  "roll": 15234,
  "calculatedRoll": 15234,
  "probability": 0.225,
  "threshold": 22500,
  "success": true,
  "isValid": true
}

Histórico de Upgrades

http
GET /api/upgrades/history?page=1&limit=20

Response:

json
{
  "upgrades": [
    {
      "id": "789",
      "fromItem": { "name": "AK-47 | Redline", "valueCents": 5000 },
      "toItem": { "name": "AWP | Asiimov", "valueCents": 20000 },
      "probability": 0.225,
      "success": true,
      "createdAt": "2024-01-15T10:30:00Z"
    }
  ],
  "stats": {
    "totalUpgrades": 45,
    "successCount": 12,
    "successRate": 0.267,
    "totalValueWon": 185000,
    "totalValueLost": 95000,
    "netProfit": 90000
  }
}

Validações

Regras de Upgrade

typescript
// upgrade-validation.service.ts
validateUpgrade(
  fromItem: Item,
  toItem: Item,
  userId: bigint,
): ValidationResult {
  const errors: string[] = [];
  
  // 1. Item destino deve valer mais
  if (fromItem.valueCents >= toItem.valueCents) {
    errors.push('Item de destino deve ter valor maior');
  }
  
  // 2. Limite de multiplicador
  const MAX_MULTIPLIER = 10;
  const multiplier = Number(toItem.valueCents) / Number(fromItem.valueCents);
  if (multiplier > MAX_MULTIPLIER) {
    errors.push(`Multiplicador máximo é ${MAX_MULTIPLIER}x`);
  }
  
  // 3. Item destino deve estar disponível
  if (!toItem.isUpgradable) {
    errors.push('Item não disponível para upgrade');
  }
  
  // 4. Probabilidade mínima
  const MIN_PROBABILITY = 0.01;
  const probability = this.calculateProbability(
    fromItem.valueCents,
    toItem.valueCents,
  );
  if (probability < MIN_PROBABILITY) {
    errors.push('Probabilidade muito baixa');
  }
  
  return {
    valid: errors.length === 0,
    errors,
  };
}

Troubleshooting

Probabilidade Incorreta

typescript
// Recalcular e verificar
const fromValue = 5000n; // R$ 50
const toValue = 20000n;  // R$ 200

const baseProbability = Number(fromValue) / Number(toValue);
console.log('Base:', baseProbability); // 0.25

const adjusted = baseProbability * 0.90; // 10% house edge
console.log('Adjusted:', adjusted); // 0.225

Item Não Removido

typescript
// Verificar inventário após upgrade
const inventory = await inventoryRepository.findByUser(userId);

const hasOriginal = inventory.some(i => i.itemId === fromItemId);
if (hasOriginal) {
  console.error('Item original não foi removido!');
  // Remover manualmente
  await inventoryService.removeItem(userId, fromItemId);
}

Documentação Técnica CSGOFlip