← JRPG

SPEC — Mini JRPG (v1)

Esta é a spec entregue a todas as execuções do bench — o contrato que cada combinação de harness e modelo teve que cumprir, sem poder alterá-lo. As diferenças entre os resultados vêm inteiramente de como cada um interpretou e implementou este mesmo documento.

Contrato funcional do jogo. Este arquivo é protegido: não pode ser alterado pelo agente. Toda regra aqui descrita é verificada por docs/tests/e2e/ e/ou pela avaliação humana (RUBRIC.md).

1. Visão geral

Um mini JRPG completo em Phaser 4, com:

O jogo deve ser jogável de ponta a ponta com teclado, com interface completa (menus, barras de HP/MP, log de mensagens) e assets próprios (ver §8).

Resolução 1920×1080. O jogo deve ser visualmente impressionante — apresentação é objetivo de primeira classe, não acabamento opcional, e vale 30 dos 100 pontos da avaliação. Você tem liberdade artística completa (§8.2). Não há persistência: recarregar a página reinicia tudo; "Novo Jogo" sempre parte de estado limpo.

2. Cenas e navegação

CenaEntradaSaída
titlecarregamento da página; sair do game overconfirm em "Novo Jogo" → map
mapnovo jogo; vitória; fugacontato com inimigo → battle
battlecontato no mapavitória/fuga → map; derrota → gameover
gameoverHP do jogador chegar a 0confirmtitle

A tela de título deve ter ao menos a opção "Novo Jogo" com cursor navegável.

3. Controles

Teclado: setas e WASD para mover/navegar; Enter ou Z = confirmar; Esc ou X = cancelar/voltar. A Debug API (§7) injeta as mesmas ações pelo mesmo caminho de input do teclado.

4. Mapa

5. Batalha

5.1 Participantes e dados

5.2 Comandos (menu, nesta ordem fixa)

  1. Atacar — dano físico em 1 inimigo. A seleção de alvo abre SEMPRE (mesmo com 1 inimigo vivo), com o cursor no índice 0.
  2. Habilidadegolpe_forte: custa mp, dano com power da skill. Sem MP suficiente = ação inválida.
  3. Itempocao: cura heal (limitado ao maxHp), consome 1 unidade. Sem unidades = ação inválida.
  4. Defender — até a próxima ação do jogador, dano recebido = max(1, ceil(dano / 2)).
  5. Fugir — sempre bem-sucedida na v1; encerra a batalha e volta ao mapa.

O cursor do menu começa em "Atacar". Na seleção de alvo, o cursor começa no inimigo de índice 0. Ação inválida (sem MP, sem item, alvo morto): nada acontece, o round não avança, uma mensagem entra no log.

5.3 Resolução do round

  1. O jogador escolhe a ação; cada inimigo vivo sempre usa Atacar no jogador.
  2. Prioridade: Defender e Fugir do jogador resolvem antes de qualquer outra ação.
  3. Demais ações resolvem por spd decrescente. Empate jogador × inimigo: jogador primeiro. Empate entre inimigos: ordem do array battle.enemies.
  4. Combatente com HP 0 não age. A batalha termina imediatamente quando um lado é eliminado (ações restantes do round são descartadas).

5.4 Fórmulas (exatas)

variacao(r)        = 0.9 + r * 0.2                      // r ∈ [0,1) vem do RNG (§6)
danoAtaque         = max(1, floor((atk * 2     - defAlvo) * variacao(r)))
danoSkill(power)   = max(1, floor((atk * power - defAlvo) * variacao(r)))
danoDefendendo     = max(1, ceil(danoNormal / 2))       // aplica após a fórmula acima
cura(pocao)        = min(maxHp, hp + heal)
clamp               : HP e MP nunca ficam negativos (mínimo 0)

5.5 Vitória, XP e level up

6. RNG determinístico

Único gerador aleatório do jogo: mulberry32, exatamente:

function mulberry32(a) {
  return function () {
    a |= 0; a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

7. Debug API (contrato)

Assim que o jogo carregar, deve existir window.__game:

window.__game = {
  version: 1,

  // Retorna uma CÓPIA PROFUNDA do estado (mutar o retorno não afeta o jogo).
  getState(): {
    scene: 'title' | 'map' | 'battle' | 'gameover',
    player: {
      x, y, level, xp, hp, maxHp, mp, maxMp, atk, def, spd,
      items: [{ id, qty }], skills: [string]
    },
    map:    null | { id, width, height, enemies: [{ id, x, y, group }] },   // não-nulo em 'map'
    battle: null | { round, defending, log: [string],                       // não-nulo em 'battle'
                     enemies: [{ id, name, hp, maxHp, atk, def, spd }] }
  },

  // Injeta input pelo MESMO caminho do teclado.
  // Resolve quando todo o processamento decorrente terminar (inclusive o round de batalha completo).
  input(action: 'up'|'down'|'left'|'right'|'confirm'|'cancel'): Promise<void>,

  // Atalho programático de batalha (equivalente a navegar o menu).
  // opts: { target?: number, id?: string }  — target: índice em battle.enemies (default 0)
  // cmd: 'attack' | 'skill' | 'item' | 'defend' | 'flee'
  // Resolve ao fim do round (ou da transição de cena). Ação inválida: resolve sem efeito (não rejeita).
  selectCommand(cmd, opts?): Promise<void>,

  setSeed(n: number): void
}

Requisitos: promises resolvem em ≤ 2s (animações podem ser encurtadas quando a ação vem da Debug API); getState() sempre disponível em qualquer cena.

8. Apresentação e assets

8.1 Canvas e renderer (restrição de ambiente)

index.html é protegido e já contém <canvas id="game" width="1920" height="1080">. O jogo deve reusar esse canvas, passando-o em canvas: no GameConfig — não crie um segundo canvas.

Consequência já verificada neste ambiente: ao receber um canvas próprio, o Phaser 4 entra em modo de ambiente customizado e rejeita Phaser.AUTO, falhando o boot com Must set explicit renderType in custom environment. Declare o renderer explicitamente.

Prefira type: Phaser.WEBGL. Phaser.CANVAS também boota, mas filtros e post-processing são WebGL-only — no renderer Canvas você perde bloom, glow, vignette, blur e afins.

Cuide também do dimensionamento: 1920×1080 raramente cabe inteiro na janela de quem avalia. Configure scale (Scale.FIT + autoCenter) para reduzir sem distorcer.

Performance já medida neste ambiente (Chromium headless do Playwright, sem GPU, renderização por software): ~40 fps com ~18 sprites animados + partículas + 1 filtro; ~24 fps numa carga abusiva de 240 sprites. Ou seja, há folga para caprichar. O limite real não é framerate, é o orçamento de 2s da Debug API (§7): quando a ação vier da Debug API, encurte ou pule animações.

8.2 Direção de arte — liberdade total

O objetivo é um jogo visualmente impressionante, não um protótipo funcional pintado.

Você tem liberdade artística completa: escolha o estilo (pixel art, vetorial, neon, aquarela procedural, minimalista, o que você quiser), a paleta, a tipografia, a composição e os efeitos. Não existe estilo prescrito, e o index.html não força mais image-rendering: pixelated — se quiser pixel art, ligue pixelArt: true; se quiser suavizado, deixe desligado.

O que é avaliado (RUBRIC — 30 dos 100 pontos):

8.3 Assets

9. Entregáveis (Definition of Done)

  1. npm test com 100% dos testes passando.
  2. npm run build sem erros.
  3. Jogo completo e jogável de ponta a ponta com teclado (título → mapa → batalhas → level up → game over).
  4. ASSETS_REPORT.md na raiz — sempre obrigatório, mesmo com tudo gerado. Formato:
# Assets Report
## Gerados
- herói — procedural (src/assets/sprites.js), 32×32, pixel art 3 cores
- ...
## Placeholders (se houver)
- fundo de batalha — INSERIR ARQUIVO em src/assets/battle-bg.png, 1920×1080, sugestão: planície ao entardecer
  1. SUMMARY.md na raiz: o que foi feito, decisões de arquitetura, limitações conhecidas.

10. Restrições

11. Checklist de completude jogável

Verificado pelo avaliador jogando de verdade, sem Debug API (RUBRIC — 1 ponto por item):

  1. Tela de título com menu navegável funciona
  2. Movimento e colisão respondem bem no teclado
  3. Encontro por contato inicia batalha corretamente
  4. Os 5 comandos de batalha funcionam (Atacar, Habilidade, Item, Defender, Fugir)
  5. Seleção de alvo funciona na batalha dupla
  6. Barras de HP/MP e log de mensagens visíveis e atualizando
  7. Vitória → volta ao mapa com inimigo removido
  8. Level up perceptível (mensagem/feedback)
  9. Derrota → game over → título
  10. Nenhum erro no console durante a sessão de 10 min

12. Roteiro de avaliação manual

Sessão de 10 minutos, em tela cheia (PROTOCOL §5):

título → mapa → 3 batalhas (incluindo a dupla) → level up → fuga → derrota proposital → voltar ao título.

Capturar screenshots das 4 cenas em resolução nativa.