Skip to content

Sistema FLIP - Roleta Dupla com Itens Raros

O sistema FLIP e uma mecanica especial de abertura de caixas que ativa uma segunda roleta contendo exclusivamente itens do tier raro. Cada caixa tem um flipThreshold configuravel (Case.flipThreshold, nullable). Quando o roll normal do jogador e maior ou igual a esse threshold (roll >= flipThreshold), o FLIP e ativado automaticamente. O jogador passa por duas roletas consecutivas: a primeira para na KNIFE (card especial), seguida de uma animacao, e a segunda roleta gira com os itens raros da caixa.


Indice

  1. Visao Geral do Fluxo
  2. Determinacao do FLIP Threshold
  3. Verificacao de Ativacao do FLIP
  4. Itens Elegiveis para FLIP
  5. Itens Normais (Nao-FLIP)
  6. Selecao do Item Ganho
  7. Preparacao de Itens para Roleta
  8. Integracao com OpenCaseUseCase
  9. Provably Fair - Seeds e Nonce
  10. Modelo de Dados
  11. Response da API
  12. Live Drop Delay
  13. Timeline do Frontend
  14. Casos de Borda
  15. Debugging
  16. Arquivos Relacionados

Visao Geral do Fluxo

1. Roll normal e calculado           -> provablyFairService.calculateRoll()
2. Flip threshold e lido da caixa    -> caseData.flipThreshold (Case.flipThreshold)
3. Verifica se roll >= threshold     -> flipService.shouldTriggerFlip(roll, threshold)
4. Se FLIP ativado:
   a. Separa itens normais           -> hashRangeStart < threshold
   b. Separa itens raros (FLIP)      -> hashRangeStart >= threshold
   c. Prepara itens para roleta      -> prepareFlipItemsForRoulette()
   d. Seleciona item pelo roll       -> flipService.selectFlipWinner(flipItems, roll)
   e. Item ganho e do tier raro      -> (roll >= threshold = range de item raro)
5. Dados FLIP sao retornados na response
6. Frontend exibe duas roletas consecutivas (25.5s total)
7. Live drop emitido com delay de 30s

Determinacao do FLIP Threshold

O threshold do FLIP e configurado por caixa na coluna Case.flipThreshold (Int, nullable). Se for null, a caixa nao tem FLIP.

Implementacao

typescript
// src/application/services/flip.service.ts

async getFlipThreshold(caseId: bigint): Promise<number | null> {
  const caseData = await this.caseRepository.findById(caseId);
  return caseData?.flipThreshold ?? null;
}

Como Funciona

  1. Le o campo flipThreshold da caixa (Case.flipThreshold)
  2. Se for null, retorna null (FLIP desativado para essa caixa)
  3. Caso contrario, esse valor e o threshold: qualquer roll >= threshold ativa o FLIP
  4. Os itens do tier raro sao os que tem hashRangeStart >= threshold; os normais tem hashRangeStart < threshold

No fluxo de abertura (OpenCaseUseCase), o threshold e lido diretamente de caseData.flipThreshold (a caixa ja vem carregada via findByIdCached), sem uma chamada extra ao FlipService.

Exemplo Pratico

Supondo uma caixa com flipThreshold = 9.280.000 e estes hash ranges:

ItemhashRangeStarthashRangeEndTier
MAG-7 Wildwood06.681.999Normal
P250 Sand Dune6.682.0008.279.999Normal
AK-47 Redline8.280.0009.279.999Normal
AWP Asiimov9.280.0009.679.999FLIP
M4A4 Howl9.680.0009.879.999FLIP
Butterfly Emerald9.880.0009.999.999FLIP

Como flipThreshold = 9.280.000, qualquer roll >= 9.280.000 ativa o FLIP e cai no range de um item do tier FLIP (hashRangeStart >= threshold). A probabilidade do FLIP e a soma das probabilidades desses itens:

  • AWP Asiimov: 4.00%
  • M4A4 Howl: 2.00%
  • Butterfly Emerald: 1.20%
  • Probabilidade total do FLIP: 7.20%

Diferenca do Modelo Antigo

O modelo antigo usava um threshold fixo (roll < 5000), ou seja, 0.05% de chance fixa. O modelo atual usa o flipThreshold configurado por caixa, tornando a probabilidade ajustavel por caixa (definida por quais itens ficam acima do threshold). O campo CaseItem.isRare ainda existe no schema, mas NAO e usado pelo FlipService — a separacao FLIP/normal e feita 100% por hashRangeStart vs flipThreshold.


Verificacao de Ativacao do FLIP

typescript
// src/application/services/flip.service.ts

shouldTriggerFlip(roll: number, flipThreshold: number | null): boolean {
  if (flipThreshold === null) {
    return false;
  }
  return roll >= flipThreshold;
}

Regras:

  • Se flipThreshold === null (nenhum item raro na caixa), FLIP nunca ativa
  • Se roll >= flipThreshold, FLIP ativa
  • O roll ja determina QUAL item raro foi ganho (via hash ranges)
  • A ativacao do FLIP e uma consequencia natural do roll cair no range de um item raro

Itens Elegiveis para FLIP

typescript
// src/application/services/flip.service.ts

async getFlipItems(caseId: bigint, flipThreshold: number | null = null) {
  const threshold = flipThreshold ?? await this.getFlipThreshold(caseId);
  if (threshold === null) return [];
  const caseItems = await this.caseRepository.getCaseItems(caseId);
  return caseItems.filter((ci) => ci.hashRangeStart >= threshold);
}

Comportamento:

  • Retorna os CaseItem cujo hashRangeStart >= flipThreshold (o tier do topo)
  • Se a caixa nao tem flipThreshold (null), retorna array vazio
  • Nao usa o campo isRare nem a raridade (Rarity enum) - a elegibilidade e puramente pela posicao do hash range
  • Os itens retornados incluem a relacao com Item (nome, imagem, valor, etc.)

Diferenca do Modelo Antigo

O modelo antigo buscava itens por raridade (COVERT, EXTRAORDINARY, com fallback para CLASSIFIED). O modelo atual seleciona os itens pela posicao do hashRangeStart em relacao ao flipThreshold da caixa, dando controle direto sobre o corte via o threshold configurado.


Itens Normais (Nao-FLIP)

typescript
// src/application/services/flip.service.ts

async getNormalItems(caseId: bigint, flipThreshold: number | null = null) {
  const threshold = flipThreshold ?? await this.getFlipThreshold(caseId);
  if (threshold === null) {
    return this.caseRepository.getCaseItems(caseId);
  }
  const caseItems = await this.caseRepository.getCaseItems(caseId);
  return caseItems.filter((ci) => ci.hashRangeStart < threshold);
}

Comportamento:

  • Retorna os CaseItem cujo hashRangeStart < flipThreshold
  • Se a caixa nao tem flipThreshold (null), retorna TODOS os itens da caixa
  • Esses itens compoe a primeira roleta (roleta normal)
  • Sao retornados no campo normalItems da response da API

Selecao do Item Ganho

O item ganho e SEMPRE determinado pelo roll unico, independente de ser FLIP ou nao. Quando o FLIP ativa, a selecao passa primeiro por selectFlipWinner (busca no tier raro); se nada casar, cai no getItemByRoll sobre todos os itens:

typescript
// src/application/services/flip.service.ts

selectFlipWinner(flipItems: any[], roll: number): any | null {
  const matchedItem = flipItems.find(
    (item) => roll >= item.hashRangeStart && roll <= item.hashRangeEnd,
  );
  return matchedItem ?? null;
}

getItemByRoll(items: any[], roll: number) {
  const matchedItem = items.find(
    (item) => roll >= item.hashRangeStart && roll <= item.hashRangeEnd,
  );

  if (!matchedItem) {
    throw new Error(`No item found for roll ${roll}`);
  }

  return matchedItem;
}

Fluxo no OpenCaseUseCase:

typescript
let wonItem;
let actualIsFlip = false;
if (isFlip && flipItems.length > 0) {
  const flipWinner = this.flipService.selectFlipWinner(flipItems, roll);
  if (flipWinner) {
    wonItem = flipWinner;
    actualIsFlip = true;
  }
}
if (!wonItem) {
  wonItem = this.flipService.getItemByRoll(allItems, roll);
}

Pontos criticos:

  • A selecao FLIP usa selectFlipWinner sobre os itens do tier raro (flipItems); o fallback getItemByRoll cobre TODOS os itens
  • O roll ja esta no range de um item raro (porque roll >= flipThreshold)
  • actualIsFlip so vira true quando selectFlipWinner retorna um vencedor — e esse flag (nao o isFlip bruto) que e persistido e retornado
  • NAO existe um segundo roll separado para o FLIP no codigo atual
  • O flipRoll na response e o MESMO valor do roll normal (retornado apenas quando actualIsFlip = true)

Preparacao de Itens para Roleta

O metodo prepareFlipItemsForRoulette formata os itens para o frontend:

typescript
// src/application/use-cases/case-opening/open-case.use-case.ts

private prepareFlipItemsForRoulette(items: any[]): any[] {
  const sortedItems = [...items].sort(
    (a, b) => Number(b.item.valueCentsBrl) - Number(a.item.valueCentsBrl),
  );

  return sortedItems.map((item) => ({
    id: item.id.toString(),
    itemId: item.item.id.toString(),
    itemName: item.item.name,
    itemImage: item.item.imageUrl,
    rarity: item.item.rarity,
    valueCents: item.item.valueCentsBrl.toString(),
    valueFormatted: (Number(item.item.valueCentsBrl) / 100).toFixed(2),
    dropChance: item.dropChance,
    weight: item.weight,
    weaponCategory: item.item.weaponCategory,
    exterior: item.item.exterior ?? undefined,
    isStatTrak: item.item.isStatTrak,
  }));
}

Comportamento:

  • Ordena por valueCentsBrl descendente (mais caros primeiro)
  • Retorna TODOS os itens passados (sem limitacao de 15 slots)
  • Converte todos os BigInt para string (necessario para JSON)
  • valueCents no retorno e valueCentsBrl (BRL, nao USD)
  • valueFormatted e o valor em reais com 2 casas decimais (ex: "707.65")
  • Usado tanto para normalItems quanto para flipItems

Diferenca do Modelo Antigo

O modelo antigo limitava a 15 slots e usava preenchimento circular quando havia menos de 15 itens. O modelo atual retorna TODOS os itens e delega ao frontend a responsabilidade de montar a roleta visual.


Integracao com OpenCaseUseCase

O FLIP e integrado diretamente no fluxo principal de abertura. Trecho relevante do OpenCaseUseCase.execute():

typescript
// Le flip threshold e itens direto da caixa (ja carregada via findByIdCached)
const flipThreshold = caseData.flipThreshold;
const allItems = caseData.items;

// Verifica se o roll ativa FLIP
const isFlip = this.flipService.shouldTriggerFlip(roll, flipThreshold);

// Separa itens por tier usando o threshold
const normalItems =
  flipThreshold === null ? allItems : allItems.filter((ci) => ci.hashRangeStart < flipThreshold);
const normalItemsData = this.prepareFlipItemsForRoulette(normalItems);

const flipItems =
  flipThreshold === null ? [] : allItems.filter((ci) => ci.hashRangeStart >= flipThreshold);
const flipItemsData = this.prepareFlipItemsForRoulette(flipItems);

// Seleciona item ganho: FLIP primeiro (selectFlipWinner), fallback getItemByRoll
let wonItem;
let actualIsFlip = false;
if (isFlip && flipItems.length > 0) {
  const flipWinner = this.flipService.selectFlipWinner(flipItems, roll);
  if (flipWinner) {
    wonItem = flipWinner;
    actualIsFlip = true;
  }
}
if (!wonItem) {
  wonItem = this.flipService.getItemByRoll(allItems, roll);
}

Dados salvos no CaseOpening:

typescript
const opening = await this.caseOpeningRepository.create({
  userId,
  caseId,
  itemId: wonItem.item.id,
  pricePaidCents: caseData.priceCents,
  itemValueCents: wonItem.item.valueCentsBrl,
  profitCents,
  serverSeed,
  serverSeedHash,
  clientSeed: finalClientSeed,
  nonce,
  roll,
  isFlip: actualIsFlip,
  flipServerSeed: null,
  flipClientSeed: null,
  flipNonce: null,
  flipRoll: null,
});

Observacao importante: No codigo atual, os campos flipServerSeed, flipClientSeed, flipNonce e flipRoll sao salvos como null. O flipRoll retornado na response e o mesmo roll normal:

typescript
return {
  opening,
  item: wonItem.item,
  dropChance: wonItem.dropChance,
  inventoryItemId: inventoryItem.id,
  serverSeedHash,
  clientSeed: finalClientSeed,
  nonce,
  roll,
  isFlip: actualIsFlip,
  flipRoll: actualIsFlip ? roll : undefined, // Mesmo valor do roll normal, so quando ativa
  flipItems: actualIsFlip ? flipItemsData : undefined,
  normalItems: normalItemsData,
};

Provably Fair - Seeds e Nonce

Estado Atual

No codigo atual, o FLIP usa o mesmo roll da abertura normal. O item ganho e determinado pelo roll principal caindo no hash range de um item do tier raro (hashRangeStart >= flipThreshold). Por isso:

  • Nao ha seeds separados para o FLIP
  • flipServerSeed, flipClientSeed, flipNonce sao salvos como null
  • flipRoll na response e igual ao roll principal

Modelo Documentado (CLAUDE.md)

O CLAUDE.md documenta um modelo com seeds derivados:

flipServerSeed = serverSeed + '_flip'
flipClientSeed = clientSeed (mesmo)
flipNonce = nonce + 1
flipRoll = calculateRoll(flipServerSeed, flipClientSeed, flipNonce)

Este modelo com seeds derivados esta descrito como a arquitetura alvo, mas a implementacao atual em FlipService e OpenCaseUseCase usa o roll unico. Os campos no banco de dados (flipServerSeed, flipClientSeed, flipNonce, flipRoll) ja existem no schema Prisma para suportar a implementacao futura.

Verificacao Provably Fair do FLIP

Como o FLIP usa o mesmo roll da abertura normal, a verificacao e identica:

typescript
// POST /cases/openings/:id/verify

const isServerSeedValid = this.provablyFairService.verifyServerSeedHash(
  opening.serverSeed,
  opening.serverSeedHash,
);

const isRollValid = this.provablyFairService.verifyResult(
  opening.serverSeed,
  opening.clientSeed,
  opening.nonce,
  opening.roll,
);

O usuario pode verificar que:

  1. O serverSeedHash corresponde ao serverSeed revelado
  2. O roll calculado com os seeds e nonce corresponde ao armazenado
  3. O roll cai no hashRangeStart/hashRangeEnd de um item do tier raro (hashRangeStart >= flipThreshold), confirmando que o FLIP deveria ativar

Modelo de Dados

CaseItem (campo isRare)

prisma
model CaseItem {
  id              BigInt   @id
  caseId          BigInt   @map("case_id")
  itemId          BigInt   @map("item_id")
  weight          Int
  hashRangeStart  Int      @map("hash_range_start")
  hashRangeEnd    Int      @map("hash_range_end")
  dropChance      Float    @map("drop_chance")
  isRare          Boolean  @default(false) @map("is_rare")

  case            Case     @relation(fields: [caseId], references: [id], onDelete: Cascade)
  item            Item     @relation(fields: [itemId], references: [id])

  @@unique([caseId, itemId])
  @@map("case_items")
  @@index([caseId])
  @@index([itemId])
}

O campo isRare ainda existe no schema (legado), mas NAO e usado pelo FlipService. A participacao no FLIP e determinada pela posicao do hashRangeStart do item em relacao ao Case.flipThreshold. Padrao de isRare: false.

Case (campo flipThreshold)

prisma
model Case {
  // ... demais campos ...
  flipThreshold          Int?     @map("flip_threshold")
  flipDisplayRareSpecial Boolean  @default(false) @map("flip_display_rare_special")
  // ...
}

flipThreshold (nullable) e o unico determinante do FLIP: se null, a caixa nao tem FLIP; caso contrario, roll >= flipThreshold ativa o FLIP e itens com hashRangeStart >= flipThreshold formam o tier raro.

CaseOpening (campos FLIP)

prisma
model CaseOpening {
  // ... campos de abertura normal ...

  isFlip          Boolean  @default(false) @map("is_flip")
  flipServerSeed  String?  @map("flip_server_seed")
  flipClientSeed  String?  @map("flip_client_seed")
  flipNonce       Int?     @map("flip_nonce")
  flipRoll        Int?     @map("flip_roll")

  // ...
  @@index([isFlip])
}

Campos presentes no schema para suportar dual provably fair:

  • isFlip: true quando o FLIP foi ativado
  • flipServerSeed: Reservado (atualmente null)
  • flipClientSeed: Reservado (atualmente null)
  • flipNonce: Reservado (atualmente null)
  • flipRoll: Reservado (atualmente null)

Response da API

Abertura Normal (sem FLIP)

json
{
  "id": "789012345",
  "inventoryItemId": "111222333",
  "userId": "100",
  "caseId": "123456789",
  "caseName": "",
  "caseImage": "",
  "itemId": "456",
  "itemName": "AK-47 | Redline",
  "itemImage": "https://...",
  "itemRarity": "CLASSIFIED",
  "itemValueCents": "15000",
  "itemValueFormatted": "R$ 150,00",
  "pricePaidCents": "1590",
  "pricePaidFormatted": "R$ 15,90",
  "profitCents": "13410",
  "profitFormatted": "R$ 134,10",
  "serverSeedHash": "a1b2c3d4e5f6...",
  "clientSeed": "minha-seed",
  "nonce": 456789,
  "roll": 8500000,
  "isFlip": false,
  "normalItems": [
    {
      "id": "1",
      "itemId": "456",
      "itemName": "AK-47 | Redline",
      "itemImage": "https://...",
      "rarity": "CLASSIFIED",
      "valueCents": "15000",
      "valueFormatted": "150.00",
      "dropChance": 0.26,
      "weight": 26,
      "weaponCategory": "RIFLE",
      "isStatTrak": false
    }
  ],
  "createdAt": "2026-02-11T10:30:00.000Z"
}

Campos FLIP quando isFlip = false:

  • isFlip: false
  • flipRoll: Ausente (undefined) - so e retornado quando o FLIP ativa
  • flipItems: Ausente (undefined, nao serializado)
  • normalItems: Presente, com itens do tier normal (hashRangeStart < flipThreshold) ordenados por valor desc

Abertura com FLIP

json
{
  "id": "789012345",
  "inventoryItemId": "111222333",
  "userId": "100",
  "caseId": "123456789",
  "caseName": "",
  "caseImage": "",
  "itemId": "999",
  "itemName": "Butterfly Knife | Emerald",
  "itemImage": "https://...",
  "itemRarity": "EXTRAORDINARY",
  "itemValueCents": "70765",
  "itemValueFormatted": "R$ 707,65",
  "pricePaidCents": "1590",
  "pricePaidFormatted": "R$ 15,90",
  "profitCents": "69175",
  "profitFormatted": "R$ 691,75",
  "serverSeedHash": "a1b2c3d4e5f6...",
  "clientSeed": "minha-seed",
  "nonce": 456789,
  "roll": 9950000,
  "isFlip": true,
  "flipRoll": 9950000,
  "flipItems": [
    {
      "id": "10",
      "itemId": "999",
      "itemName": "Butterfly Knife | Emerald",
      "itemImage": "https://...",
      "rarity": "EXTRAORDINARY",
      "valueCents": "70765",
      "valueFormatted": "707.65",
      "dropChance": 1.20,
      "weight": 120,
      "weaponCategory": "KNIFE",
      "exterior": "FACTORY_NEW",
      "isStatTrak": false
    },
    {
      "id": "9",
      "itemId": "998",
      "itemName": "M4A4 | Howl",
      "itemImage": "https://...",
      "rarity": "COVERT",
      "valueCents": "45000",
      "valueFormatted": "450.00",
      "dropChance": 2.00,
      "weight": 200,
      "weaponCategory": "RIFLE",
      "isStatTrak": false
    },
    {
      "id": "8",
      "itemId": "997",
      "itemName": "AWP | Asiimov",
      "itemImage": "https://...",
      "rarity": "COVERT",
      "valueCents": "25000",
      "valueFormatted": "250.00",
      "dropChance": 4.00,
      "weight": 400,
      "weaponCategory": "SNIPER",
      "exterior": "FIELD_TESTED",
      "isStatTrak": false
    }
  ],
  "normalItems": [
    {
      "id": "1",
      "itemId": "456",
      "itemName": "MAG-7 | Wildwood",
      "itemImage": "https://...",
      "rarity": "CONSUMER",
      "valueCents": "37",
      "valueFormatted": "0.37",
      "dropChance": 66.82,
      "weight": 6682,
      "weaponCategory": "SHOTGUN",
      "isStatTrak": false
    }
  ],
  "createdAt": "2026-02-11T10:30:00.000Z"
}

Campos FLIP quando isFlip = true:

  • isFlip: true
  • flipRoll: Presente, igual ao roll (mesmo valor)
  • flipItems: Presente, array com itens do tier raro (hashRangeStart >= flipThreshold) ordenados por valueCentsBrl desc
  • normalItems: Presente, array com itens do tier normal (hashRangeStart < flipThreshold) ordenados por valueCentsBrl desc
  • O itemId/itemName/itemRarity/itemValueCents na raiz da response referem-se ao item raro ganho

Live Drop Delay

typescript
const liveDropDelay = result.isFlip ? 30000 : 12000;
TipoDelayMotivo
Normal12.000ms (12s)Animacao da roleta normal
FLIP30.000ms (30s)Primeira roleta + animacao faca + segunda roleta

O live drop e emitido via setTimeout apos o commit da transacao. Todos os jogadores conectados recebem o evento live:drop via WebSocket (broadcast global).


Timeline do Frontend

Duracao Total

Normal: ~12 segundos FLIP: ~25.5 segundos

Timeline FLIP Detalhada

t = 0.0s     -> Recebe response da API com isFlip = true
t = 0.0s     -> Configura estados: setIsFlip(true), setFlipItems(...), setFlipRoll(...)
t = 1.5s     -> Inicia PRIMEIRA roleta (itens normais + card KNIFE no meio)
t = 11.5s    -> Primeira roleta PARA no card KNIFE
t = 11.5s    -> Inicia animacao KNIFE (rotacao 3D)
t = 14.5s    -> Esconde animacao KNIFE
t = 14.5s    -> Mostra SEGUNDA roleta (apenas itens raros / flipItems)
t = 15.5s    -> Inicia spin da segunda roleta (vai ate o item ganho)
t = 25.5s    -> Segunda roleta PARA no item ganho
t = 25.5s    -> Mostra tela de vitoria

Fases

FaseInicioFimDuracaoDescricao
Primeira roleta1.5s11.5s10sRoleta normal para na KNIFE
Animacao KNIFE11.5s14.5s3sKNIFE girando em 3D
Setup segunda roleta14.5s15.5s1sTroca visual para roleta de raros
Segunda roleta15.5s25.5s10sRoleta de raros para no item ganho

Estados do Frontend

typescript
const [isFlip, setIsFlip] = useState(false);
const [showKnifeAnimation, setShowKnifeAnimation] = useState(false);
const [showFlipRoulette, setShowFlipRoulette] = useState(false);
const [flipItems, setFlipItems] = useState<any[]>([]);
const [flipRoll, setFlipRoll] = useState<number | null>(null);
const [currentPhase, setCurrentPhase] = useState<
  'idle' | 'first-roulette' | 'knife' | 'second-roulette' | 'won'
>('idle');

Primeira Roleta

A primeira roleta usa os normalItems da response e inclui um card KNIFE no meio (repeat index 2, posicao central). A roleta e calculada para parar exatamente no card KNIFE.

Animacao KNIFE

Card especial com rotacao 3D (rotateY: [0, 180, 360]) e efeito de escala (scale: [1, 1.2, 1]). Dura 3 segundos com overlay escuro (bg-black/80).

Segunda Roleta

Usa os flipItems da response. O frontend repete os itens visualmente para criar efeito de loop e calcula o offset final baseado no flipRoll para parar no item ganho.


Casos de Borda

Caixa sem flipThreshold (null)

Se Case.flipThreshold e null:

  • getFlipThreshold() retorna null
  • shouldTriggerFlip() retorna false
  • FLIP nunca ativa para essa caixa
  • flipItems na response sera undefined
  • normalItems contera todos os itens da caixa

Apenas 1 Item no Tier FLIP

Se apenas 1 item tem hashRangeStart >= flipThreshold:

  • flipItems tera apenas 1 elemento
  • Frontend precisa repetir esse item visualmente na segunda roleta
  • A probabilidade do FLIP e igual a dropChance desse unico item

flipThreshold muito baixo

Se flipThreshold for muito baixo (ex.: 0 ou 1):

  • Quase todos (ou todos) os itens terao hashRangeStart >= flipThreshold
  • FLIP ativara em praticamente TODAS as aberturas
  • normalItems sera vazio ou quase vazio
  • Frontend precisa tratar a primeira roleta com poucos/nenhum item normal

Roll no Limite do Threshold

Se roll === flipThreshold:

  • shouldTriggerFlip() retorna true (usa >=)
  • O roll esta exatamente no hashRangeStart do primeiro item raro
  • O item selecionado sera o item raro com menor hashRangeStart

Nenhum Item Encontrado para Roll

Se getItemByRoll() nao encontra item:

  • Lanca Error('No item found for roll {roll}')
  • Indica que os hash ranges nao cobrem todo o espectro (bug de configuracao)
  • Deve ser corrigido via ProbabilityService.assignHashRanges()

Debugging

Backend - Logs Implementados

[OpenCaseUseCase] Roll generated: 9950000
[OpenCaseUseCase] Flip threshold: 9280000
[OpenCaseUseCase] Is FLIP: true (roll >= threshold)
[OpenCaseUseCase] Won item: Butterfly Knife | Emerald (tier FLIP)

Checklist de Verificacao

  1. FLIP nao ativa quando deveria:

    • Verificar se Case.flipThreshold esta configurado (nao null)
    • Verificar se ha itens com hashRangeStart >= flipThreshold (tier raro nao vazio)
    • Verificar se o roll esta realmente >= flipThreshold
  2. FLIP ativa quando nao deveria:

    • Verificar o valor de Case.flipThreshold (talvez baixo demais)
    • Verificar se os hash ranges estao corretos
  3. Item errado ganho no FLIP:

    • Verificar se selectFlipWinner() recebe os flipItems (tier raro) e o fallback getItemByRoll() recebe allItems
    • Verificar se os hash ranges nao tem overlap ou gaps
    • Usar ProbabilityService.assignHashRanges() para recalcular
  4. flipItems vazio na response:

    • Verificar se Case.flipThreshold esta configurado e ha itens com hashRangeStart >= flipThreshold
    • flipItems so e incluido quando o FLIP ativa (actualIsFlip = true)
  5. Verificar integridade dos hash ranges:

    typescript
    const items = await caseRepository.getCaseItems(caseId);
    const totalCoverage = items.reduce(
      (sum, i) => sum + (i.hashRangeEnd - i.hashRangeStart + 1), 0
    );
    // Deve ser exatamente 10.000.000
    
    const { flipThreshold } = await caseRepository.findById(caseId);
    const flipItems = items.filter(i => i.hashRangeStart >= flipThreshold);
    // Estes sao os itens do tier FLIP

Arquivos Relacionados

ArquivoResponsabilidade
src/application/services/flip.service.tsLogica completa do FLIP: threshold, itens raros/normais, selecao por roll
src/application/use-cases/case-opening/open-case.use-case.tsIntegracao do FLIP no fluxo de abertura, prepareFlipItemsForRoulette
src/application/services/provably-fair.service.tsCalculo e verificacao do roll
src/application/services/probability.service.tsCalculo de hash ranges (assignHashRanges)
src/application/use-cases/case-opening/verify-case-opening.use-case.tsVerificacao Provably Fair
src/presentation/controllers/case-opening.controller.tsEndpoint POST /cases/:id/open
src/application/dto/case-opening.dto.tsDTOs: FlipItemDto, CaseOpeningResponseDto
src/infrastructure/websocket/websocket.gateway.tsEmissao de live:drop e balance:updated
prisma/schema.prismaModelos Case (flipThreshold), CaseItem e CaseOpening (campos FLIP)

Documentação Técnica - ProCases