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 Original | Item Destino | Probabilidade |
|---|---|---|
| R$ 10 | R$ 50 | 18% |
| R$ 10 | R$ 100 | 9% |
| R$ 50 | R$ 100 | 45% |
| R$ 50 | R$ 200 | 22.5% |
| R$ 100 | R$ 500 | 18% |
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=456Response:
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/verifyResponse:
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=20Response:
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.225Item 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);
}