MTG Conquest — Life Tracker (Flutter)

10 minutos de leitura

Este post serve como changelog oficial do app. Sempre que uma nova versão for lançada, adicione uma entrada no topo da seção de changelog abaixo.

Convenção de changelog: entradas [Unreleased] — <data> são specs de planejamento históricos — registram o que estava previsto antes do lançamento. Não representam versões futuras pendentes. Quando uma versão é lançada, o spec permanece abaixo dela como referência do que foi planejado.

⬇ Baixar APK — v1.2.2

Instale em qualquer Android habilitando “Fontes desconhecidas” nas configurações.


Changelog

v1.2.2 — 01/03/2026

  • Melhoria: Orientação da tela gerenciada por contexto — SetupScreen sempre em portrait, GameScreen e GameOverScreen sempre em landscape. Controlado no _AppRouter via SystemChrome.setPreferredOrientations reativo ao gameProvider; valor inicial definido em main().

v1.2.1 — 01/03/2026

  • Bug fix: Busca de comandantes via Scryfall — a API exige os headers User-Agent e Accept: application/json em toda request; sem eles retorna HTTP 400. Builds release também precisavam de <uses-permission android:name="android.permission.INTERNET" /> no AndroidManifest.xml. URLs reescritas com Uri.encodeQueryComponent de forma absoluta no ScryfallService, eliminando ambiguidade de resolução do Dio.
  • Melhoria: Autocomplete agora filtra por regras Conquest (t:legendary t:creature or t:planeswalker) via /cards/search — apenas cartas elegíveis aparecem na lista. isValidConquestCommander atualizado: aceita Legendary Creature ou Planeswalker sem restrição de legalidade de formato.
  • Melhoria: Background da PlayerArea com blur reduzido (sigma 18 → 4) e alinhamento topCenter para exibir a arte no topo da carta. Com partner, fundo dividido 50/50 entre os dois comandantes.

v1.2.0 — 28/02/2026

  • Melhoria: Seleção manual do jogador inicial — durante o pré-início cada PlayerArea exibe um ícone de coroa (workspace_premium). Toque na coroa define aquele jogador como inicial; coroa preenchida em champagne indica o selecionado, contorno branco suave indica os demais. Ao iniciar a partida a coroa é substituída pelo LandIndicator normalmente. Novo método setStartingPlayer(int playerIndex) no GameNotifier.
  • Melhoria: Redesign visual — paleta Slate + Ouro. Fundo #1A1D23, painéis #21252D, ouro dessaturado #C9A84C (champagne), verde musgo #1E3D28, cores de mana/jogador dessaturadas, active glow reduzido (borda 1.5 px, sombra com 40% opacidade). Land indicator ativo #5CB85C no lugar do neon #00FF88. Aplicado em todos os arquivos do projeto.

v1.1.1 — 28/02/2026

  • Bug fix: Veneno — jogador não era eliminado automaticamente ao atingir 10 counters de Poison. O spec de v1.1.0 descrevia a regra mas a implementação não aplicava a consequência. Corrigido em adjustCounter: ao atingir 10 veneno, chama _eliminateInState + _checkWinCondition, idêntico ao comportamento de vida zerada.

v1.1.0 — 28/02/2026

  • Bug fix: Dano de comandante — ao reduzir o contador com -, a vida do jogador afetado não era restaurada. Correção computa realDelta = newValue - current (após clamp 0–99) e aplica nos dois sentidos via adjustLife dentro de addCommanderDamage.
  • StormCounter na TopBar, ao lado do relógio de partida: botão ⚡N — toque para incrementar a cada magia conjurada, long-press para resetar; reseta automaticamente no nextTurn.
  • Marcadores de jogador (PlayerCounters) na PlayerArea: bottom sheet com grid +/- para Poison, Energy, Experience, Rad e Ticket. Botão exposure na bottom row mostra total acumulado; campo counters adicionado ao PlayerModel.

[Unreleased] — 19/02/2026

  • Bug fix: Dano de comandante — ao reduzir o contador com -, a vida do jogador afetado não é restaurada. Correção aplica o delta real (após clamp 0–99) nos dois sentidos dentro do método addCommanderDamage, sem necessidade de método novo.
  • Nova funcionalidade: StormCounter na TopBar, ao lado do relógio de partida
    • Um único botão global — qualquer jogador toca para incrementar o contador a cada magia conjurada na mesa
    • Reseta manualmente (long-press) ou automaticamente ao passar o turno do jogador ativo
  • Nova funcionalidade: Marcadores de jogador do Magic na PlayerArea:
    • Poison (veneno) - jogador perde com 10 counters
    • Energy (energia) - recurso persistente entre turnos
    • Experience (experiência) - acumula durante a partida
    • Rad (radiação do set Fallout) - representa níveis de radiação
    • Ticket (do set Unfinity) - usado para mecânicas de sticker
    • Implementar PlayerCounters widget com grid de botões +/-
    • Adicionar campo de marcadores no PlayerModel

v1.0.0 — 18/02/2026

  • Lançamento inicial
  • Suporte a 1–4 jogadores com layouts dedicados e rotação 180° para modo mesa
  • Rastreamento de vida (toque ±1, long-press ±5) e dano de comandante por slot (main/partner)
  • Suporte a partners: até 2 comandantes por jogador
  • Eliminação automática ao zerar vida (sem confirmação) e por dano de comandante (com confirmação)
  • Timers de partida (countdown configurável) e turno (count-up com acúmulo por jogador)
  • Busca de comandantes via API Scryfall com validação de legalidade e identidade de cor
  • Indicador de land por jogador: ativo apenas para o jogador do turno, reseta automaticamente ao passar o turno
  • Fase pré-início com scry, timers pausados e sorteio de jogador inicial
  • Histórico de partidas em memória (vencedor, comandante, turno, tempo)
  • Regras de Conquest: eliminação por vida zerada ou dano de comandante acumulado ≥ limite

Sobre o App

App Flutter para partidas de Commander no formato Conquest. Offline, sem persistência entre sessões, estado reativo via Riverpod. Todos os dados vivem em memória enquanto o app está aberto.

Funcionalidades

  • 1–4 jogadores com layouts dedicados e rotação 180° para modo mesa
  • Vida com toque/long-press (±1/±5) por área do jogador
  • Dano de comandante rastreado individualmente por slot (main e partner separados)
  • Suporte a partners: até 2 comandantes por jogador
  • Eliminação por vida zerada: automática e imediata, sem confirmação
  • Eliminação por dano de comandante: exibe dialog de confirmação antes de eliminar
  • Badge com maior dano recebido e nome do comandante ameaçador
  • Indicador de land play por turno (só jogador ativo)
  • Regra de scry pela distância circular ao jogador inicial
  • Fase pré-início: scry visível, timers pausados, sorteio aleatório ou seleção manual por coroa
  • Countdown de partida configurável + count-up acumulado por turno/jogador
  • Menu in-game (⋮) com reiniciar e voltar ao menu
  • Reiniciar mantém nomes e comandantes, zera vida e dano, sorteia novo início
  • Busca de comandantes via API Scryfall filtrada por regras Conquest (Legendary Creature ou Planeswalker), com preview de arte e identidade de cor
  • Background com arte do comandante por jogador — blur leve, ancorado no topo da carta; com partner, fundo dividido 50/50 entre os dois comandantes
  • Configurações editáveis: vida inicial, limite de dano de comandante, deck mínimo, duração
  • Histórico em memória: vencedor, comandante, jogadores, turno, tempo
  • StormCounter global na TopBar: registra magias conjuradas no turno; reseta automaticamente ao passar o turno (toque = +1, long-press = reset manual)
  • Marcadores de jogador por área: Poison, Energy, Experience, Rad e Ticket com botões ±; Poison ≥ 10 elimina o jogador automaticamente
  • Orientação adaptativa: portrait no menu de configuração, landscape durante a partida — sem depender de virar o dispositivo

Arquitetura

O app segue arquitetura reativa com Riverpod como gerenciador de estado. A navegação principal é reativa: nenhuma tela faz Navigator.push no fluxo principal — o _AppRouter observa o gameProvider e renderiza a tela certa automaticamente. A HistoryScreen é a única exceção, aberta via Navigator.push como overlay.

Models

Model Responsabilidade
GameState Snapshot imutável da partida: jogadores, config, activePlayerIndex, startingPlayerIndex, turnNumber, isStarted, isGameOver, winnerId, stormCount
GameConfig Configurações: playerCount, vida inicial, limite de dano de comandante, deck mínimo, duração da partida, tableMode, startingPlayerIndex (índice inicial antes de qualquer sorteio)
PlayerModel Dados do jogador: vida, commanders (até 2), dano recebido por chave "sourcePlayerId_slot", isEliminated, landPlayedThisTurn, turnTimeAccumulated, counters (poison/energy/experience/rad/ticket)
CommanderModel Nome, imageUrl (nullable — null quando adicionado manualmente sem busca Scryfall) e colorIdentity
MatchRecord Registro imutável de partida encerrada: vencedor, comandante, jogadores, turno, tempo decorrido

Providers

GameNotifierStateNotifier<GameState?> central. Estado null significa que o app está na tela de setup.

Principais métodos:

  • startGame — Inicializa jogadores com vida cheia usando config.startingPlayerIndex como ponto de partida; entra em fase pré-início com timers pausados (o sorteio do jogador inicial é feito depois, por randomizeStartingPlayer)
  • startMatch — Define isStarted: true e inicia os dois timers (partida e turno)
  • restartGame — Reseta vida e dano, preserva nomes e comandantes, sorteia novo início, volta ao pré-início
  • resetGame — Para timers e define state = null, voltando ao SetupScreen
  • adjustLife — Soma delta à vida do jogador; se vida ≤ 0, elimina automaticamente sem confirmação
  • eliminatePlayer — Marca jogador como eliminado e verifica condição de vitória
  • nextTurn — Acumula tempo do turno no jogador ativo, reseta land indicator e stormCount, avança ao próximo jogador vivo, reinicia o timer de turno
  • addCommanderDamage — Atualiza o contador "sourcePlayerId_slot" (clamp 0–99); computa realDelta = newValue - current e aplica via adjustLife nos dois sentidos (positivo = dano, negativo = restaura vida)
  • setCommander — Define ou substitui comandante no slot 0 (main) ou 1 (partner)
  • removeCommander — Remove o comandante de um slot; dano já registrado na chave desse slot permanece
  • randomizeStartingPlayer — Sorteia novo jogador inicial durante fase pré-início (sempre diferente do atual)
  • getScryValue — Retorna distância circular do jogador ao startingPlayerIndex para cálculo do scry
  • toggleLandPlayed — Inverte landPlayedThisTurn; restrito ao jogador ativo, ignorado silenciosamente para os demais
  • incrementStorm — Incrementa stormCount em 1; restrito à partida iniciada e não encerrada
  • resetStorm — Zera stormCount manualmente
  • adjustCounter — Soma delta ao contador do tipo especificado do jogador (clamp 0–999); se counterType == 'poison' e valor resultante ≥ 10, elimina automaticamente sem confirmação (mesmo fluxo de adjustLife)
  • setStartingPlayer — Define manualmente o jogador inicial durante o pré-início; atualiza startingPlayerIndex e activePlayerIndex

MatchTimerNotifier — Countdown da duração da partida. Congelado no pré-início, parado automaticamente no game over.

TurnTimerNotifier — Count-up do turno atual. Reiniciado do zero a cada nextTurn, parado no game over.

HistoryNotifier — Lista em memória de MatchRecord, mais recente primeiro. Alimentado automaticamente ao fim de cada partida. Suporta clearHistory().

Condições de Eliminação

Condição Limite Confirmação Método
Vida zerada life ≤ 0 Não — automático adjustLife
Dano de comandante commanderDamageLimit (configurável) Sim — dialog CommanderDamagePanel._handleAdjust + eliminatePlayer
Poison ≥ 10 (fixo, regra MTG) Não — automático adjustCounter

Em todos os casos, após marcar o jogador como eliminado, _checkWinCondition verifica se restar apenas 1 jogador ativo para encerrar a partida.

Telas

Tela Descrição
SetupScreen Configuração de nomes, comandantes (main + partner por jogador) e GameConfig. Acesso ao histórico via overlay
GameScreen Layout adaptativo 1–4 jogadores com rotação 180° no modo mesa e TopBar fixo
GameOverScreen Exibe vencedor, comandante(s), vida final e tempo acumulado de cada jogador. Botões Reiniciar e Voltar ao Menu
HistoryScreen Lista de partidas registradas com opção de limpar histórico completo

Widgets Principais

Widget Descrição
TopBar Pré-início: ícone de dado (Sortear) + timer estático + botão Iniciar. In-game: menu ⋮ + turno/jogador ativo + countdown com cor dinâmica + StormCounter (toque=+1, long-press=reset) + botão Passar Turno com tempo do turno
PlayerArea Background com arte do comandante (blur sigma 4, topCenter); com partner divide 50/50. LifeCounter, botões ±1/±5, badge de maior dano, botão PlayerCounters, LandIndicator (in-game) / coroa de seleção (pré-início), ScryIndicator
CommanderDamagePanel Bottom sheet com dano por fonte (main e partner separados). Botões ±. Ao atingir o limite, exibe dialog de confirmação antes de eliminar. Rodapé exibe “Maior dano único / limite” do jogador alvo
PlayerCounters Bottom sheet com grid de 5 linhas (Poison, Energy, Experience, Rad, Ticket), cada uma com ícone colorido, valor atual e botões ±. Total acumulado visível na PlayerArea
CommanderSearch Busca Scryfall com debounce de 500 ms, até 5 sugestões pré-filtradas por regra Conquest (Legendary Creature ou Planeswalker), preview de arte e identidade de cor. Fallback manual sem API
LifeCounter Total de vida com estilo adaptado a valores negativos ou de muitos dígitos
ScryIndicator Badge com valor de scry (distância ao jogador inicial). Visível apenas na fase pré-início
LandIndicator Toggle de land play. Clicável apenas pelo jogador ativo; resetado a cada nextTurn

Serviços

ScryfallService — Cliente Dio com headers obrigatórios (User-Agent, Accept: application/json) e timeouts de 5 s. URLs absolutas com Uri.encodeQueryComponent. Dois métodos: autocomplete(query) consulta /cards/search filtrando por tipo Conquest; fetchByName(name) consulta /cards/named?fuzzy= e valida via isValidConquestCommander (Legendary Creature ou Planeswalker, sem restrição de legalidade). Provider: scryfallServiceProvider.

Roteamento

_AppRouter — Widget reativo que observa gameProvider e renderiza SetupScreen (state null), GameScreen (state não-null, isGameOver: false) ou GameOverScreen (isGameOver: true). Também gerencia orientação da tela via SystemChrome.setPreferredOrientations: portrait no setup, landscape no jogo. Histórico usa Navigator.push como overlay sobre o fluxo principal.