O ranking global do Navistron é o sistema que registra e exibe os melhores desempenhos dos jogadores. Cada partida pode gerar um registro permanente no banco de dados, alimentando três rankings distintos (geral, semanal e mensal), perfis individuais de jogador e estatísticas agregadas. Neste artigo, vamos detalhar cada etapa: desde o momento em que o jogador clica em "Save & Ranking" até a exibição final na tabela de classificação.

O Fluxo Completo de Salvamento: Do Game Over ao MongoDB

Quando a nave é destruída e a animação de morte termina, a função endGame() prepara um objeto pendingSession com todos os dados da partida e exibe o overlay de game over após um delay de 700ms. O overlay apresenta as estatísticas finais e um campo de texto para o nome do piloto (máximo 20 caracteres).

Ao clicar em "SAVE & RANKING" (ou pressionar Enter), o nome digitado é processado: trim() para remover espaços, toUpperCase() para padronizar em maiúsculas, e slice(0, 20) para respeitar o limite. Se o campo estiver vazio, o nome padrão "ANONYMOUS" é usado. O score é então enviado via POST /api/scores com 8 campos da partida.

No servidor, a API aplica validação por castagem de tipo: cada campo é convertido com Number() ou String(), com defaults seguros (score: 0, tier: 0, spread: 1, tierName: 'TIER I'). O campo playedAt é gerado no servidor com new Date(), garantindo um timestamp confiável independente do relógio do cliente. O documento é inserido na collection scores do banco navistron via MongoDB.

Os 9 Campos Armazenados por Score

Cada registro no ranking contém exatamente 9 campos, capturando o estado completo da partida:

  • name — Nome do piloto (string, até 20 caracteres, maiúsculas)
  • scorePontuação total (número inteiro)
  • tier — Índice do tier final (0–6, representando Tier I a VII)
  • tierName — Nome legível do tier (ex: "TIER III")
  • boosts — Total de boosts coletados durante a partida
  • spread — Nível de spread dos projéteis ao morrer
  • time — Tempo de sobrevivência em segundos inteiros (Math.floor(timeAlive))
  • difficultyMultiplicador de dificuldade final (arredondado a 2 casas decimais)
  • playedAt — Timestamp do servidor (Date), usado para filtros temporais

Esses campos permitem não apenas ordenar por pontuação, mas também analisar correlações: quanto tempo o jogador sobreviveu, qual tier alcançou, quantos boosts coletou e qual era a dificuldade no momento da morte.

Ordenação e Top 100: Como o Ranking É Montado

O ranking global usa uma consulta MongoDB direta: db.collection('scores').find({}).sort({ score: -1 }).limit(100). Isso significa:

  • Sem filtro — todos os scores de todos os tempos são considerados
  • Ordenação decrescente por score — o maior score aparece primeiro
  • Limite de 100 — apenas os top 100 são retornados

Em caso de empate, o MongoDB mantém a ordem natural de inserção (quem registrou primeiro aparece acima). Não há critério de desempate secundário explícito como tempo de sobrevivência ou tier. A página de ranking usa ISR (Incremental Static Regeneration) com revalidação a cada 300 segundos (5 minutos), equilibrando dados frescos com performance de carregamento.

Rankings Semanal e Mensal: Filtro por Período

Além do ranking geral (all-time), o Navistron oferece dois rankings temporais acessíveis por abas de navegação:

  • Ranking Semanal — mostra os top 100 scores dos últimos 7 dias, filtrado por playedAt >= (agora - 7 dias)
  • Ranking Mensal — mostra os top 100 scores dos últimos 30 dias, filtrado por playedAt >= (agora - 30 dias)

Ambos mantêm a mesma ordenação (score: -1) e limite (100). A diferença é um filtro $gte no campo playedAt que restringe os resultados ao período desejado. Isso permite que novos jogadores compitam em igualdade contra veteranos — mesmo sem chance no ranking geral, é possível dominar o ranking semanal.

Perfil de Jogador: Estatísticas Agregadas e Rank Global

Cada jogador que salva ao menos um score recebe automaticamente uma página de perfil em /player/{nome}. O perfil exibe estatísticas calculadas via aggregation pipeline do MongoDB, agrupando todos os scores do jogador:

  • Total de partidas$sum: 1
  • Melhor score$max: '$score'
  • Score médio$avg: '$score' (arredondado)
  • Melhor tier$max: '$tier', convertido para nome ("TIER IV")
  • Tempo total jogado$sum: '$time' (soma de todas as partidas)
  • Tempo médio$avg: '$time' (arredondado)
  • Total de boosts$sum: '$boosts'
  • Maior spread$max: '$spread'
  • Primeira e última partida$min / $max de playedAt

O rank global é calculado contando quantos jogadores únicos têm um melhor score superior ao do jogador consultado, usando uma segunda aggregation: agrupa por nome, pega o $max do score de cada um, filtra os que são maiores, conta, e soma 1. Se 14 jogadores têm best score maior que o seu, seu rank é #15.

Abaixo das estatísticas, o perfil exibe uma tabela com os top 20 scores do jogador, permitindo ver a evolução do desempenho ao longo do tempo. Páginas de jogadores são pré-geradas estaticamente para até 500 nomes (via generateStaticParams) com ISR de 1 hora, e jogadores não pré-gerados são renderizados sob demanda.

A Tabela de Ranking: Colunas, Medalhas e Cores

O componente RankingTable renderiza 8 colunas de informação para cada score:

  • # — Posição: medalhas de ouro 🥇, prata 🥈 e bronze 🥉 para o top 3, numérica para os demais
  • Piloto — Nome do jogador, com link para a página de perfil
  • Score — Pontuação formatada com toLocaleString(), em cor ciano (#7df4ff)
  • Tempo — Formatado como M:SS
  • Tier — Nome do tier com cor específica: ciano (I), verde (II), amarelo (III), laranja (IV), rosa (V), roxo (VI), branco (VII)
  • Boosts — Total de boosts coletados
  • Spread — Nível de spread final
  • Data — Data da partida em formato pt-BR

A tabela não faz ordenação no cliente — os dados já chegam pré-ordenados do servidor por score decrescente.

Leaderboard In-Game: Veja o Ranking Sem Sair do Jogo

Após salvar o score (ou clicar Skip), o engine do jogo exibe um leaderboard overlay diretamente dentro da tela de jogo. Esse overlay busca o top 100 via GET /api/scores e renderiza uma tabela compacta com as mesmas colunas do ranking público: posição, piloto, score, tempo, tier, boosts, spread e data.

Se o jogador acabou de salvar, sua entrada na tabela é destacada visualmente (comparando o ID retornado pela API com cada registro) e a tabela rola automaticamente até a posição destacada. Abaixo do leaderboard, uma tabela de patrocinadores é exibida com links clicáveis e tracking de cliques.

Sessões Anônimas: O Que Acontece ao Clicar "Skip"

Quando o jogador clica "SKIP", a partida é registrada como sessão anônima do tipo "Unregistered" na collection anonymous_sessions — separada do ranking. Se o jogador fechar a aba sem escolher (nem Save nem Skip), o sistema usa navigator.sendBeacon no evento beforeunload para salvar como tipo "Unknown". Se iniciar um novo jogo sem resolver, também salva como "Unknown".

Sessões anônimas contêm os mesmos dados de partida (score, tier, boosts, spread, tempo, dificuldade) mas nunca aparecem no ranking público. Elas servem para telemetria e analytics, permitindo entender o comportamento de jogadores que preferem não se registrar.

Posso Aparecer Múltiplas Vezes no Ranking?

Sim. Cada partida gera um registro independente no MongoDB. Um mesmo piloto pode aparecer várias vezes no top 100 com scores diferentes — não há agrupamento por nome no ranking. O nome é digitado do zero a cada partida (não há login, conta ou localStorage), então cada registro é completamente autônomo. Isso incentiva rejogabilidade: bater seu próprio recorde anterior e ocupar múltiplas posições no ranking.

Infraestrutura: MongoDB e ISR

O banco de dados é MongoDB hospedado externamente, conectado via MONGODB_URI. Em desenvolvimento, a conexão é cacheada em global._mongoClientPromise para sobreviver ao Hot Module Replacement (HMR). Em produção, cada cold start do Vercel cria uma nova conexão. O banco navistron utiliza 3 collections: scores (ranking), anonymous_sessions (sessões não registradas) e sponsors (patrocinadores).

As páginas de ranking usam Incremental Static Regeneration (ISR do Next.js): o ranking geral, semanal e mensal revalidam a cada 5 minutos (300s), e os perfis de jogador a cada 1 hora (3600s). Isso garante tempos de carregamento quase instantâneos para a maioria dos acessos, com dados atualizados em intervalos razoáveis.

FAQ — Perguntas Frequentes sobre o Ranking do Navistron

Posso usar o mesmo nome várias vezes?

Sim. Como não há sistema de login, cada partida é um registro independente. Você pode salvar com o mesmo nome em todas as partidas, e cada score aparecerá individualmente no ranking. Dois jogadores diferentes também podem usar o mesmo nome — a identidade é baseada unicamente no texto digitado.

Quantos scores aparecem no ranking?

O ranking exibe os top 100 scores em todas as visualizações (geral, semanal e mensal). O leaderboard in-game também mostra os top 100. Os perfis de jogador mostram os top 20 scores individuais daquele piloto, além das estatísticas agregadas de todas as partidas.

Como funciona o critério de desempate?

O ranking é ordenado exclusivamente por pontuação total decrescente (score: -1). Em caso de empate exato, prevalece a ordem de inserção no MongoDB — quem salvou o score primeiro aparece acima. Não há desempate secundário por tempo de sobrevivência, tier ou dificuldade.

O que é o rank global no perfil do jogador?

O rank global indica a posição do jogador considerando o melhor score de cada jogador único. Diferente do ranking (que lista partidas individuais), o rank global agrupa por nome e compara apenas o melhor resultado de cada um. Se 14 jogadores distintos têm best score superior ao seu, você é #15 no rank global.

Os patrocinadores têm vantagem no ranking?

Não. Patrocinadores são exibidos em uma tabela separada abaixo do ranking, sem qualquer influência na ordenação dos scores. Eles não recebem posições especiais, pontos bônus ou destaque no leaderboard — a competição é idêntica para todos os jogadores.