Churros Fantasy · Kavo · diagnóstico e plano · 19/10/2026

Quase tudo foi construído. O que falha é a ligação entre as partes.

Achamos cerca de 100 problemas. 13 quebram algo que você pediu. Em 11 desses 13, o defeito está entre dois módulos feitos por agentes diferentes: cada parte funciona sozinha, mas uma não entende a outra.

34,6 millinhas de código, escritas hoje em ~7 h por 15 agentes
26testes, só de tempo e combate. Nenhum de movimento, loja, atendente, menus ou som
3–10 sde tela escura ao abrir, sem nenhum “carregando”
0vezes jogado numa placa de vídeo de verdade ou ouvido por alguém

Novo nesta versão: 🎬 abertura · 🧗 parkour · 🎨 arte · 👥 elenco · 🌆 bairro vivo · 🍩 fase 2 · 🎥 câmera · 🎳 física · 🦸 transformação · 📋 seus pedidos

🎬 A tela de abertura nova

O seu logo e a sua música, com as fontes de city pop. O 1º toque liga a música e o logo bate no primeiro tempo forte; o 2º começa o jogo.

Tela de abertura antes do primeiro toque
Antes do 1º toque. Logo, “Tóquio, 1983” em cursiva de neon e “APERTE UMA TECLA”.
Tela de abertura com a música tocando
Com a música. O logo acende com brilho rosa e “ENTER PARA JOGAR” pulsa na batida (164 BPM).

A camada do título é a do jogo de verdade, por cima de um quadro parado da rua de noite (a máquina estava sem memória para rodar o jogo inteiro no navegador de teste). ⚠️ O japonês do logo diz チュロスクエスト (“Churros Quest”).

🎥 Por que você vive ajustando a câmera

A câmera só volta sozinha para as costas do Kavo quando você segura W, que é justamente quando ela já está atrás. Virou com A ou D, o Kavo fica de perfil até você apertar Q/E.

Só teclado, sem Q/E, em velocidade real. Segurando D por 2,4 s, o Kavo corre de lado e a câmera corrige só 6°. Com W+A por 1,25 s para virar, ela fica parada em 45° e não acompanha. Quando ele encosta na fachada, a distância pula de 7,7 m para 5,3 m.
Lanternas vermelhas cobrindo o Kavo
Lanterna na frente. O Kavo some atrás das lanternas vermelhas, que não ficam pontilhadas como as paredes.
Kavo correndo na diagonal com a câmera parada
Virando a esquina. O Kavo vai na diagonal e a câmera continua olhando a rua. A dica “aperte Esc” segue no meio da tela 4 s depois.

As causas, medidas com a câmera real do jogo na rua real

Como deveria ser

Prova de que funciona: um agente aplicou as correções 1 e 2 numa simulação com a câmera real do jogo. Depois de virar 90°, ela voltou para as costas em 0,67–1,03 s, sem andar em círculos, e com 40 m de visão à frente. Com Shift+D, o Kavo correu reto por 23 m. O jogo em si não foi alterado.

🎳 A física que você disse ser o que mais gostou

No vídeo do Threads, o que te pegou foi a jogabilidade e a física. A conversa anterior listou 5 ingredientes. Medimos cada um no código real.

Para chegar no feel do vídeo: ligar o boliche para qualquer corpo acima de ~6 m/s, dividir o empurrão pelo peso de cada inimigo, fazer golpes e corpos empurrarem os objetos da rua, e pôr o pisão e as explosões para empurrar o que estiver em volta.

🧗 Parkour: por que trava e como resolver

A sua reclamação: o Kavo fica preso nos prédios, não sobe direito, não tem física, não é fluido e não funciona como Assassin’s Creed.

Medido na rua real: 6.230 tentativas só com teclado

47%das tentativas tiveram algum problema
0 de 222vezes, pendurado num telhado ou sacada, uma tecla fez o Kavo subir
72%dos teleportes perto de parede terminam com o Kavo dentro dela
86%das subidas para o telhado vizinho mais alto falham

Por que trava (confirmado no código)

Como os especialistas resolvem

Existe biblioteca pronta? Não existe uma biblioteca de parkour estilo Assassin’s Creed para navegador. As peças existem prontas: o controlador do Jolt (para não travar), as animações da Quaternius e o three-mesh-bvh, que já está no jogo, para achar as bordas. O parkour se monta em cima delas.

🦸 O hominho que vira Kavo

Você decidiu: o personagem começa como um rapaz comum, se transforma, e só depois da transformação ganha os superpoderes. Abaixo, como os jogos e o tokusatsu fazem isso, e como fica no Kavo. Os números marcados como proposta são meus, para testar jogando.

Quem faz o quê

A transformação

O coração amarelo do peito é o relógio

O que você talvez não pensou

📋 Seus pedidos: o que foi feito, mal feito ou ficou de fora

Um agente conferiu, no código, 248 pedidos e promessas: o que você pediu com as suas palavras e tudo o que o Claude anterior prometeu nos planos 2.0, 3.0 e 4.0.

135✅ feitos e funcionando
51⚠️ feitos mal ou pela metade
12🔌 existem no código, mas nenhuma tecla liga
50❌ não feitos

Dos 44 pedidos feitos com as suas palavras, 31 estão no jogo. O buraco está nas promessas: cerca de um terço do que o Claude anterior recomendou não existe ou não está ligado.

Pedidos seus que ficaram mal feitos ou de fora

Promessas dos planos que não viraram jogo

O que um jogador novo vê

Joguei o build real como se fosse a primeira vez, em 1280×720. Os prints são da partida, sem retoque.

Tela de título do Kavo sobre a rua de Tóquio
Título. A rua já impressiona, mas o texto só promete luta: não fala da 7-Eleven, da atendente nem do parkour. Sem acesso ao Google, “KAVO” sai na fonte comum, como aqui.
Primeiro quadro de jogo com a dica de Esc
Primeiro quadro. A única orientação é “aperte Esc para ver a lista de golpes”. Não diz para onde ir nem o que fazer.
Kavo andando com a dica cobrindo o chão
2 s andando. A dica fica no meio da tela, justo onde o Kavo pisa. A 7-Eleven aparece ao fundo, à esquerda, sem nada dizendo que dá para entrar.
Placar mostra onda 1 sem nenhum inimigo na tela
Onda 1. O placar avisa, mas nenhum inimigo aparece na tela. Eles nascem a ~75 m, e não há seta mostrando de onde vêm.
6 s de combo, em velocidade real. Mira travada, J J J J, energia, lançador, golpes no ar e super. O vídeo é mudo: o navegador de teste não grava som.
Onomatopeia cobrindo o inimigo no momento do golpe
Cada golpe tapa o alvo. A onomatopeia (BAM!, WHAM!, ZAP!) cai bem em cima do inimigo, e ele some. Com o CRT por cima, nem a palavra dá para ler.
Câmera colada na parede com nuvem rosa cobrindo a tela
Fim do combo de 16 golpes. A câmera fica colada numa porta de aço e uma nuvem rosa cobre a tela inteira. Não se vê o Kavo nem o inimigo.
Câmera colada nas costas do Kavo na calçada, com a 7-Eleven à direita
Calçada da 7-Eleven. Perto das fachadas, a parede puxa a câmera para tão perto que o Kavo cobre metade da tela. A loja fica espremida à direita.
Kavo no balcão da 7-Eleven
Balcão. Por dentro, a loja está ótima. Só que o “I conversar” aparece para alguém que não se vê: o Kavo tapa a atendente, ou ela se escondeu porque a onda está ativa. E duas caixas de texto se empilham no meio da tela.
Diálogo com a Sra. Hoshino, com a cabeça do Kavo tapando a cena
Conversa. A câmera fica atrás da cabeça, e as antenas tapam a Sra. Hoshino (pendência nº 4). Ela só aparece no retrato da caixa de texto.

13 coisas que quebram

provado = um agente rodou o código real do jogo e mediu. conferido = eu mesmo li o código e confirmei.

🥊 Combate

🦗 Movimento

🏪 Loja e progresso

⏳ Abrir o jogo

Por que isso aconteceu

Não é desleixo de um agente. É o método de dividir o trabalho.

  1. Cada agente era dono dos seus arquivos

    Os 15 trabalharam em paralelo, cada um testando a sua parte sozinha, com peças falsas no lugar das dos outros. O teste do combate, por exemplo, usa um inimigo de mentira.

  2. As partes se falam por nomes soltos

    coffee de um lado, energyGain do outro. Limite 12 de um lado, 120 do outro. O verificador de código não pega esse tipo de erro.

  3. Os pedidos entre agentes ficaram num papel

    Ficaram anotados em docs/wave2-requests/, e parte deles nunca foi feita.

  4. A junção levou ~1 hora, e o teste final é raso

    Ele só confere que o jogo abre, que aparece uma onda e que um golpe acerta. Ninguém jogou o conjunto de ponta a ponta.

O que atrapalha a diversão

Não chega a quebrar, mas tira a graça. Está agrupado pelo momento do jogador.

1 · Abrir

2 · Aprender

3 · Lutar

4 · Explorar e comprar

5 · Voltar outro dia

Fase 1 · Como deixar o jogo de hoje melhor

Nada do que você pediu sai do jogo. O que muda é a ordem: consertar → câmera → parkour → física de grupo → transformação → abrir rápido → ensinar → fechar o ciclo → impacto → testar de verdade → link.

  1. 🔧 Consertar as 13 falhas e testar com as peças reais

    Cada correção é pequena; a maioria mexe em 1 a 10 linhas. O ganho maior vem de um teste do jogo inteiro: comprar café faz a energia subir 1,5×; lançar permite 5 golpes no ar; esquivar no instante liga a câmera lenta. Os nomes passam a ser fixos no código, para o verificador pegar o próximo coffee contra energyGain.

  2. 🎥 Câmera que você não precisa tocar

    A câmera volta para as costas do Kavo ao virar para qualquer lado. Q/E giram só a câmera. As paredes não empurram a câmera de repente, e com a mira travada há setas para os inimigos fora da tela. A especificação, momento a momento, está na seção da câmera.

  3. 🧗 Parkour que não trava

    Trocar a colisão caseira pelo controlador de personagem do Jolt, que já está no jogo. Subir só quando você pede, com um dono só para o movimento, caminho checado antes de cada salto e animação de verdade. Detalhes na seção do parkour.

  4. 🎳 Física de grupo

    Ligar o “boliche” para que um inimigo arremessado derrube os outros, fazer o peso de cada inimigo contar no empurrão, pôr golpes e corpos para mexer nos objetos da rua, e fazer o pisão acertar o que está embaixo. É o que você mais gostou no vídeo.

  5. 🦸 Hominho → Kavo

    Um personagem novo para o hominho, o henshin com o coração do peito como relógio, e a 7-Eleven como posto de energia. O Kavo atual continua igual. Detalhes na seção da transformação.

  6. ⚡ Abrir rápido

    O “carregando” passa a ser a primeira coisa da página. A física vira um arquivo à parte (905 → 742 KB comprimido), que o navegador prepara em paralelo. As fontes vêm dentro do jogo. Os efeitos são preparados antes do título.

    Nos dados do Poki, o jogador desiste se o carregamento passa de 10 s. Com 10 MB para baixar, só 70% de quem começa a carregar no Brasil chega a jogar. Poki · W3C/Poki

  7. 🎓 Ensinar jogando, na própria rua

    Um verbo por vez, em 4 tempos: aprender, usar num caso mais difícil, uma reviravolta, provar que domina. Com a transformação, a ordem nasce sozinha: o hominho aprende a andar, pular e escalar; a 1ª onda traz o 1º henshin; depois vêm J → pulo duplo → K → L → mira. A lista de golpes do pause fica como está. E um objetivo sempre visível no canto da tela: a missão ativa ou a próxima onda.

    Hayashida (Mario 3D Land) e o kishōtenketsu: Game Developer · Celeste ensina 4 verbos no prólogo · o 2XKO usa J/K/L no PC e mesmo assim confundiu um ex-campeão de Marvel vs Capcom: EventHubs

  8. 🔁 Fechar o ciclo lutar → ganhar ienes → comprar → ficar mais forte

    Ondas com fim e respiro (rodadas e recorde). Uma tabela de preços só. O caixa e a atendente vendendo a mesma lista, com os mangás à venda. Um save só, com “novo jogo”.

  9. 💥 Impacto que se sente sem cansar

    O que os jogadores mais sentem como impacto: hitstop, som coerente e câmera. Avisos de ataque claros, no máximo 2 inimigos atacando ao mesmo tempo, inimigos suavizados entre quadros e uma defesa que quebra. Exagerar nos efeitos piora o jogo: efeito médio funciona melhor do que efeito extremo.

    Estudo de reviews do Steam (2022) · Sakurai sobre hitstop · fichas de ataque do Spider-Man · efeito médio × extremo

  10. 🖥️ Testar de verdade

    Jogar numa placa de vídeo real, ouvir o som e testar num teclado barato. Mira + W + J + Espaço são 4 teclas ao mesmo tempo, e teclados comuns falham a partir de 3. O controle de videogame já está no roadmap.

    Microsoft sobre ghosting · o mesmo problema no DMC5

  11. 🔗 Link que abre sem Claude

    O jogo é um arquivo só, e qualquer hospedagem estática serve: Cloudflare Pages (25 MiB por arquivo, grátis) ou itch.io (aceita um .html sozinho). O bloqueio de rede era do outro ambiente; daqui eu publico normalmente, e esta página é a prova. Antes, vale trocar a marca 7-Eleven pela paródia (brand.ts), porque o link fica público.

    limites da Cloudflare · itch.io HTML5

🍩 Fase 2 · Churros Fantasy, Ato 1

Como li o seu pedido: a fase 1 é o plano acima, aplicado ao jogo de hoje. A fase 2 transforma o Ato 1 do Filme 1 (cenas 1.1 a 1.10) em jogo. Kavo é o protagonista, no lugar do Kenji. Para o povo, o herói é o “gafanhoto de Tóquio” do jornal da cena 1.9, o que mantém a identidade secreta. O roteiro inteiro está guardado, e nada foi mudado nele.

✅ O conflito da armadura está resolvido pelas suas referências. O Kavo continua vermelho, como hoje. No peito, sai o raio e entra o coração amarelo com 木, que é o Hearto Prism e o relógio da transformação. Veja no elenco.

O que o jogo de hoje já entrega para o roteiro

O que acrescentar ao roteiro para virar jogo

Buracos que o roteiro precisa fechar para o Ato 1 funcionar

O que você talvez não pensou

Ordem da fase 2

  1. 🎨 Visual novo

    Sombreado de anime com contorno, cenário Nagai/Sailor Moon e os personagens do elenco. Vem primeiro, para não refazer o resto depois.

  2. 🌙 Uma noite completa na rua que já existe

    Um dia curto na barraquinha (churros → aluguel), a noite com henshin, patrulha e crimes, e a manhã com o jornal.

  3. 🥋 O dojo do Chapurin

    Os golpes passam a vir do treino, e o platô vira o campo de treino.

  4. ☔ O beco, o ancião e o primeiro henshin

    As cenas 1.6 e 1.7 viram a abertura do jogo: o tutorial de pulo.

  5. 🏠 A vila e a escola

    As cenas de dia, com a Chiri e os vizinhos.

  6. 🎤 A live house e o gancho

    As cenas 1.5 e 1.10, curtas, fecham o Ato 1.

  7. 🌆 Bairro vivo

    A 7-Eleven quebra inteira, e a rua ganha pedestres, carros, motos, arcade center e consequências para quem bate em NPC.

🎨 Direção de arte do Churros Fantasy

A sua direção: os cenários seguem Hiroshi Nagai com a ilustração de fundo da Sailor Moon dos anos 90. Personagens e objetos interativos seguem Takashi Murakami, Ardhira Putra e o anime Sailor Moon dos anos 90.

⚠️ Conflito com o jogo de hoje. Hoje o jogo é pixel baixo (~270 linhas) com efeito de TV de tubo. A direção nova pede traço limpo e cor chapada, e o pixel baixo borra justamente o contorno e a cor lisa.

Minha recomendação: sair do pixel baixo e desenhar em resolução cheia, com sombreado de anime (cel shading) e contorno. Da TV de tubo fica só um grão leve de VHS, que você pode desligar. Nada muda no visual até o seu “sim”. O Guilty Gear Xrd trocou sprites por 3D com sombreado de anime para sair do limite de resolução e ganhar câmera dramática nos especiais. Motomura, GDC 2015

👥 O elenco

Primeiro, os personagens com referência sua. As suas imagens valem mais do que as sugestões do roteiro: o Kavo não usa uniforme escolar preto, a Chiri não usa uniforme de marinheiro, e a Aisu é loira, não de cabelo lilás.

Kavo civil
Kavo, o rapaz. Boné vermelho manchado com protetor de orelha, camiseta amarela, suspensório vermelho, jeans claro, munhequeira vermelha e tênis branco de cano alto. As cores já são as da armadura.
Kavo transformado, quatro vistas
Kavo transformado. Vermelho metálico, olhos compostos e antenas de bolinha amarelos, boca prateada, tronco preto, cinto prateado e coração amarelo com 木 no peito.
Chiri civil
Chiri. Maria-chiquinhas com xuxinha vermelha, óculos redondos, cardigã verde, blusa branca com laço vermelho, saia pregueada vermelha, meião branco e sapato boneca.
Magic Chiri
Magic Chiri. Capacete verde com viseira rosa e tiara dourada, maria-chiquinhas para fora, corpete branco com laço laranja, mangas bufantes verdes, saia laranja e verde, e capa branca.
Aisu Kurimo
Aisu Kurimo. Coquinhos loiros, orelhas pontudas, presas no sorriso, jaqueta vermelha de banda marcial com galões dourados, saia pregueada, botas brancas e katana nas costas.
Maria
Maria. Manto azul, véu e vestido brancos, faixa dourada e auréola, sobre nuvens.

Resolve o conflito da armadura: ela fica vermelha, como hoje. A mudança é no peito, que troca o raio pelo coração amarelo com 木. Esse coração é o Hearto Prism e o relógio da transformação. Quando chegar a hora, mostro o antes e depois.

Os que faltavam, desenhados a partir do roteiro

Tudo aqui é sugestão minha, no estilo da direção de arte, e espera o seu ok.

🌆 Bairro vivo, com cara de GTA

O seu pedido: o cenário quebra (a 7-Eleven inteira, com vidros, prateleiras e produtos), os NPCs podem apanhar, e a rua tem gente, carro, moto e arcade center.

Lista completa

Todos os itens, com arquivo e linha de cada um, separados por relatório. Os que mais importam já estão resumidos acima.

Combate, inimigos e câmera

Data: 2026-10-19. Fonte: cópia só de leitura em /tmp/kavo-analise/apps/rubro-arena. Os caminhos abaixo são relativos a essa pasta.

Legenda: 🔴 quebra · 🟠 atrapalha a diversão · 🟡 polimento

Como verifiquei

  • Li inteiros: src/combat/*, src/enemies/{index,enemy,brain,tune,projectile,ragdoll}.ts, src/camera.ts, src/time.ts, src/events.ts e src/main.ts.
  • Li em parte: contracts.ts, input.ts, audio.ts, fx.ts, docs/plano-3.0.md e docs/wave2-requests/{enemies,controls,fx,animation,render}.md.
  • Tipos e testes: npx tsc --noEmit passa limpo e npx vitest run passa 26/26. Os testes do Fighter usam um inimigo falso (src/combat/fighter.test.ts:33-44), por isso não pegam os itens 1, 2 e 5.
  • Prova com as classes reais: rodei a classe Fighter e a classe Enemy reais num script fora do repo, com vite-node e sem editar nada. Os resultados aparecem como Prova.

Pendências do §7 do HANDOFF na minha área

  • #4, câmera de diálogo: ✅ confirmado. camera.ts:638 põe a câmera 2,2 m atrás, 0,8 m à direita e só 0,3 m acima da cabeça. main.ts:61 marca o Kavo com camFade = false, então ele nunca fica pontilhado. Ver item 16.
  • #5, combo depois da Explosão: ✅ confirmado. Achei 3 causas (itens 4 e 1).
  • #10, witch time desacelera o Kavo: ✅ confirmado em time.ts:12-18. Há um problema mais grave por baixo: a esquiva perfeita nunca dispara contra golpe corpo a corpo (item 2).
  • #11, placas mescladas sem dither: ✅ confirmado. A causa é a regra de tamanho em camera.ts:556: uma malha mesclada com mais de 3,5 m de largura vira "parede", não "prop". Solução: pôr userData.camFade = true nas malhas de placa, ou separar as placas do batch.
  • #6, apito do policial: do lado dos inimigos, enemy.ts:1278-1283 só desenha "PIIII!" com fx.word e não emite evento, então o áudio não tem gancho. Falta um emit({ type: 'whistle', pos }) por policial.

Itens, do mais grave ao menos grave

1. 🔴 Combo aéreo cai no 3º golpe: dois limites de juggle em escalas diferentes

  • Onde:

    • lado do inimigo: src/enemies/tune.ts:26 (juggleMax: 12), src/enemies/enemy.ts:302-305 e :405;
    • lado do combate: src/combat/rules.ts:14 (juggleMax: 120) e src/combat/moves.data.ts:51-63 (custos de 4 a 12 por golpe);
    • src/combat/fighter.ts:914 manda juggleCost: d.juggle cru para o inimigo.
  • O que o jogador sente:

    • lança com C+J, sobe com Espaço e faz J J J;
    • no 3º golpe aéreo o inimigo "acorda" e cai em pé;
    • a rota manual do plano-3.0 §2.2 ("J J J → C+J cravada") não fecha;
    • num voador, que já conta como "no ar", isso acontece no 3º jab no chão.
  • Prova (Enemy real):

    Golpe Pontos de juggle Efeito
    lançador 0
    air1 6
    air2 12
    air3 19 (> 12) hitstun cortado para 0,10 s e gravidade ×1,6

    0,1 s depois do air3, teching = true.

  • Correção:

    • deixar uma fonte da verdade só. O combate já decide dropped no planHit, com limite 120;
    • no receiveHit de HitInfo2, o inimigo ignora TE.juggleMax, ou passa a usar TE.juggleMax = 120;
    • criar um teste com o Enemy real.

2. 🔴 A esquiva perfeita (witch time) nunca dispara contra golpe corpo a corpo

  • Onde:
    • enemy.ts:1169 faz if (!pl.invulnerable) pl.fighter.hurt(...); o mesmo padrão aparece em enemy.ts:1199, enemy.ts:1241 e enemies/index.ts:487;
    • main.ts:101 passa invulnerable: fighter.invulnerable;
    • fighter.ts:181 e fighter.ts:1073 deixam a esquiva invulnerável nos primeiros 0,25 s;
    • a checagem da esquiva perfeita fica dentro de hurt(), em fighter.ts:1224.
  • O que o jogador sente:
    • esquiva no último instante e nada acontece: sem câmera lenta e sem contra-ataque;
    • a janela perfeita (0,12 s) cabe inteira dentro da invulnerabilidade, então o inimigo nem chama hurt();
    • só funciona contra tiros, e apenas pela checagem do início da esquiva (fighter.ts:1095-1100).
  • Prova: nos 8 primeiros frames de uma esquiva, invulnerable === true em todos.
  • Correção:
    • os inimigos chamam fighter.hurt() sempre; ele já testa, nessa ordem, parry → esquiva perfeita → invulnerável;
    • tirar o !pl.invulnerable dos 4 lugares;
    • no tiro, decidir o missed pelo retorno de hurt().

3. 🔴 Itens de buff não fazem nada: cada módulo usa um id diferente

  • Onde:
    • o combate lê 'coffee' e 'isotonic' (fighter.ts:188, fighter.ts:448, fighter.ts:792, fighter.ts:928);
    • máquina de venda e caixa mandam 'energyGain' e 'dodgeRecover' (src/interact/items.ts:9-11, :41, :43, :52-53);
    • a Sra. Hoshino manda 'cafe', 'isotonico' e 'oden' (src/npc/index.ts:46-48).
  • O que o jogador sente:
    • paga ¥130–160 por "energia ×1,5" ou "esquiva mais rápida" e o efeito nunca entra;
    • o oden da atendente cura, mas o buff dele também é ignorado.
  • Correção:
    • criar type BuffId = 'energyGain' | 'dodgeRecover' | 'oden' em contracts.ts e tipar Buff.id: BuffId;
    • trocar as leituras no fighter;
    • a partir daí, o tsc pega o próximo erro desse tipo.

4. 🔴 Explosão Kavo ("repete") trava o Kavo 0,6 s e não devolve os quiques (causa do #5)

  • Onde:
    • fighter.ts:1428-1445: burstWave nunca marca c.landed;
    • moves.data.ts:76: a Explosão é tier 3 com cancel: [], e moves.data.ts:94 (canCancel) não tem tier acima de 3;
    • fighter.ts:331: o atalho do turbo exige c.landed;
    • fighter.ts:1421 zera só o estado do lado do combate;
    • fighter.ts:895-897 reimporta e.limits com ||= no golpe seguinte;
    • enemy.ts:299-302 recusa o 2º quique com base no próprio limits, que ninguém zera.
  • O que o jogador sente:
    • aperta J+K+L no meio do combo e a vítima fica parada ("repete");
    • mas o Kavo não consegue bater por 0,6 s, e nesse tempo o hitstun dela acaba (30 frames, já encurtados pelo combo);
    • mesmo quando conecta, a 2ª cravada vira knockdown.
  • Prova: depois da Explosão, o primeiro J só sai aos 0,600 s.
  • Correção:
    • em burstWave, this.cur.landed = true quando algum inimigo for atingido;
    • criar refreshLimits() no Enemy (no contrato IEnemy2) e chamar junto com o do combate;
    • dar à vítima mantida um hitstun fixo de pelo menos 0,7 s, sem decaimento.

5. 🔴 Golpe que não conecta conta como acerto: combo, energia e efeitos falsos

  • Onde:
    • fighter.ts:918 ignora o retorno de receiveHit;
    • fighter.ts:845 marca landed;
    • fighter.ts:926-929 soma combo, energia e hitstop;
    • do lado do inimigo, enemy.ts:262 (levantando, invulnerável) e enemy.ts:267-269 (caído, sem OTG) retornam false;
    • o pedido está em docs/wave2-requests/enemies.md §2. O contrato já foi trocado (contracts.ts:432), mas o combate nunca usou.
  • O que o jogador sente:
    • bater num inimigo caído ou levantando dá faísca, "POW!", tremida, combo e barra, mas ele não reage;
    • dá para "farmar" energia num corpo no chão, +3 por golpe, e chegar à Explosão sem lutar.
  • Prova: com o inimigo recusando todos os golpes, 4 J deram combo 4 e energia 40 → 52.
  • Correção:
    • no strike(): const ok = e.receiveHit(h) !== false; if (!ok) return false;;
    • checkHits só marca landed quando ok;
    • num golpe que não conecta, tocar um "tuc" seco, sem hitstop.

6. 🔴 L logo depois de acertar vira Vanish, e não dá para aparar no meio do combo

  • Onde:
    • fighter.ts:365-367: o decide() testa o Vanish antes da guarda;
    • fighter.ts:1115-1120: janela de 0,35 s, com pelo menos 1 barra;
    • fighter.ts:1223: o parry exige estar em guard.
  • O que o jogador sente:
    • no meio do combo vem um soco de outro inimigo e ele aperta L para aparar;
    • o Kavo gasta uma barra, teleporta para trás da vítima e leva o soco;
    • quanto mais barra, pior a defesa. É o contrário do Arkham, onde o contra funciona a qualquer hora.
  • Prova: jab acerta e L vem 2 frames depois. Resultado: ação vanish, energia 40 → 10, e o golpe de 9 entra (HP 100 → 91).
  • Correção:
    • se algum inimigo estiver com attackImminent (já existe, enemy.ts:196-201) ou houver um tiro chegando, L vira guarda/parry;
    • o Vanish só sai com o caminho livre. Outra opção: mover o Vanish para L + direção para a frente.

7. 🟠 Golpes com custo saem de graça com a barra vazia

  • Onde: fighter.ts:465-468. Com if (this.energy < def.cost) { this.empty(); }, o golpe começa assim mesmo.
  • O que o jogador sente:
    • investida (frente+K), gancho anti-aéreo (C+K), Tatsumaki e Palma Hoshino funcionam com a barra zerada;
    • só toca o som de "vazio", e a barra perde o sentido.
  • Prova: com energia 0, frente+K → ação kiRush.
  • Correção:
    • checar o custo em decide() antes do start(), como já é feito com o super, e devolver false;
    • manter o empty().

8. 🟠 O agarrão do Bruto não segura o Kavo e dá dano de longe 0,55 s depois

  • Onde:
    • enemy.ts:1204-1213: marca liftT = 0.55 e escreve "AGARROU!";
    • enemy.ts:1235-1245: stepLift chama hurt(22) sem checar a distância;
    • enemy.ts:1067: o liftT nunca é zerado em receiveHit, toAir ou toRag.
  • O que o jogador sente:
    • lê "AGARROU!", mas continua andando livre;
    • corre 5 m e, meio segundo depois, leva 22 de dano e cai, como se apanhasse do ar;
    • se o Bruto for derrubado durante o agarrão (pelo trem, por exemplo), ao levantar ele "arremessa" o Kavo de onde estiver.
  • Correção:
    • prender de verdade: estado grabbed no Player via IPlayer2 (trava o input, põe o Kavo acima do Bruto com place e libera com uma fuga, como amassar L);
    • no mínimo, checar a distância no fim e zerar liftT em qualquer reação.

9. 🟠 Trem: o Kavo voa para o lado errado e o knockdown é cancelado

  • Onde:
    • main.ts:236 faz player.enterState('fall', hz.speed * 0.4);
    • fighter.ts:1249 vira o Kavo de frente para o golpe;
    • controller.ts:227-229: o enterState zera a ação;
    • no passo seguinte, fighter.ts:227 vê a ação diferente e cancela o knockdown.
  • O que o jogador sente:
    • o trem empurra para a frente e para o lado, mas o Kavo sai voando a 9,6 m/s para TRÁS (contra o trem, rumo ao meio dos trilhos);
    • ainda cai controlável no ar, sem queda nem animação de levantar.
  • Correção:
    • deixar o fighter.hurt() cuidar: ele já põe actionVel = d * 7 e vy;
    • para o trem, dar só o impulso extra: p.actionVel.copy(d).multiplyScalar(hz.speed * 0.4); p.vy = 9, sem enterState.

10. 🟠 A câmera lenta é global, e o último pedido troca a escala de todos

  • Onde:
    • time.ts:12 sempre faz slowScale = scale e slowLeft = max(...);
    • time.ts:15-18 tem uma escala só;
    • as chamadas estão em fighter.ts:537, :930, :1182, :1200, :1317, :1423 e em main.ts:176-179.
  • O que o jogador sente:
    • no witch time e no parry o Kavo também fica lento (0,25–0,3×), e a vantagem some;
    • as chamadas brigam entre si. Parry (0,25× por 0,5 s) seguido do contra (0,5× por 0,2 s) deixa o resto do parry a 0,5×;
    • um K.O. no meio volta tudo para 0,3×, e o ritmo fica errático.
  • Correção:
    • canais de tempo em time.ts: scaleFor('world') e scaleFor('player');
    • o witch time escala só o world (inimigos, projéteis e física);
    • uma pilha de pedidos de câmera lenta, com id e prioridade: vale a menor escala ainda ativa, e não "o último ganha".

11. 🟠 As teclas usam o relógio real, enquanto o jogo usa o relógio de jogo

  • Onde:
    • fighter.ts:240 faz consume(b, 220) em milissegundos reais, e fighter.ts:247 faz pressedWithin('jump', 60);
    • input.ts:238-248 usa performance.now;
    • o comentário de buffer.ts:26 diz que o buffer é em tempo de jogo e congela no hitstop, mas a janela de fora não congela;
    • o hitstop chega a 0,24 s (moves.data.ts:47, super: 0.2 × 1,2 no beat, fighter.ts:910) e a até 0,6 s com C.hitstop = 3.
  • O que o jogador sente:
    • o lançador acerta, o jogo congela 0,15–0,18 s, ele toca Espaço durante o congelamento e o super pulo não sai;
    • o Espaço só conta se cair nos últimos 60 ms ou se ainda estiver apertado;
    • no witch time (um passo a cada ~67 ms reais), um toque rápido de Espaço também se perde.
  • Correção:
    • fazer consume/pressedWithin medirem em tempo de jogo, passando o gameTime do main.ts;
    • ou o fighter ler as teclas a cada frame, mesmo congelado, e guardar no InputBuffer dele, que já é em tempo de jogo.

12. 🟠 A IA trava: ficha de ataque presa e corpo pendurado seguram a onda

  • Onde:
    • enemy.ts:1081-1086: o estado engage corpo a corpo não tem limite de tempo e só larga a ficha com d > 11;
    • brain.ts:117: só inimigo FORA da tela rouba ficha;
    • enemy.ts:661: o ragdoll sem K.O. só levanta com clear < 0.9 e não tem prazo;
    • enemies/index.ts:106: a onda só termina (cleared) com zero corpos na rua.
  • O que o jogador sente:
    • ficha presa: com o Kavo em cima de uma máquina de venda, num degrau ou num telhado a menos de 11 m, o inimigo que pegou a ficha fica parado embaixo para sempre. Nas ondas 1–2 só existe 1 ficha, então ninguém mais ataca e a luta morre;
    • corpo pendurado: um corpo arremessado que enganchar numa sacada ou numa escada de incêndio, com a pelve a mais de 0,9 m do chão, nunca levanta nem some, e a próxima onda nunca vem.
  • Correção:
    • prazo no engage: depois de ~2,5 s sem atacar, dropTokens(); LOST; atkCd = 1, como o atirador já faz em enemy.ts:1267;
    • ragdoll sem K.O. com ragT > 6: teleportar para o chão mais próximo e levantar (ou remover).

13. 🟠 O aviso de ataque não diz "agora" nem "de onde"

  • Onde:
    • src/enemies/look.ts:261-263: o "!" e o visor piscando duram o windup inteiro (0,5–0,9 s);
    • o attackImminent (enemy.ts:196-201) só é lido pela câmera (camera.ts:519);
    • audio.ts:384 toca enemyWindup sem posição, embora o evento traga pos;
    • a janela do parry é de 6 frames (config.ts:18).
  • O que o jogador sente:
    • o "!" aparece meio segundo antes e não muda, então não há sinal do instante certo para o parry de 0,1 s;
    • o atacante fora da tela só se ouve, e o som vem do centro;
    • o agarrão do Bruto, que não dá para defender, tem o mesmo "!" de um jab.
  • Correção:
    • aviso em dois estágios (Sekiro, Arkham): "!" amarelo no windup, depois um flash branco e um "tink" posicional quando attackImminent vira true;
    • cor própria para o que não se defende (agarrão, pisão);
    • seta na borda da tela para quem tem ficha e está fora de vista;
    • s.enemyWindup(t, e.pos) passando pelo Panner3D.

14. 🟠 Os teleportes ignoram paredes (Vanish, super, contra-ataque)

  • Onde:
    • fighter.ts:1151-1161: o blink usa p.place, que não testa colisão (controller.ts:193-196). O comentário do código diz o contrário;
    • o destino do Vanish é 1 m ALÉM do alvo (fighter.ts:1138-1142);
    • os teleportes do super estão em fighter.ts:1337-1340.
  • O que o jogador sente:
    • inimigo encostado na parede (é onde ele termina depois de todo quique na parede) + Vanish = Kavo dentro do prédio, empurrado para o outro lado ou preso;
    • a rota clássica do DBFZ (quique na parede → Vanish) é justamente a que quebra.
  • Correção:
    • antes do place, testar collision.capsuleFree (já existe em collision.ts:261) no destino;
    • se estiver bloqueado, tentar os lados (±90°) e, por último, o nosso lado do alvo.

15. 🟠 O jogo não ensina nem mostra o estado do combate

  • Onde:
    • fighter.ts:163-177 expõe comboLeft, counterReady, turbo e chargeLevel, mas nem main.ts:287-290 nem o HudSource (hud.ts:7-21) leem esses campos;
    • o jogo inteiro tem 2 dicas (main.ts:141 e :153), embora src/ui/index.ts:17 documente ui.hint('launcher', ...);
    • são 17 golpes em 3 botões (MOVE_LIST em moves.data.ts), e só o L tem 5 significados.
  • O que o jogador sente:
    • não sabe quando o contra-ataque está pronto (tem 1 s para apertar J depois do parry);
    • não sabe quanto falta do turbo nem quando o combo vai cair;
    • não sabe que Vanish, pisão no caído e investida existem; tudo fica escondido no Esc.
  • Correção:
    • HUD mínimo, no espírito "Apple": barra fina do combo (comboLeft), um "J!" de 1 s quando counterReady e o anel do turbo em volta da barra;
    • dicas de uma vez só, ligadas a eventos:
      • 1º lançador que acerta → "Espaço: super pulo";
      • 1º inimigo caído → "J: pisão";
      • 1ª barra cheia → "L depois de acertar: Vanish".

16. 🟠 Câmera de diálogo: as antenas tapam a atendente (#4)

  • Onde:
    • camera.ts:638 põe a câmera 2,2 m atrás, 0,8 m à direita e só 0,3 m acima de uma cabeça a 1,45 m;
    • main.ts:61 faz rig.root.userData.camFade = false;
    • o ponto de mira fica só 0,2 m para o lado (camera.ts:646).
  • O que o jogador sente: na conversa com a Sra. Hoshino, a cabeça e as antenas do Kavo ficam na frente do rosto dela.
  • Correção:
    • plano por cima do ombro, mais aberto e mais alto: d*1.6 + right*1.15 + (0, 0.55, 0), com a mira 0,45 m para o lado dela;
    • ou, só no modo dialog, pontilhar a cabeça e as antenas do Kavo (setCamFade no grupo da cabeça).

17. 🟠 Inimigos não são interpolados e tremem quando a câmera se mexe

  • Onde:
    • main.ts:243-245 interpola só o player (prevPos → pos);
    • enemy.ts:1465 sincroniza a malha com o rag.p do último passo;
    • enemies/index.ts:276.
  • O que o jogador sente:
    • em monitor de 120/144 Hz, ou nos frames com 0 ou 2 passos, o Kavo e a câmera andam lisos e os inimigos andam aos trancos;
    • com a mira travada, girando em volta do alvo, isso aparece mais.
  • Correção:
    • guardar rag.prev no começo do passo;
    • no render, misturar prev → p com o mesmo alpha = acc/DT antes do syncEnemyMesh;
    • fazer o mesmo com projéteis e capas.

18. 🟠 Guarda infinita: não quebra, não mata e ainda enche a barra

  • Onde:
    • fighter.ts:1229-1240: dano de raspão de 15% com piso de 1 HP (Math.max(1, ...)), +2 de energia por golpe bloqueado e nenhum medidor de guarda;
    • fighter.ts:1172-1193: aparar um TIRO faz cambalear um inimigo qualquer a até 6 m (minDot -1.01), e o tiro segue em frente.
  • O que o jogador sente:
    • segurar L de frente para o atacante deixa o Kavo imortal, e ainda carrega a barra. Nas ondas 1–2 só um inimigo ataca por vez;
    • aparar um tiro faz um inimigo aleatório cambalear, em vez de devolver o tiro.
  • Correção:
    • medidor de guarda no estilo do Sifu: quebra em ~4 golpes e deixa 1 s de atordoamento;
    • o raspão pode matar, ou pelo menos não gera energia;
    • aparar um projétil = pr.reflect(), que já existe em projectile.ts:29.

19. 🟠 Curva das ondas: pico na onda 4, chefe da onda 8 mais fraco e platô depois da 8

  • Onde: enemies/index.ts:115-126; a vida está em enemy.ts:160-162 (+3 HP por onda, chefe ×2,5).
  • O que o jogador sente:
    • onda 4: o primeiro Bruto do jogo só aparece aqui, porque floor((w-1)/3) dá 0 na onda 3. E chega junto com um Bruto chefe (≈398 HP, superarmadura), 2 atiradores, 1 voador e 1 capitão, sem nenhum recruta comum;
    • onda 8: o chefe é um Recruta (≈203 HP), metade do chefe anterior;
    • depois da 8: o total trava em 10 inimigos e só sobe +3 HP por onda.
  • Correção:
    • tabela das ondas 1–8 escrita à mão, apresentando cada tipo sozinho primeiro (como o DOOM faz nas arenas);
    • chefes cada vez mais fortes;
    • depois da onda 8, aumentar fichas e agressividade, não só HP.

20. 🟡 O bônus de ritmo é sorte e não tem sinal na tela

  • Onde: src/audio/beat.ts:42-45; janela de ±0,09 s (config.ts:23-24) a 108 BPM, o que dá 32% de chance por acaso; fighter.ts:903-910.
  • O que o jogador sente: 1 golpe em cada 3 sai "no ritmo" sem ele tentar, e sem marcador de batida o bônus parece aleatório.
  • Correção: janela de ±0,06 s e um pulso discreto no HUD a cada batida (por exemplo, a borda da barra de vida), como o Hi-Fi Rush faz.

21. 🟡 Funções prontas e desligadas (código morto)

  • Onde:
    • capa: agarrar a capa (puxar, girar, arremessar, cobrir a cabeça) existe em enemy.ts:545-590 e no contrato (contracts.ts:434-435), mas só o demo chama (src/demos/enemies.ts:227). O startGrab (fighter.ts:960-973) não olha a capa;
    • outros:
      • o evento CANCEL do brain nunca é enviado (brain.ts:15, :39);
      • frozen() (time.ts:21) não é usado;
      • toLegacy e o caminho wave-1 (fighter.ts:90-100, :918) existem sem nenhum inimigo wave-1;
      • FAST tem nomes mortos (main.ts:29: flyKick, diveAtk, special).
  • O que o jogador sente: o HANDOFF §5.5 promete que a capa "pode ser agarrada ou jogada sobre a cabeça", e no jogo isso não existe.
  • Correção: no startGrab, se e.canCapeGrab?.(p.pos), chamar capeGrab(p.pos, intent ? 'throw' : 'cover', dir); apagar o resto.

22. 🟡 Efeitos e sons em dobro

  • Onde:
    • Vanish: fighter.ts:1128-1130 faz burst, afterimage e som; o evento repete tudo em fx.ts:430 e audio.ts:453 (com outra chave de trava, vanish contra vanish-ev);
    • parry: fighter.ts:1184 e fx.ts:425 disparam dois parryFlash;
    • tremor da vítima: acontece duas vezes por golpe, em enemy.ts:412-416 e em fx.ts:386 (via fighter.ts:936);
    • Explosão: fighter.ts:1431-1433 e fx.ts:446 criam duas ondas de choque.
  • O que o jogador sente:
    • Vanish com eco, flashes empilhados e tremidas que se somam;
    • num super de 6 golpes o tremor da câmera satura (camera.ts:241 soma até 1,2).
  • Correção: um dono só por efeito. Regra simples: o combate só emite o evento, e fx e áudio desenham e tocam.

23. 🟡 Bola de energia e raio atravessam paredes

  • Onde:
    • src/combat/projectiles.ts:37-61 não faz raycast no mundo; o tiro do inimigo faz (enemies/index.ts:464);
    • o raio de 30 m (fighter.ts:1374-1408) não tem oclusão.
  • O que o jogador sente: o Kavo acerta inimigos atrás de prédios e da konbini.
  • Correção:
    • usar o mesmo col.raycast(pos, dir, stepLen) dos tiros inimigos;
    • no raio, cortar o to no primeiro acerto de collision.raycast.

24. 🟡 Contexto demais no mesmo botão atrapalha o básico

  • Onde:
    • fighter.ts:436: andar na direção do alvo + K dá investida, nunca bola;
    • fighter.ts:393-394: J×4 sempre lança e sobe sozinho, mesmo cercado;
    • enemy.ts:1177: o golpe inimigo só pega até 1,1 m de altura, então o ar vira zona segura.
  • O que o jogador sente:
    • quem anda para a frente e aperta K (o jeito natural) nunca solta a bola;
    • quem aperta J sem parar vai para o ar a cada 4 golpes;
    • no ar nenhum inimigo corpo a corpo alcança, então o melhor é ficar lá.
  • Correção:
    • investida só com a frente "fresca" (apertada há menos de 150 ms, como no DBFZ) ou com a mira travada;
    • auto-lançador só se não houver outro inimigo com ficha a menos de 4 m;
    • voador e atirador miram primeiro o Kavo quando ele está no ar.

25. 🟡 Custo por passo em pontos quentes

  • Onde:
    • ragdoll: ragdoll.ts:101-105 e :126-130 fazem 15 floorBelow e 15 pushSphere por inimigo por passo, em TODOS os estados, até de pé (enemy.ts:735). Com 14 inimigos são ~420 consultas BVH por passo;
    • alocações:
      • enemy.ts:1312-1425 cria arrays literais for (const j of [...]) por inimigo por passo;
      • enemy.ts:505 cria um _d.clone() por pino da capa por passo;
      • main.ts:101 (getPlayer()) cria um objeto novo por chamada, várias vezes por inimigo por passo;
      • fighter.ts:830 cria um new Vector3 por passo de golpe ativo;
      • camera.ts:622 cria [...camFaded()] a cada frame.
  • O que o jogador sente: nada isolado, mas é o primeiro lugar a perder FPS com a onda cheia mais a polícia, num notebook.
  • Correção:
    • inimigo de pé ou atordoado: raycast só nos pés, e o resto segue o alvo;
    • arrays de índices como const no módulo;
    • getPlayer devolvendo sempre o mesmo objeto, só atualizado.

Ordem sugerida para consertar

  1. Itens 1, 2, 4 e 5: são correções curtas e destravam o combo MvC e o witch time prometidos.
  2. Itens 3, 6 e 7: economia e defesa.
  3. Itens 8 a 12: bugs de IA, tempo e trem.
  4. Itens 13 a 19: leitura do combate e dificuldade.
  5. Itens 20 a 25: o resto.

Para cada um: um teste no vitest com as classes reais Fighter e Enemy (o mock atual esconde os itens 1, 2 e 5).

Movimento e parkour

Escopo: src/player/** (controller, movement/, parkour/, animator, anim/), src/collision.ts, a cápsula do player em src/physics/index.ts, as partes do player em src/main.ts e src/input.ts, os docs de movimento, tests/e2e/movement.mjs + harness.mjs e docs/wave2-requests/*.md.

Método:

  • Li o código inteiro dessas áreas, sem editar nada em /tmp/kavo-analise.
  • npx tsc --noEmit passou limpo e npx vitest run passou 26/26. Nenhum desses 26 testes cobre movimento, parkour ou colisão.
  • Simulei o Player real no Node (Collision + Parkour + controller verdadeiros, sem WebGL), na mesma pista do harness: chão, bloco de 3 m com a face oeste em x=20, mesmo __place.
    • Os scripts estão em .local-state/kavo-analise/agentes/sim-movimento/audit/*.test.ts.
    • Para rodar: copie src/ do app para a pasta, crie o link node_modules, depois npx vitest run.
  • Não rodei o browser. Os números "sim" abaixo saem dessa simulação.

Legenda: 🔴 quebra · 🟠 atrapalha a diversão · 🟡 polimento.


Confirmações e refutações do HANDOFF

  • §5.2, números: confirmados em src/player/movement/params.ts:32-88:
    • corrida 8,6 m/s;
    • gravidade 28,4 m/s², simétrica;
    • pulos 9,9 / 12,3 / 16,3 m/s, mais 0,25·v;
    • mortais 14,6 m/s;
    • pouso pesado acima de 9 m;
    • rampa máxima com normal.y 0,788;
    • sem giro no ar (controller.ts:791-801).
  • §5.2, "altura do pulo longo": confirmada como escolha. longJumpGrav 0.7 (params.ts:65) dá 1,39 m de altura em vez de 0,89 m. A linha do teste fica vermelha para sempre; o alvo precisa ser atualizado (item 24).
  • §5.2 / §7.8, "janela de borda e probe falham por causa do harness": confirmado, com outra causa.
    • A hipótese de que "o swap de colisão não chega ao Parkour" é falsa: harness.mjs:190-199 varre player → pk → collision e troca a colisão do Parkour também.
    • A causa real é o __place, explicado no item 3.
  • §5.3, parkour com bordas dinâmicas, edge assist e motion warp: confirmado.
    • O edge assist funciona. Sim H: Espaço 3 ou 6 frames antes da borda sai exatamente na borda.
    • O coyote também funciona: Espaço até 6 frames depois de sair do chão ainda dá jump1.
    • Problemas novos: o fio ignora a curvatura (item 15) e há scan duplicado (item 23).
  • §7.10, witch time desacelera o Kavo: confirmado.
    • Um relógio só para tudo: main.ts:217-222 faz gdt = clock.tick(rdt) e o acumulador do player.step recebe o tempo escalado.
    • As janelas releasedWithin('crouch', 90) e consume(..., 400) usam tempo real (input.ts:238-253), então elas mudam de tamanho relativo durante o slowmo.

Itens, por gravidade

1. 🔴 Dois sistemas de agarrar borda disputam o mesmo player, e Espaço pendurado no SM64 não sobe

  • Onde:

    • SM64: controller.ts:544-575 (tryLedge, 0,25–1,2 m), states.ts:73, estados ledgeHang/ledgeClimb (states.ts:232-242).
    • Parkour: parkour/index.ts:320-327 (mantle 0,3–1,3 m, hang 1,2–2,4 m).
    • O parkour roda antes (controller.ts:297) e só quando há direção para a parede (index.ts:311).
  • O que o jogador vê: o mesmo pulo contra o mesmo muro cai num de dois "pendurados" diferentes, conforme a altura e se a tecla está segurada. Os controles também são diferentes:

    Pendurado SM64 Pendurado do parkour
    Andar pela borda (shimmy) não tem tem
    Quina e salto entre bordas não tem tem
    Pose uma outra
    Câmera de pendurado não liga (item 17) liga
  • Pior caso: no ledgeHang do SM64, Espaço é capturado pelo parkour antes do estado.

    • tryParkour não exclui ledgeHang (controller.ts:447).
    • plan() trata KIN como chão (index.ts:262) e cai no catLeap (index.ts:290).
    • Resultado: o Kavo faz um "salto de gato" no lugar e troca para o pendurado do parkour. Só o segundo Espaço sobe.
  • Evidência (sim):

    • W segurado: 0,3–1,2 m vira ledgeClimb/ledgeHang do SM64 no frame 0; 1,3–2,3 m vira hang do parkour no frame 3.
    • Sem direção: só SM64, e nada acima de 1,2 m.
    • SM64 pendurado + Espaço: ledgeHang>catLeap>hang, com o pé ainda 2,1 m abaixo do topo. Segundo Espaço: hang>mantle>idle.
  • Correção:

    • Um sistema só. Apagar tryLedge/ledgeHang/ledgeClimb.
    • Deixar o parkour cobrir 0,25–1,2 m caindo, sem exigir direção, como o SM64 (planAir com into || vy<=0).
    • Paliativo imediato: pôr 'ledgeHang' na lista de exclusão de tryParkour (controller.ts:447), para Espaço chegar em states.ts:237.

2. 🔴 Depois de um teleporte, segurar W leva o Kavo na direção errada

  • Onde:
    • controller.ts:195: place() faz latchYaw = facing.
    • O latch está no espaço de yaw da câmera: atrás do player, yaw = facing + π (camera.ts:220).
    • Ele só é refeito quando a tecla de direção muda (controller.ts:369-370).
  • O que o jogador vê: segurando W durante um vanish/blink (fighter.ts:1157 chama place) ou um respawn do killY (controller.ts:338), o Kavo corre ao contrário, ou de lado, até soltar ou trocar a tecla. Com a mira travada não acontece, porque lock() refaz o latch.
  • Evidência (sim latch.test.ts): W = +x antes. Depois de place(+2 m, mesma frente), a intenção vira (−1, 0), o oposto. Com a frente girada 90°, vira (0, 1).
  • Correção: em place(), this.lastDirKey = 0; this.lastCamYaw = NaN; em vez de latchYaw = facing. O próximo passo trava no yaw real da câmera.

3. 🔴 Harness de movimento: a causa raiz das falhas de borda e probe é o __place, não o swap

  • Onde: tests/e2e/harness.mjs:257-262 chama p.place(feet, facing) sem resetVel. Por isso state, grounded, parkour (o PkMove), floorY e o latch vazam de uma linha da tabela para a próxima.
  • Probe:
    • A linha começa "no chão" por causa do estado vazado, então vale o snap de degrau de 0,45 m (params.ts:15, controller.ts:601-608), não a sonda aérea de 0,30 m.
    • Sim: com a semântica do harness o probe mede 0,45 m (FAIL); com place(..., true) mede 0,30 m (PASS).
  • Borda:
    • Quando uma linha termina num hang do parkour, que é contínuo e nunca termina sem input, place() não zera player.parkour. O parkourStep devolve o corpo para o hang antigo.
    • Sim: todas as linhas seguintes "agarram" no frame 0 até 2,8 m (FAIL).
    • Além disso, o latch do item 2 inverte o W em cada linha.
  • Correção no harness:
    • Usar p.place(v, f, true), chamar r.parkour.reset() e forçar o re-latch (item 2).
    • Dividir a linha de borda em duas medidas: agarrar do SM64/mantle 0,25–1,2 m e hang 1,2–2,4 m. Mesmo com o harness certo, o parkour pendura até 2,3 m e a meta 0,25–1,2 falha.
    • Mais barato e confiável: levar essas medidas para o vitest, no Node, como a simulação desta auditoria.

4. 🔴 Vanish/blink atravessa parede

  • Onde:
    • fighter.ts:1133-1142: o destino fica 1 m atrás do alvo, sem checar colisão.
    • fighter.ts:1151-1160: o blink chama p.place(). O comentário diz "collision still applies, unlike place()", mas o ramo com place é sempre o usado.
    • controller.ts:193-204: place() não valida nada.
    • collision.ts:201: a cápsula ignora faces vistas por trás, então quem fica dentro de um sólido não é empurrado para fora.
  • O que o jogador vê: inimigo prensado na parede, que é a rota do combo (quique na parede, depois vanish), põe o Kavo dentro do prédio, com a câmera lá dentro.
  • Correção:
    • Antes do blink, collision.sphereCast(alvo → dest, RAD) e encurtar até hit.distance − RAD.
    • Depois, capsuleFree. Se não couber, cair do lado do Kavo.
    • Ou usar warpTo(dest, 1/60), que integra paredes (controller.ts:391-409).

5. 🔴 Mergulho, barriga e cambalhota estão inalcançáveis, e a lista de golpes promete um "Degrau no inimigo" que não existe

  • Onde:
    • Ninguém chama startDive() (controller.ts:938).
    • diveAtk só existe na tabela wave-1 (contracts.ts:223). O combate wave-2 (src/combat/) não tem nenhuma ocorrência de diveAtk ou startDive.
    • Os estados dive, belly, rollout e getup (states.ts:160-171, 198-204) e os clips correspondentes são código morto.
    • É requisito explícito do dono (HANDOFF §2: "mergulho e deslize de barriga").
    • ui/moves.ts:25 anuncia "Pule de cima de um inimigo para renovar o pulo duplo", mas nada fora do player zera usedDouble no contato com inimigo.
  • Correção:
    • No fighter.attackMove(): J no ar sem alvo a menos de 4 m e com |speed| > 6 chama p.startDive().
    • Implementar o degrau: em enemies.update, pé sobre a cabeça com vy < 0 → p.vy = 9; p.usedDouble = false. Se não for implementar agora, tirar a linha da lista.

6. 🟠 Espaço 1–3 frames antes de encostar na parede gasta o pulo duplo e o wall kick se perde

  • Onde:
    • states.ts:64-67: a prioridade no ar é wall kick (só depois do contato), depois coyote, depois duplo.
    • controller.ts:810-817: canDouble() olha só o chão, não a parede.
  • Evidência (sim kick.test.ts, parede de 10 m):
    • Espaço −3 / −2 / −1 frame: jump4>wallSlide, sem chute.
    • +1 a +8 frames: wallKick.
    • +9: duplo.
    • No SM64 não existe duplo para "comer" o aperto adiantado. Aqui, quem aperta cedo, que é o comum, nunca chuta.
  • Correção: fazer em canDouble() o mesmo que já existe para o chão (doubleMinFloor). Um raio a pés+1,0 m na direção do movimento, com alcance max(0,6, |speed|·0,1); se encontrar parede de frente (|n.y| < 0,3, -f·n > 0,7), devolver false. O aperto fica no buffer de 0,15 s e o chute sai no contato.

7. 🟠 Qualquer pulo rente a uma parede vira wall run lateral

  • Onde:
    • parkour/index.ts:306: dispara com (wantUp || upHeld), e o Espaço ainda está segurado no frame da decolagem.
    • parkour/index.ts:471: aceita ângulo 0°, ou seja, correr paralelo à parede.
  • Evidência (sim wallrun.test.ts):
    • Parede a 0,5 ou 0,8 m ao lado, Espaço tocado 4 frames: jump1>wallRunSide.
    • Segurado: igual.
    • A 1,2 m: pulo normal.
  • O que o jogador vê: correndo pela calçada colado nas fachadas, não dá para fazer o pulo triplo; todo pulo gruda na parede.
  • Correção:
    • Exigir aproximação: componente da velocidade para dentro da parede de pelo menos ~1 m/s, ou ângulo de 10–35°, não 0°.
    • Exigir também um segundo Espaço no ar (wantUp fresco), ou Espaço segurado por 0,2 s depois da decolagem.

8. 🟠 O mesmo Espaço dá 2º pulo ou vault, conforme caia 1 frame antes ou depois do pouso

  • Onde:
    • controller.ts:1024-1028: o aperto bufferizado dispara dentro de landDefault via groundJump e pula o tryParkour.
    • Um aperto depois do contato passa primeiro pelo parkour (controller.ts:297).
  • Evidência (sim G, caixote de 1 m a 1 m do ponto de pouso):
    • Espaço −2/−1 frame: jump2>ledgeClimb.
    • +1/+2 frames: vault>run.
  • Correção: uma regra só.
    • Opção A: no pouso com buffer, chamar pk.plan(..., wantUp=true) antes do groundJump.
    • Opção B: dentro da janela da corrente (chainKind 1/2 e chainWindow), o parkour de chão não pega o Espaço.
    • Para o feel SM64, a B é melhor: a corrente do triplo tem prioridade.

9. 🟠 Pular perto de poste vira giro no poste

  • Onde: parkour/index.ts:600-602. No ar basta dh < 0,85 do eixo e a intenção para frente.
    • Isso é 0,34 m de ar entre a cápsula (0,35) e um poste de 0,16 m.
    • A rua tem 15 postes nas calçadas (street.ts:71-72, z = ±4,2), justamente onde se corre e se briga.
  • Evidência (sim D): poste 0,5 m ao lado da linha de corrida. Espaço em qualquer ponto de 4 a 7 m antes: run>jump>poleSwing.
  • Correção:
    • No ar, dh < RAD + ft.r + 0,1 (~0,6 m), com velocidade fechando no eixo acima de 2 m/s.
    • Não pegar durante jumpKind 3 nem logo após a decolagem (< 0,1 s).

10. 🟠 Correr contra a base de uma rampa íngreme anima corrida a 8,6 m/s parado no lugar

  • Onde:
    • controller.ts:609-611 devolve x/z mas mantém speed.
    • animator.ts:544: quando a velocidade medida é ~0, o animator confia em p.speed e roda o ciclo de corrida, com passos e eventos step (som).
  • Evidência (sim steep.test.ts, 50°): por 2 s, state=run, speed=8.60, deslocamento 0,000 por passo.
  • Correção: nesse ramo, tratar a rampa como parede. Pôr wallN igual à normal horizontal da rampa e hitWall = true, para o wallHit sangrar a velocidade (controller.ts:693-698).

11. 🟠 Objeto erguido ou chutado vira fantasma para sempre

  • Onde:
    • collision.ts:131-137: addDynamic só é chamado no construtor.
    • interact/carry.ts:60, 274: o objeto sai do BVH ao erguer ou chutar e nunca volta depois que assenta.
  • O que o jogador vê: a máquina de venda arremessada assenta, mas o Kavo, o parkour e os raios da câmera passam através dela. Não dá para subir nela.
  • Correção:
    • No settle() do carry: object.updateMatrixWorld(true); ctx.collision.addDynamic(mesh). O bake é por mesh e barato.
    • Expor addDynamic no ICollision.

12. 🟠 Painel de ajustes: valores congelam no localStorage, há pastas mortas e o "Restaurar padrão" restaura só as mortas

  • Onde:
    • tuning.ts:77-82: o localStorage sobrescreve os defaults do código.
    • tuning.ts:118-124: ao mexer em um slider, salva o objeto inteiro. A partir daí, mudar jump1 no código não muda nada naquele navegador.
    • As pastas legadas "Chão / Pulos / Gravidade / Ar" (tuning.ts:5-46, 128-137) não são lidas por ninguém (grep: zero leituras de T.jump1, T.coyoteTime etc.). Têm os mesmos rótulos da pasta real "Movimento" ("1º pulo", "buffer do pulo", "tolerância da beirada").
    • O botão (tuning.ts:142) só restaura T, não as pastas registradas.
    • Faltam no painel: wallKickWindow, wallKickSpeed, chordFrames, runState, airBack, wallSlideSpeed, diveBoost, sidehop*. Há também números mágicos em states.ts (1,6 / 4,5 / 5,5 / 0,14 / 0,32 / 0,36 / 0,18 / 0,42 / −30...).
  • Correção:
    • Salvar só as chaves diferentes do default, com um hash dos defaults como versão; se o hash mudar, descartar.
    • Botão de reset por pasta.
    • Apagar os grupos legados de movimento.
    • Registrar os tempos dos estados.

13. 🟠 Espaço tem ~30 significados e C ~13; o parkour de chão "rouba" pulos a 1,9 m

  • Onde:
    • parkour/index.ts:276: alcance 0,85 + 0,12·v, até 1,9 m.
    • README A: o contexto ganha do pulo.
    • Correndo a 8,6 m/s, qualquer obstáculo de 0,5–3,1 m até 1,9 m à frente transforma o pulo em vault, mantle, catLeap ou wallRunUp. Em briga perto de máquina de venda ou banco, o pulo "some".
  • Lista de golpes incompleta (ui/moves.ts:8-27): faltam mortal lateral, deslize na parede, saltos entre bordas, wall run lateral e o atalho Espaço→C de 3 frames.
  • Benchmarks:
    • AC Unity separa com um modificador segurado (R2) mais "parkour para cima/baixo".
    • DL2 põe tudo no pulo, mas não tem pulo triplo SM64 competindo.
  • Correção:
    • Parkour de chão só quando a intenção aponta para o obstáculo (intent·n < −0,5) e fora da janela da corrente.
    • Encurtar o alcance para ≤ 1,2 m.
    • Completar a lista de golpes.

14. 🟠 Giro lento da câmera com o mouse não muda o W

  • Onde: controller.ts:363-370. Só segue a câmera quando ela gira a mais de 1,9 rad/s.
    • O arrasto do mouse (camera.ts:299, 0,006 rad/px) a 150 px/s dá 0,9 rad/s e é ignorado enquanto a tecla está segurada.
    • Q/E a 2,6 rad/s fica perto do limiar com frames irregulares.
  • O que o jogador vê: girando a câmera devagar enquanto corre, o W deixa de ser "frente da tela".
  • Correção: a câmera expõe manualYawDelta por frame (Q/E + mouse). O latch ignora só o auto-giro e o escape de parede, e passa todo giro manual.

15. 🟡 Fio: o Kavo anda até 0,55 m acima do fio no meio do vão

  • Onde:
    • street.ts:156, 176: o fio visível cai 0,55 / 0,45 m no meio.
    • street.ts:132: guarda points e sag.
    • features.ts:59-61: usa só path[0] e path[last], em linha reta.
    • index.ts:632-637: o equilíbrio segue essa linha.
  • Correção: no stepBalance, a altura vira lerp(a,b,u) − sag·4u(1−u), ou interpolar userData.points. Usar a mesma curva no teste de pegar.

16. 🟡 Quatro limiares diferentes de "chão" geram mantle em telhado íngreme seguido de escorregão

  • Onde:
    • topo de borda: n.y > 0,6 (index.ts:211);
    • pouso do vault: 0,7 (index.ts:357);
    • chão andável: 0,788 (params.ts:86);
    • borda SM64: 0,8 (controller.ts:556).
  • O que o jogador vê: um mantle num telhado de 45° termina em idle, o settle() falha (controller.ts:492) e ele escorrega.
  • Correção: um WALKABLE_NY em params, usado por todos.

17. 🟡 A câmera de pendurado nunca liga no pendurado do SM64

  • Onde: main.ts:270-272. O teste player.state === 'ledgeHang' está dentro do ramo pk ? …, e no pendurado SM64 player.parkour é null. Então o teste está morto e a câmera fica no modo default.
  • Correção: (pk && HANG.has(pk.kind)) || player.state === 'ledgeHang' ? 'hang' : pk ? 'parkour' : …, ou o item 1 resolve.

18. 🟡 Movimentos cinemáticos do parkour atravessam coisas no caminho

  • Onde:
    • vault: o warp puxa até 1,9 m de aproximação sem varredura (index.ts:366-370);
    • ledgeJump: arco reto de até 2,6 m (index.ts:911);
    • tirolesa e wall run: 1 raio só, na altura do quadril (index.ts:941, 1093);
    • roll: 1 raio a 0,5 m (index.ts:665).
  • O que o jogador vê: passa através de poste, placa, toldo e inimigo.
  • Correção: um capsuleFree ou sphereCast por chave do warp no plan. Se falhar, recusar o movimento ou encurtar.

19. 🟡 Física duplicada: cápsula Jolt e cápsula BVH

  • Onde: physics/index.ts:269-289, 333-337 e main.ts:279.
  • Problemas:
    • A cápsula Jolt é cinemática e empurra props com força infinita, mas o controller BVH não enxerga props: atravessa cones e caixas e não sobe neles.
    • setPlayerCapsule recebe HALF constante, então nunca agacha. O controller também mantém a parede até 1,85 m em crouchSlide/belly (params.ts:21).
    • A posição é alimentada uma vez por frame renderizado, mas MoveKinematic roda por tick do Jolt. A 30 fps a velocidade vista pelos props dobra num tick e zera no seguinte.
  • Correção: documentar a cápsula Jolt como "só empurra", passar HALF reduzido em crouch/slide, chamar setPlayerCapsule dentro do loop fixo e escolher a dupla de props que bloqueia o player (caixas grandes no BVH).

20. 🟡 Constantes e tipos duplicados: mudar uma coisa quebra outra em silêncio

  • Constantes:
    • RAD, HEAD, HANG 2.1 e OFF_WALL 0.42 próprios em parkour/index.ts:16-21;
    • hangDepth 2.12 e o offset RAD + 0.05 no SM64 (controller.ts:572);
    • speeds = { walk 3.2, run 8.6, sprint 11.5 } fixo no animator (animator.ts:65), sem ligação com MT.runSpeed, então o pé desliza se a corrida for ajustada.
  • Tipos:
    • três PkMove: parkour/index.ts:67, movement/parkour-link.ts:28 e anim/parkour.ts:166 (este com [k: string]: any);
    • dois ParkourInput;
    • o pedido de docs/wave2-requests/parkour.md para pôr step/landing/edgeDelay/reset no IParkour não foi aplicado, daí o cast as unknown as PkApi (controller.ts:152).
    • Exemplo do desvio: anim/parkour.ts testa typeof m.dir === 'number' (animator.ts:465), mas dir é Vector3.
  • Correção: um body.ts com as medidas da cápsula, importado por params, parkour, animator e main; um único PkMove em contracts.ts.

21. 🟡 Alocações por passo (raios são baratos; o problema é o lixo para o GC)

  • Custo medido (sim cost.test.ts): 5 raios + 4 varreduras de cápsula por passo correndo no plano; 6 + 4 caindo contra muro alto.
  • Alocações:
    • planTagged faz new Vector3 por feature por passo (index.ts:565); com ~60–80 features em Tóquio, são ~4–5 mil objetos/s;
    • player.feet(new Vector3()) (index.ts:260) e fwdOf(..., new Vector3()) (index.ts:268, 304, 562) a cada passo;
    • Collision.raycast clona ponto e normal em todo hit (collision.ts:156), mais o objeto interno do three-mesh-bvh;
    • capsuleContacts cria closures e um objeto literal por BVH por passada (collision.ts:194, 229-233);
    • o animator faz tmpV.clone() a cada frame (animator.ts:737).
  • Correção:
    • scratch vectors;
    • raycastInto(out) sem clone;
    • callbacks de shapecast criados uma vez;
    • pré-filtrar features por grade espacial (só as a menos de 3 m).

22. 🟡 C serve a dois donos: C→J no ar trava o Kavo antes da cravada

  • Onde:
    • main.ts:223-224: o fighter roda antes do player.
    • O player consome C a cada passo (controller.ts:380) e, no ar, C inicia groundPound, cuja entrada zera speed, lat e vy (states.ts:70-72, 209).
    • O fighter só consome C depois de iniciar a cravada (fighter.ts:490).
    • Como C costuma descer 1–3 frames antes do J, o Kavo para no ar antes da cravada. No chão, entra em crouch/crouchSlide por 1–3 frames antes do lançador.
  • Correção: atrasar o pisão em 3 frames (CHORD_MS): marcar poundPendingT e só entrar em groundPound se não vier J/K nesse intervalo.

23. 🟡 Robustez em pontos pequenos

  • controller.ts:439-442: uma exceção no parkour (pkFailed) desliga o parkour pelo resto da sessão, só com um console.error.
  • controller.ts:338 (respawn do killY) não chama pk.reset() nem camRig.snapTo (só o restart faz, main.ts:158-160).
  • controller.ts:533: o ledgeClimb força grounded = true mesmo com settle() falhando, o que dá um pulo de coyote grátis no ar.
  • controller.ts:152 + main.ts:57-59: o construtor do Player cria um Parkour que escaneia todos os meshes e é jogado fora.
  • street.ts:111-115: cada poste vira 2 features (o solid tagueado e o marker), o dobro no loop do item 21.
  • Correção:
    • religar o parkour após N segundos e logar no HUD de dev;
    • respawn com reset completo;
    • grounded = settle();
    • pk opcional no construtor;
    • deduplicar features por path.

24. 🟡 Movimento não tem nenhum teste unitário, e as metas do harness estão desatualizadas

  • Testes: os 26 testes do vitest são de combate e tempo. A simulação desta auditoria mostra que dá para testar o Player no Node em segundos. Hoje a única rede de segurança é o e2e do browser, que tem o bug do item 3.
  • Metas desatualizadas:
    • pulo longo: alvo 0,89 m contra 1,39 m por escolha consciente;
    • janela do wall kick: 0,14 s (params.ts:69) contra ~0,167 s no SM64 (5 frames a 30 fps, conforme docs/movement-audit.md).
  • Correção:
    • criar src/player/movement.test.ts com as cenas desta auditoria: borda, probe, latch, chute, wall run, poste, rampa;
    • pôr no alvo o que foi decidido;
    • abrir a janela do chute para 0,167 s.

25. 🟡 No ar com teclado, a deriva lateral liga e desliga de uma vez

  • Onde: controller.ts:794, 800. lat = −airSide·k·sin(d)·m é atribuído direto, não acumulado; com m < 0,1, lat = 0. Com teclas digitais, D no ar vira 2,4 m/s de lado na hora e solta para zero na hora.
  • Benchmarks: o SM64 tem empurrão analógico. O A Hat in Time dá inércia ao controle aéreo e mira o duplo e o mergulho pela direção.
  • Correção: lat = approach(lat, alvo, 12·dt) (~0,2 s para 2,4 m/s). Manter o "não acumula além de 2,4 m/s".

Feel contra benchmarks (resumo)

Item Kavo hoje Referência
Coyote 0,10 s (params.ts:53). Sim: funciona até +6 frames Celeste usa uma graça curta (Thorson); guias citam 0,05–0,15 s. SM64 não tem
Buffer de pulo 0,15 s por botão, em tempo de jogo (controller.ts:183-186) Celeste: o pulo bufferizado sai no 1º frame no chão. Problema daqui: o buffer pula o parkour (item 8)
Wall kick 0,14 s depois do contato; antes do contato vira duplo (item 6) SM64: 5 frames a 30 fps ≈ 0,167 s (docs/movement-audit.md)
Controle aéreo sem girar; lateral liga/desliga (item 25) SM64 analógico; AHiT com inércia
Parkour para cima Espaço por contexto, até 1,9 m à frente (item 13) AC Unity: modificador segurado + cima/baixo; DL2: tudo no pulo, sem triplo competindo
Leitura da animação 2 poses de pendurado (item 1); corrida no lugar (item 10); mergulho sem gatilho (item 5) —

Ukikipedia devolveu 403 neste ambiente. Os valores do SM64 vêm de docs/movement-audit.md (trechos de Ukikipedia que o agente anterior coletou).

Fontes

Rua, loja, atendente, menus e som

Auditoria de 19/10/2026, feita só lendo código da cópia /tmp/kavo-analise/apps/rubro-arena. Não abri navegador. npx tsc --noEmit passou limpo e npx vitest run deu 26/26, mas nenhum dos 26 testes cobre as áreas deste relatório (NPC, missões, carteira, lojas, input, UI, áudio). Também rodei um script em node (vite-node) que importa brain.ts, items.ts e products.ts para conferir preços e a progressão da amizade. Os números da tabela de preços e da progressão vêm dele.

Legenda: 🔴 quebra (algo prometido não funciona) · 🟠 atrapalha a diversão · 🟡 polimento


Resumo em uma linha

A cidade é bonita e o combate está ligado, mas a economia não funciona como prometido: bebidas de efeito não fazem nada, os mangás e o caixa não podem ser usados, os golpes desbloqueados somem ao recarregar e os preços estão espalhados por 6 lugares. Para quem joga pela primeira vez, não há tutorial nem objetivo visível, e as ondas não param nunca.


Pendências do HANDOFF na minha área: confirmo ou refuto

# / § O que o HANDOFF diz Veredito
§7 #2 NPC via Claude desligado Confirmado, e pior. useCapability depende de window.claude (src/npc/runtime.ts:38-47). No link "sem Claude" que o dono quer (§8), a fala livre nunca vai usar o modelo. Mesmo assim o jogo convida a "Enter fala livre" (src/npc/index.ts:394, src/npc/dialog.ts:246)
§7 #3 Áudio nunca ouvido, fontes nunca testadas Continua igual. As fontes ainda vêm do Google Fonts (index.html:8-10); offline, cai em Arial Black
§7 #4 Câmera do diálogo: as antenas tapam a atendente Confirmado pela geometria (item 14)
§7 #6 Falta o som do apito Confirmado, e pior: além de não existir whistle() em src/audio/world.ts, ninguém chama play('whistle'). Enemy.whistle só desenha "PIIII!" (src/enemies/enemy.ts:1278-1282)
§7 #7 Preços diferentes Confirmado, e pior: são 6 fontes, os efeitos divergem e 4 itens de efeito não fazem nada (itens 1 e 4)
§7 #9 O noren perde as letras Confirmado: ClothOpts não tem map (src/contracts.ts:468) e o noren estático é escondido quando o tecido carrega (src/world/tokyo/index.ts:311)
§7 #11 Placas mescladas sem dither Não reverifiquei (é render/câmera)
§5.4 "Golpes desbloqueáveis: tatsumaki, raio-duplo (mangás)" Refutado no jogo montado: não dá para comprar os mangás (item 2)
§5.9 "Bicicletas, cones e lixeiras também podem ser erguidos" Refutado: só as 2 bicicletas têm userData.pickup (src/world/tokyo/street.ts:274). Cones e lixeiras são corpos de física e não dá para erguê-los
§5.10 Missão "gangue na porta" A gangue não aparece na porta (item 5)
§5.7 "chuva e poças" A chuva começa desligada (rain: 0, src/world/tokyo/index.ts:33) e, ligada, não tem som (item 21)

Achados, em ordem de gravidade

1. 🔴 Café e isotônico não fazem nada, em nenhuma das três lojas

  • Onde: src/interact/items.ts:41,43,52,53 dão os buffs energyGain / dodgeRecover. A atendente usa cafe, isotonico e oden (src/npc/index.ts:46-48). O combate só lê coffee e isotonic (src/combat/fighter.ts:188,448,792,928).
  • O que acontece: o jogador paga ¥130–160, vê o aviso "energia ×1,5 (30 s)" ou "esquiva mais rápida" e nada muda. Os 4 itens de efeito do jogo (Café em lata e Isotônico Sweat na máquina; Café e Isotônico na atendente) são dinheiro jogado fora.
  • Correção: usar os ids coffee e isotonic em items.ts, ou traduzir em Fighter.addBuff. Um teste no vitest, item por item: aplicar o efeito e conferir que fighter.buffs mudou.

2. 🔴 Não dá para usar o caixa: mangás, karaagê e bentô não podem ser comprados

  • Onde: src/interact/index.ts:50-54 marca o caixa como suppressed quando há um NPC a menos de 2,5 m. O caixa fica em (4,4; -8) e a Sra. Hoshino em (5,6; -8), a 1,2 m. Com isso, o prompt fica nulo (src/interact/shop.ts:29) e use não faz nada (shop.ts:31). O estoque dela (src/npc/brain.ts:10-16) só tem onigiri, oden, café, isotônico e onigiri secreto. Os mangás aparecem na estante (src/world/tokyo/konbini/interior.ts:237), mas productSlots não vira nada que se possa usar: só a demo os lê.
  • O que acontece: Tatsumaki e Raio Duplo são impossíveis de aprender, e karaagê e bentô também ficam de fora. O jogador vê o mangá na estante e não consegue fazer nada com ele.
  • Correção: colocar no diálogo dela uma página "Revistas e marmitas" com os itens do caixa (SHOP), vendidos por shopSell. A estante pode virar um objeto de uso, "pegar mangá → leve ao caixa", que abre a conversa já nessa página.

3. 🔴 Os golpes desbloqueados somem ao recarregar, e a Palma Hoshino se perde para sempre

  • Onde: Fighter._unlocked só existe em memória (src/combat/fighter.ts:147,191). kavo-unlocks-v1 é gravado (src/interact/items.ts:114-115), mas nunca é relido para o lutador; só serve para mostrar "já aprendido". A memória da atendente grava taught: true (src/npc/brain.ts:137) e canTeach exige !taught (brain.ts:77).
  • O que acontece: o jogador junta amizade 80 e conclui 2 missões. Pela conta do script: as missões levam a amizade a 55, e o resto vem de mais 13 compras (cerca de ¥1.950) ou de ~13 ondas limpas. Ele aprende o golpe, aperta F5 e perde o golpe, sem nenhum jeito de reaprender.
  • Correção: ao iniciar, em main.ts, chamar fighter.unlockMove para cada id de kavo-unlocks-v1 e, se brain.memory.taught, também para palma-hoshino. Mais um teste de "recarregar mantém o golpe".

4. 🟠 Preços espalhados por 6 lugares, com efeitos diferentes para o mesmo item

  • Onde: src/npc/brain.ts:10-16 (atendente), src/interact/items.ts:38-46 (máquina), :48-57 (caixa), :63-74 (prateleira), src/world/tokyo/konbini/products.ts:234-252 (KONBINI_ITEMS) e cartazes pintados em src/world/tokyo/konbini/textures.ts:62,69,76. O mapa EFFECTS da atendente (src/npc/index.ts:44-50) é código morto: main.ts:128 passa sell: shopSell, que aplica o efeito do caixa (src/interact/shop.ts:87-95).
  • O que acontece: o cartaz diz "COFFEE ¥120" e ela cobra ¥130. O cartaz do oden diz "70円〜" e ela cobra ¥220, mas aplica o efeito do oden do caixa, que custa ¥200. Os nomes também mudam de loja para loja: "Onigiri de salmão" / "Onigiri", "Bebida isotônica" / "Isotônico" / "Isotônico Sweat".
  • Correção: um catálogo único (src/economy/catalog.ts: id, nome, preço, efeito) que todos importam, com os cartazes desenhados a partir dele. Apagar EFFECTS e KONBINI_ITEMS.price. Um teste que falha se o mesmo id tiver 2 preços. (Tabela completa no fim do relatório.)

5. 🟠 "Gangue na porta" não acontece na porta e se completa sozinha se você morrer

  • Onde: src/npc/quests.ts:88 chama c.enemies?.nextWave() sem at, mas a API existe justamente para isso (src/enemies/index.ts:113-115: "quest: gang at the konbini door"). A conclusão em quests.ts:116-121 vale para qualquer en.cleared. restart() (src/main.ts:155-165) roda enemies.clear() e não mexe nas missões.
  • O que acontece: (a) a "gangue" é só a próxima onda comum, que cai nas pontas da rua e ainda soma no contador "ONDA N"; se outra onda estiver viva, as duas se empilham. (b) Basta aceitar, morrer (ou ir em Esc → Recomeçar) e a missão conta como feita: +¥500 e +20 de amizade.
  • Correção: passar SPOTS.konbini.door como at, guardar a referência dos inimigos da missão e concluir só quando esses caírem. Em restart(), chamar clerk.quests.clear() e brain.questFail(ativa). E não avançar o contador global de ondas.

6. 🟠 Ninguém ensina o jogo, e o título não diz o que fazer

  • Onde: o jogo inteiro tem só 3 dicas: Esc (src/main.ts:153), Shift (main.ts:141) e "I conversar" (src/npc/index.ts:394). O título (index.html:131-135) só fala de luta ("lance as sombras para o alto") e mostra 6 teclas escritas à mão, que não acompanham o remapeamento. "APERTE QUALQUER TECLA" não vale para Enter, clique ou tecla sem função: só ação mapeada inicia (src/input.ts:147-155, main.ts:170). No celular nada acontece e nada avisa que precisa de teclado.
  • O que acontece: a primeira onda cai 6 s depois do início (main.ts:152). O jogador não sabe que existem ienes, konbini, atendente nem missões. Para dominar o jogo há 11 teclas, 4 combinações e 33 entradas em 6 abas da lista de golpes (src/ui/moves.ts), mais 3 golpes desbloqueáveis que nem aparecem nela (estão em src/combat/moves.data.ts:120-122 e não chegam à UI).
  • Correção: um primeiro minuto guiado. Título com o objetivo em uma linha ("Aguente as ondas, junte ienes, gaste na konbini, ajude a Sra. Hoshino"). Iniciar com Enter, Espaço ou clique. Teclas do título desenhadas com rich('{attack}'). Dicas na hora certa: J depois do 1º inimigo perto, mira (Shift) com 2 inimigos, C+J depois de 3 acertos, "I comprar" perto da máquina, "¥ = compras" na 1ª moeda.

7. 🟠 O objetivo some da tela: missão só aparece em aviso de 3 s

  • Onde: o HUD (src/hud.ts:7-23) não tem campo de missão. A missão aparece só em avisos curtos (src/npc/quests.ts:79,86,90), o tempo do oden só em avisos aos 60, 30 e 10 s (quests.ts:109) e a gangue não tem marcador.
  • O que acontece: depois que o aviso some, o jogador esquece para onde ia. O oden tem 120 s, mas ele não vê o relógio correndo.
  • Correção: uma linha "MISSÃO" no canto direito (título, tempo e distância em metros), com uma seta na borda da tela quando o alvo estiver fora de vista.

8. 🟠 As ondas não param nunca: a cidade não dá respiro e o jogo não tem fim

  • Onde: src/main.ts:229-231: sempre que a rua fica limpa, a próxima onda vem 5 s depois, sem limite e sem tela de fim. Com inimigo a menos de 9 m, a atendente se esconde e fecha a conversa (src/npc/index.ts:396-407). Não se guarda recorde.
  • O que acontece: comprar, conversar e subir no telhado vira uma corrida contra a próxima onda. Não há meta ("chegar à onda 10"), nem placar, nem recorde. O ciclo "ondas → ienes → curas → ondas" fecha, mas não leva a lugar nenhum.
  • Correção: intervalo maior com contagem no HUD, ou um "sino da rua" para chamar a próxima onda. Pausar as ondas dentro da konbini e durante as missões de telhado. Um chefe na onda 10 com tela de resumo e recorde salvo.

9. 🟠 Golpe perto do balcão conta como agressão à atendente

  • Onde: a atendente não tem hitTest. Vale a esfera padrão de radius × 0,6 = 1,32 m em volta do peito dela (src/interact/index.ts:97, src/npc/index.ts:431). Todo golpe testa objetos de cenário (src/combat/fighter.ts:849-852), mesmo depois de acertar um inimigo. O resultado é amizade −30, 40 s sem conversa e polícia chamada (src/npc/index.ts:364-380).
  • O que acontece: o jogador está no ponto de atendimento, a 1,8 m dela, e aperta J. É a tecla logo abaixo do I. Ou briga com um inimigo que o seguiu até o balcão. Ela foge, fica brava 40 s e chama 2–3 policiais. O jogador não entende o porquê.
  • Correção: um hitTest que só vale atrás do plano do balcão (x > 4,68) e só para golpes que não acertaram inimigo. O 1º toque vira bronca ("Ei! Cuidado!"); a polícia só vem no 2º golpe em até 3 s.

10. 🟠 O trem roda no relógio real e o dano no relógio do jogo; o empurrão aponta para dentro do trem

  • Onde: world.update(rdt, …) (src/main.ts:278) chama crossing.update(dt) (src/world/tokyo/index.ts:369). Já o teste de atropelamento roda no passo de jogo (main.ts:235-236). No acerto, fighter.hurt vira o Kavo de frente para o impacto (src/combat/fighter.ts:1252) e depois enterState('fall', 9,6) apaga a ação e o empurra para a frente (src/player/controller.ts:227-230), ou seja, de volta para o trem.
  • O que acontece: com câmera lenta (esquiva perfeita, último K.O.), hitstop ou pausa, o aviso de 8 s encolhe, porque o trem não desacelera junto com o jogador. Quem pausa no meio dos trilhos durante o aviso pode voltar já sendo atropelado. Ao ser atingido, o Kavo voa em direção ao trem e atravessa os trilhos, em vez de ser jogado para fora.
  • Correção: passar o gdt para a passagem de nível (world.update(rdt, feet, gdt)). Depois de hurt, fazer player.facing = atan2(hz.dir.x, hz.dir.z) antes do enterState, ou aplicar a velocidade em actionVel. Opcional: aviso no HUD quando o jogador estiver a menos de 6 m dos trilhos durante o alarme.

11. 🟠 A música de Tóquio troca para a da arena quando se pisa na passagem de nível

  • Onde: a zona crossing tem ambience: 'crossing' (src/world/tokyo/index.ts:248). main.ts:259 chama audio.ambience('crossing') e, como 'crossing' não está em TOKYO_BEDS (src/audio.ts:65), audio.ts:171-172 liga a trilha "arena". O evento zone também chama ambience, com outra regra (audio.ts:428-432), então são duas chamadas que brigam.
  • O que acontece: cada vez que o jogador cruza os trilhos, onde ficam o trem e a ponta leste da rua, a trilha city-pop vira synthwave de arena no compasso seguinte, e volta ao sair.
  • Correção: trocar ambience: 'crossing' por 'street' (os sinos já vêm do evento train) ou incluir 'crossing' em TOKYO_BEDS. Deixar um único caminho: só main.ts ou só o evento.

12. 🟠 O áudio pode nunca ligar se a primeira tecla for Esc ou se a janela perder o foco

  • Onde: init() guarda a promessa de Tone.start() e devolve sempre a mesma (src/audio.ts:102-118). Ela é chamada a cada onPress (src/main.ts:168), inclusive no Esc e no "pause" falso disparado por blur ou troca de aba (src/input.ts:123-125). Pela regra do navegador, Esc não conta como gesto do usuário.
  • O que acontece: quem aperta Esc no título (a tela mostra "Esc pausa") ou troca de aba enquanto carrega pode ficar a sessão inteira sem som. É provável: no Chrome, resume() fora de um gesto fica pendente, e o código nunca tenta de novo. Clicar com o mouse também não inicia o som.
  • Correção: só guardar a promessa quando getContext().state === 'running'. Chamar Tone.start() em todo keydown que não seja Esc e em pointerdown, e ignorar o caminho de blur/visibilidade.

13. 🟠 Ajustes do painel (`) congelam valores antigos para sempre

  • Onde: rubro-tuning-v1 grava o objeto T inteiro em qualquer mudança e depois sobrepõe os padrões do código (src/tuning.ts:58-67). As pastas dos módulos fazem o mesmo (tuning.ts:77-84,120). "Restaurar padrão" só restaura o grupo de movimento, não Som, Interação, Trem, Rua, Konbini, Câmera… (tuning.ts:142).
  • O que acontece: o dono mexe uma vez num slider e, a partir daí, toda correção de valor que vier no código (pulos, câmera, trem) é ignorada no navegador dele. Ele vai achar que a correção "não pegou".
  • Correção: gravar só as chaves que diferem do padrão, com versão (ou hash do build) na chave. "Restaurar padrão" deve limpar todos os rubro-tuning-*, e o painel deve mostrar um selo "ajustes modificados" quando houver sobrescrita.

14. 🟠 Câmera do diálogo: as antenas do Kavo ficam na frente do rosto dela

  • Onde: src/camera.ts:630-648: a câmera fica 2,2 m atrás e 0,8 m à direita da cabeça (1,45 m), só +0,30 m acima, e mira o rosto dela deslocado −0,2 m para a esquerda (camera.ts:638,646). O alvo é a cabeça dela + 0,35 m (src/npc/index.ts:91,295). Pela conta, a linha de visão passa uns 0,25 m à direita do centro do Kavo, a ~1,78 m de altura, onde ficam as antenas.
  • O que acontece: a conversa principal do jogo é vista com uma antena cruzando o rosto da atendente.
  • Correção: subir a câmera (+0,6 m) e abrir mais para a direita (1,2 m), ou deixar as antenas pontilhadas (camFade) no modo dialog, ou fazer um contraplano de 3/4 vindo de trás do balcão. Conferir com print.

15. 🟡 "Fala livre" promete conversa e entrega um robô de palavras-chave

  • Onde: sem window.claude, freeText cai em scriptedReply (src/npc/script.ts:101-128). O nome capturado é a última palavra da frase (script.ts:108): "me chamo Luciano e quero onigiri" vira "onigiri-san".
  • O que acontece: o jogador é convidado a escrever e recebe "Hm? Fala devagar…" para quase tudo.
  • Correção: se o dono quiser a Hoshino conversando de verdade no link público, usar um Worker com a API do Claude, chave no servidor e limite de uso (nunca OpenAI, pela regra do dono). Até lá, trocar o rótulo para "Enter: escrever (ela entende palavras simples)" e capturar o nome com o grupo do regex, não com pop().

16. 🟡 Sons que faltam ou estão trocados

  • Apito do policial mudo: não existe método em src/audio/world.ts e play() ignora em silêncio (world.ts:65-70). Enemy.whistle nem chama o áudio (src/enemies/enemy.ts:1278-1282).
  • Menus mudos: existem uiMove, uiSelect, uiBack e pause em world.ts, mas a UI (src/ui/*) não toca nenhum.
  • Bipe errado: comprar na máquina toca o bipe do caixa e o barulho da sacola, porque buy é emitido antes de vend coin (src/interact/vending.ts:120,124 contra a regra em src/audio.ts:406-409). No gachapon acontece o mesmo (src/interact/gachapon.ts:131), e a manivela não tem som.
  • Mesmo jingle para tudo: o prêmio da máquina (rouletteWin) toca no jackpot, na missão concluída (audio.ts:440) e no golpe novo (audio.ts:472). Os momentos mais importantes não têm som próprio.
  • Correção: criar whistle() (trinado de 2,8–3,2 kHz, 0,5 s) e chamá-lo em Enemy.whistle. Um gancho de som em ListNav, pushModal e popModal. Emitir buy com a origem (source: 'vending' | 'gacha') e pular o bipe nesses casos. Jingles próprios para missão e golpe.

17. 🟡 As moedas envelhecem em dobro

  • Onde: ix?.wallet.update(DT) em src/main.ts:227, e de novo dentro de Interactions.update (src/interact/index.ts:172).
  • O que acontece: as moedas somem em ~12,5 s em vez de 25 (src/interact/tune.ts:18), e o ímã e a gravidade rodam no dobro da velocidade.
  • Correção: apagar uma das duas chamadas.

18. 🟡 Teclado: rótulos não batem com o layout físico

  • Onde: os rótulos saem de e.code (src/input.ts:54-67). Semicolon vira "Ç" (padrão ABNT2) e Backquote vira "" (padrão US), uma mistura dos dois. O diálogo usa e.key para 1–4 (src/npc/dialog.ts:259). O teste de fantasma desenha um QWERTY fixo (src/ui/pause.ts:39-44). Os menus escutam W/S/A/D/I fixos (src/ui/dom.ts:203-210`).
  • O que acontece: num AZERTY, o movimento funciona (é por posição física), mas a tela diz "W A S D / Q E" onde as teclas são Z Q S D / A E, e escolher 1–4 no diálogo exige Shift. No ABNT2 do dono, o painel "`" é na verdade a tecla ', e quem usa teclado US vê "Ç" no remapeamento.
  • Correção: navigator.keyboard?.getLayoutMap() para desenhar o rótulo real (com e.code de reserva), e.code Digit1–4 e Numpad1–4 no diálogo, e menus que sigam as teclas mapeadas.

19. 🟡 Shift como mira padrão esbarra em recursos do Windows

  • Onde: lock: ['ShiftLeft','ShiftRight'] (src/input.ts:24). O aviso só aparece depois do 5º toque (input.ts:143-146, src/main.ts:141), quando o Windows já abriu a janela de Teclas de Aderência. No modo "segurar", manter o Shift direito pressionado por 8 s abre as Teclas de Filtragem.
  • Correção: mira padrão numa tecla que não seja modificadora (ou no botão direito do mouse), ou modo "alternar" por padrão. O aviso deve vir no 3º toque rápido.

20. 🟡 Quebrar máquina rende mais do que comprar, e dá para repetir com F5

  • Onde: estilhaçar rende ¥100–200 e destruir mais ¥300–800 (src/interact/vending.ts:172,187), multiplicado por 3 máquinas. Elas voltam inteiras a cada recarga, e os ienes ficam salvos (src/interact/wallet.ts:50,153). Uma bebida custa ¥110–160.
  • O que acontece: a melhor "renda" do jogo é quebrar 3 máquinas e apertar F5, o que tira o sentido das ondas e das lojas. A atendente perde só 5 de amizade por máquina.
  • Correção: derrubar ¥30–80 por máquina, ou salvar o estado de quebrada, ou consertá-las depois de N ondas.

21. 🟡 Chuva desligada e muda

  • Onde: rain: 0 por padrão (src/world/tokyo/index.ts:33,98). main.ts:261-266 liga a chuva visual, mas nunca chama audio.setRain (src/audio.ts:177). Poças e reflexo dependem de wet, que fica em zero.
  • O que acontece: chuva e poças, que o dono pediu, só aparecem pelo painel de desenvolvedor, e mesmo assim sem som.
  • Correção: chuva padrão (~0,6) ou um ciclo de clima, chamando audio.setRain(weather.rain > 0,1) quando ela mudar.

22. 🟡 O prêmio da máquina abre um menu do nada no meio da luta

  • Onde: src/interact/vending.ts:318-322: uns 5 s depois da compra, o "ATARI" abre um menu modal, esteja o jogador onde estiver. O jogo pausa no meio da luta.
  • Correção: soltar a 2ª lata sozinha, com o aviso "ATARI!", sem menu; ou só abrir o menu se o jogador ainda estiver ao alcance.

23. 🟡 Inimigos atravessam a porta de vidro fechada

  • Onde: konbini.update(dt, feet) não recebe others (src/world/tokyo/index.ts:370), embora a API aceite (src/world/tokyo/konbini/index.ts:194-199). As folhas da porta não têm colisão.
  • O que acontece: o inimigo passa pelo vidro fechado, e a porta não abre para ele.
  • Correção: passar os pés dos inimigos para konbini.update.

24. 🟡 Progresso espalhado em mais de 10 chaves, sem "novo jogo" e com castigo que some

  • Onde: kavo-yen-v1, kavo-unlocks-v1, kavo-gacha-v1, kavo-npc-hoshino-v1, kavo-bindings-v2, kavo-input-options-v1, kavo-volume-v1, kavo-hints-v1 e rubro-tuning-*. No recarregamento, a amizade vira max(5, salva) (src/npc/brain.ts:131), então bater nela e recarregar apaga o castigo. loadCollection não confere o formato: se a chave guardar "null", Object.keys(null) dispara um erro a cada quadro em prompt() (src/interact/gachapon.ts:22-24,108). Volume salvo sem validação pode virar NaN (src/ui/pause.ts:51). O limite de 3 compras por conversa (brain.ts:30,151) obriga a fechar e reabrir o diálogo.
  • Correção: um objeto kavo-save-v2 com versão e migração, um botão "Apagar progresso" em Opções, a amizade salva como ela está, formato validado na leitura e o limite de compras removido.

25. 🟡 Textos que não seguem as teclas e uma ficção que se contradiz

  • Onde:
    • Teclas escritas à mão: "Espaço · recomeçar" no K.O. (src/hud.ts:61), "I conversar" (src/npc/index.ts:394), "L na hora certa" e "J e L juntos" nas fofocas (src/npc/script.ts:45-50).
    • O título e as ondas falam de "sombras" (hud.ts:60, index.html:133); ela fala de "gangue" e "moleques de capa"; o gachapon, de "inimigos".
    • "Golpe novo: Palma Hoshino" não diz o comando (trás + J).
    • "Barra azul cheia" (script.ts:50), mas a barra cheia fica amarela.
  • Correção: usar rich() com {jump}, {interact} e {defense} em todo texto que cita tecla; escolher um nome para os inimigos; no aviso de golpe novo, incluir o comando e acrescentar a seção "Desbloqueados" na lista de golpes.

Tabela de preços: todas as fontes (saída do script)

Item Máquina (DRINKS) Caixa (SHOP) Prateleira (SHELF) KONBINI_ITEMS Atendente (STOCK) Cartaz na loja
Oden — ¥200 (cura lenta 60/10 s) — ¥120 ¥220 "70円〜"
Café ¥130 "Café em lata" (30 s) ¥150 (60 s) — ¥120 ¥130 ¥120
Isotônico ¥150 "Sweat" (30 s) ¥160 (60 s) — ¥160 ¥160 —
Bentô — ¥480 — ¥550 — —
Chá verde ¥140 (cha-verde) — ¥150 (cha) ¥150 — —
Onigiri — ¥150 — ¥150 ¥150 (fala dela: ¥150)
Karaagê — ¥220 — ¥220 — ¥220
Mangás — ¥500 — ¥500 — —

O que o jogador consegue usar de fato: só os preços da atendente e da máquina, porque o caixa está bloqueado (item 2). E o efeito que a atendente aplica é sempre o do caixa (item 4).


Respostas diretas: o que o jogador de primeira viagem vive

  • Tem tutorial? Não. São 3 dicas no jogo todo; as fofocas da atendente ensinam um pouco (aparar, agarrar), mas só para quem pergunta.
  • O objetivo está claro? Só "ONDA 1 · derrube todas as sombras". Nada fala de ienes, konbini, missões ou golpe secreto.
  • Quantos controles? 11 teclas, 4 combinações e variações por contexto: 33 entradas em 6 abas na lista de golpes, mais 3 desbloqueáveis que não aparecem lá.
  • O HUD mostra o que importa? Mostra vida, energia (3 segmentos), mira, onda, ienes e combo. Faltam missão ativa e tempo, aviso de trem, efeitos ativos e amizade (esta só dentro do diálogo).
  • O ciclo fecha? Só no "luto → ganho ienes → compro cura". Os buffs não funcionam (1), os mangás não podem ser comprados (2), o golpe secreto some (3), as missões acabam em 3 e depois vem "Hoje não tenho nada", o gachapon (10 figurinhas, src/interact/gachapon.ts:13-20) não dá recompensa, e não existe fim nem recorde (8).
  • Idioma: a UI é toda em português e o japonês aparece romanizado com tradução entre parênteses (bom). As onomatopeias em inglês são escolha de estilo. Os problemas são os nomes dos itens e a ficção (itens 4 e 25).

Lacunas de conteúdo e becos sem saída

  • Pessoas: 1 NPC; o Sr. Tanaka só existe durante a missão do oden.
  • Missões: 3, de uma vez só (src/npc/brain.ts:20-24). Depois disso os telhados, as tirolesas e o prédio de 4 andares ficam sem uso.
  • Konbini: ~1.500 produtos que são só física; caixa eletrônico, copiadora, máquina "SEVEN CAFÉ", estufa de frango, oden e revistas são enfeite. Dá para comprar 5 itens.
  • Rua: o botão do semáforo (src/world/tokyo/index.ts:255-262) não serve para nada porque não há carros; as portas com noren do outro lado são caixas sólidas; nas duas pontas há barricadas.
  • Objetos erguíveis: só 2 bicicletas e 3 máquinas.

Marca e risco legal (curto)

src/world/tokyo/konbini/brand.ts:31-41 usa o nome, as cores e o logo da 7-Eleven real, mais "SEVEN CAFÉ" e "7P". A bebida "Isotônico Sweat" (src/interact/items.ts:41) lembra Pocari Sweat. A campainha da porta é original (arpejo de Fmaj7, src/audio/world.ts:168-176), não a da FamilyMart. Para uso interno está ok; antes de um link público (§8), trocar BRAND pela paródia.

O que eu não consegui verificar (precisa de navegador)

  • O enquadramento real da câmera do diálogo.
  • Se os dois telhados das missões são alcançáveis em 120 s.
  • Se os inimigos chegam à konbini pela malha de navegação.
  • O comportamento exato do Chrome com resume() pendente (item 12 marcado como provável).
  • As fontes e o som de ouvido.
Imagem, desempenho e carregamento

Auditoria de leitura do código em /tmp/kavo-analise/apps/rubro-arena (cópia, nada foi editado), em 19/10/2026.

Como medi (e o limite das medidas)

  • Tamanho por dependência: build dividido com sourcemap em /tmp/kavo-analise/build-sm (KAVO_SPLIT=1 vite build --sourcemap). Um script decodifica os mappings e soma os bytes minificados de cada pacote.
  • Draw calls, materiais e luzes: censo sem navegador. O script monta a rua de Tóquio, o Kavo, a Sra. Hoshino, as máquinas e 8 inimigos via vite-node (DOM e canvas falsos) e conta o que o renderer enviaria em cada passe. A contagem não tem frustum culling: é o teto de uma onda cheia com tudo na tela.
  • Tempos de CPU: process.cpuUsage() com o Jolt e o recast reais, numa VM compartilhada de 4 núcleos com load average de 49 a 62. Os valores absolutos ficam inflados. A comparação entre eles vale.
  • Sem navegador e sem GPU: o Chromium estava em uso por outro processo. Tempo de compilação de shader, fps real e memória de GPU são estimativas e estão marcados assim.
  • Scripts em .local-state/kavo-analise/agentes/render-perf-scripts/.

Números de base

O quê Valor
dist/index.html 6.613.370 bytes · gzip 1.804.805
Script inline no <head>, antes do <body> 6,39 M caracteres. 273 caracteres acima de U+00FF (japonês) obrigam string de 16 bits: ≈12,2 MB de RAM só para o texto do script
Draw calls, passe principal (onda de 8, sem culling) 622
Draw calls, passe de sombra 260
Kavo 84 draws (42 peças + 42 cascas de contorno) + 42 de sombra · 7,3 k triângulos
Cada inimigo 37 a 46 draws + 17 a 20 de sombra
3 máquinas de venda + 2 gachapon 114 draws, 78 materiais únicos
Cena de Tóquio 787 objetos, 152 materiais, ~23 programas de shader (estimativa) + 5 de pós + ~15 de FX
Geometria 130 k triângulos · 6,3 MB
Luzes 1 hemisférica, 2 direcionais (1 com sombra 2048²), 6 pontuais
Passes por quadro sombra + cena 480×270 + 13 passes de pós (pixel, brilho, 10 de blur, 2 de downsample) + CRT em resolução cheia = 15 passes de tela cheia
Platô (?mapa=plato) 484 draws, 231 de sombra, 61 k triângulos

Bytes minificados no bundle, por dependência

Pacote KB Observação
jolt-physics (wasm dentro de string JS) 3.468,9 54 % do arquivo
código do app (src/) 753,9
@recast-navigation/wasm (+ core 48 + generators 14 + three 7) 709,3 (+69) wasm em base64 de 451 KB
three (module 346,6 + core 224,1 + examples) 590,8
tone + standardized-audio-context + automation-events 258,3
tweakpane + plugin-essentials (painel de dev) 296,1 sempre carregado e montado no boot
three.quarks + quarks.core 145,7
three-mesh-bvh 46,9
xstate 38,8
stats-gl (só com ?dev) 35,8 entra inline mesmo sem ?dev
gsap, lil-gui, @pixiv/three-vrm, troika-three-text 0 nunca importados

Jolt: arquivo .wasm contra string embutida

bruto gzip brotli
jolt-physics.wasm.wasm + wasm.js (cola) 2.021 + 965 KB 742 + 136 KB 507 + 95 KB
wasm-compat.js (o que o jogo usa) 3.222 KB 905 KB 622 KB

CPU do boot, medido na VM carregada

Etapa ms de CPU
createWorld('tokyo') 654
Jolt: decodificar e compilar o wasm 208 (1.542 ms de relógio)
Jolt addStatic (52 malhas) 118
attachPhysics (10 tecidos + 29 props) 39
navmesh do recast 193
BVH de colisão 64
buildRubro 16
buildInteract 20

Fica fora da tabela: a montagem do Tweakpane e a compilação de shader do primeiro quadro, que só o navegador mede.


Problemas, do mais grave ao menos grave

1. 🔴 Boot sem nenhum retorno: tela escura até o título, e o "CARREGANDO…" nunca aparece

  • Onde:
    • index.html:10: CSS do Google Fonts que bloqueia a renderização e também a execução do script de módulo;
    • no dist, o script inline de 6,4 MB fica no <head>, antes do <body> que contém #loading (index.html:108);
    • main.ts:39-296: main() é síncrono do começo ao fim;
    • main.ts:295: remove #loading no mesmo tick;
    • o 1º requestAnimationFrame compila todos os shaders antes da 1ª pintura.
  • Impacto para o jogador:
    • nada aparece durante o download de 6,6 MB (1,8 MB gzip);
    • depois vêm o parse do script, ~1,3 s de CPU de montagem (medido: mundo 654 ms + navmesh 193 + Jolt 365 + BVH 64…) e a compilação de ~25 programas no 1º quadro (estimativa: 0,5 a 3 s no Windows/ANGLE);
    • num notebook fraco ou no 4G, são 3 a 10 s de tela escura parada. A tela de carregamento existe, mas não consegue pintar.
  • Correção:
    • mover o <script> para o fim do <body>, ou começar o main com await new Promise(r => requestAnimationFrame(() => setTimeout(r))), para o CARREGANDO… pintar;
    • quebrar o boot em etapas com await entre elas e um texto de progresso: mundo, colisão, navmesh, física;
    • antes de mostrar o título, chamar await rp.renderer.compileAsync(scene, cam);
    • carregar as fontes sem bloquear (ver item 13).

2. 🟠 Jolt embutido como string: 3,47 MB, 54 % do jogo, analisado já no boot

  • Onde:
    • src/physics/jolt.ts:1: import initJolt from 'jolt-physics/wasm-compat', estático;
    • main.ts:19 + main.ts:132: o "carrega assíncrono" só adia a instanciação, porque o parse acontece no boot;
    • vite.config.ts:7,11: single-file com assetsInlineLimit de 100 MB.
  • Impacto:
    • +72 % de bytes brutos e +22 % no gzip contra o .wasm (905 contra 742 KB);
    • sem compilação em streaming e sem cache do wasm entre visitas;
    • a string fica retida na memória: o script inteiro vira ~12 MB em UTF-16 por causa de 273 caracteres japoneses;
    • o build:artifact também não resolve: o chunk principal continua com 5.530 KB (1.552 KB gzip) porque o Jolt vai junto.
  • Correção:
    • no build dividido, usar jolt-physics/wasm com o .wasm como asset (?url) e WebAssembly.instantiateStreaming;
    • carregar com import() depois do título;
    • deixar o single-file só como export "offline";
    • o recast (709 KB) tem o mesmo problema: usar recast-navigation com .wasm separado.

3. 🟠 ~900 draw calls por quadro numa onda cheia (622 no principal + 260 de sombra)

  • Onde:
    • src/player/rig.ts:48-52: cada peça é um Mesh com uma casca de contorno filha (anim/look.ts:24-34);
    • src/enemies/look.ts:63-77, 108-125: o mesmo padrão nos inimigos;
    • src/interact/vendingMesh.ts: 114 draws e 78 materiais para 5 objetos.
    • A rua em si está bem agrupada, com ~22 draws.
  • Impacto:
    • com ANGLE/D3D11 num iGPU, cada draw custa ~10 a 20 µs de CPU no three (estimativa). São 9 a 18 ms por quadro só para enviar a cena;
    • em notebook fraco o jogo cai abaixo de 60 fps justo no combate, que é quando a tela enche de inimigos;
    • o fps cai pela CPU, não pela GPU: a cena é só 480×270.
  • Correção:
    • fundir as peças rígidas de cada personagem num SkinnedMesh com pesos rígidos (1 osso por peça): 1 draw + 1 de contorno por personagem;
    • ou tirar as cascas, porque o passe pixel já desenha a tinta da silhouette pela profundidade. Isso exige o antes/depois do npm run test:visual, pela regra do Louie contra regressão;
    • nas máquinas de venda, juntar os canvas num atlas e usar 1 ou 2 materiais por máquina;
    • nos inimigos, InstancedMesh por tipo de peça.

4. 🟠 Engasgo de compilação de shader no primeiro golpe, no primeiro POW!, no primeiro especial e na primeira capa

  • Onde:
    • nenhuma chamada a compile/compileAsync no projeto (grep vazio);
    • as FX são criadas em pool, mas os programas só compilam quando aparecem pela primeira vez:
      • fx.ts:130-146: 4 Points, sprites, speedlines, shocks, beams;
      • fx/bursts.ts:344-363: 4 sistemas quarks;
      • fx/ghost.ts:38;
      • aura, orb, rain;
      • physics/cloth.ts:499: a capa.
  • Impacto:
    • o primeiro impacto da partida congela o jogo por 50 a 300 ms (estimativa ANGLE, sem cache de shader do navegador);
    • acontece justo no momento em que o "game feel" tem que brilhar. O primeiro capitão de capa repete o engasgo.
  • Correção:
    • no boot, fora da tela, disparar uma de cada FX, um inimigo de cada tipo e uma capa;
    • em seguida, await renderer.compileAsync(scene, camera), que usa o KHR_parallel_shader_compile, e esconder tudo.

5. 🟠 Destruir uma máquina de venda recompila todos os materiais iluminados

  • Onde:
    • src/interact/vending.ts:181: this.mesh.light.visible = false;
    • luz pontual criada em vendingMesh.ts:243, uma por máquina, 3 no total.
  • Impacto:
    • o número de luzes pontuais faz parte da chave de programa do three. Ao passar de 6 para 5, todo material toon/lambert em vista (≈15 a 20 programas) recompila no quadro do estouro;
    • a cena mais satisfatória do jogo vira um engasgo de centenas de ms (estimativa), e a 2ª e a 3ª máquina repetem.
  • Correção: light.intensity = 0 no lugar de visible = false. É uma linha, e o rail.ts:169 já faz assim com a luz vermelha.

6. 🟠 O pontilhado da câmera destrói o programa e recompila na próxima vez

  • Onde: src/shaders/fade.ts:36-55. O clone do material é descartado ao voltar ao sólido (:41).
  • Impacto:
    • quando o último objeto pontilhado volta a sólido, o dispose() zera usedTimes e o three apaga o programa camfade;
    • na próxima vez que a câmera passa atrás de um poste ou placa, o programa compila de novo;
    • resultado: micro-engasgo recorrente durante a exploração.
  • Correção:
    • guardar o clone por malha e só trocar mesh.material de volta, sem dispose;
    • ou injetar uCamFade nos materiais originais (padrão 0) e evitar o clone.

7. 🟠 Inimigos, projéteis e atendente não são interpolados: tremem em 75/120/144 Hz e na câmera lenta

  • Onde:
    • só o player é interpolado (main.ts:243);
    • os inimigos sincronizam a malha dentro do passo fixo (src/enemies/index.ts:276).
  • Impacto:
    • em monitor de 144 Hz, os inimigos andam no ritmo 2-3-2-3 quadros enquanto o Kavo e a câmera andam lisos: dá tremedeira visível;
    • em 75 Hz (monitor de escritório) e em 120 Hz (MacBook Pro), a cadência também não bate com a do Kavo;
    • na câmera lenta do último K.O. (main.ts:176-179, 0,3×), o ragdoll é atualizado a ~18 Hz e fica picotado no momento mais cinematográfico.
    • O passo fixo em si está certo, e a proteção contra a espiral da morte existe:
      • rdt ≤ 0,1 s e no máximo 6 passos (main.ts:214,222,239);
      • a física tem o próprio limite de 4 passos (physics/index.ts:312-316).
  • Correção:
    • guardar a pose anterior (rag.p) e chamar enemies.render(alpha = acc / DT) depois do loop, como já é feito com o player;
    • o mesmo para projéteis;
    • opcional: limite de 60 fps nas opções.

8. 🟠 Os 10 tecidos de noren simulam o tempo todo, mesmo longe ou fora da tela

  • Onde:
    • src/world/tokyo/index.ts:289-311: são 5 noren × 2 metades;
    • physics/index.ts:345-349 processa todos os tecidos em todo passo.
  • Medido: 2,86 ms de CPU por passo com os 10 tecidos, contra 0,08 ms sem eles (Jolt real, VM carregada: o absoluto está inflado, a razão de 35× vale). A parte JS da renderização é só 0,095 ms. O custo está no solver de corpo mole.
  • Impacto:
    • de 1 a 3 ms por passo, gastos com panos que o jogador quase nunca vê ao mesmo tempo;
    • as capas de capitão e de chefe somam mais corpos moles na onda;
    • em notebook fraco, isso compete com o combate.
  • Correção:
    • congelar o tecido (sem preStep e com o corpo desativado) quando está a mais de ~15 m da câmera ou fora do frustum;
    • nesse estado, mostrar o staticNoren, que já existe (index.ts:211, 311).

9. 🟠 Cada rastro (afterimage) custa 84 draws, e o pool nasce no primeiro uso

  • Onde: src/fx/ghost.ts:31-50. O filtro só exclui fxOverlay, mas as cascas de contorno usam userData.outline (anim/look.ts:31) e entram.
  • Impacto:
    • no primeiro vanish ou super dash, o jogo cria 4 × 84 = 336 Meshes e compila o shader: engasgo;
    • com os 4 rastros visíveis, são +336 draw calls no momento mais intenso, chegando a ~1.200 draws.
  • Correção:
    • pular userData.outline, o que corta pela metade;
    • montar o pool no boot;
    • ou "assar" a pose numa geometria única por rastro, com 1 draw.

10. 🟠 Sem tratamento de perda de contexto WebGL, e a simulação continua com a tela preta

  • Onde: nenhum webglcontextlost ou webglcontextrestored no projeto (render.ts:193-242, main.ts).
  • Impacto:
    • reset de driver, volta do sleep em iGPU ou excesso de abas: a tela fica preta e os inimigos continuam batendo;
    • não aparece nenhuma mensagem.
  • Correção:
    • no webglcontextlost: preventDefault(), ctx.setPaused('gpu', true) e um aviso na tela;
    • no webglcontextrestored: applySize() e despausar;
    • testar com WEBGL_lose_context.

11. 🟠 Sem pausa automática ao perder o foco, e o áudio e o trem seguem com o jogo parado

  • Onde:
    • src/input.ts:124-125 só solta as teclas;
    • audio.ts não trata visibilitychange;
    • main.ts:278: world.update(rdt, …) roda com o tempo real mesmo com o menu de pausa aberto.
  • Impacto:
    • com o alt-tab e a janela ainda visível, o jogo continua, o Kavo fica parado e apanha;
    • com a aba oculta, o rAF para, mas a trilha e a ambiência continuam;
    • com o menu aberto, o trem, o sinal de pedestre e a porta seguem o relógio. Ao fechar o menu, o trem pode já estar chegando.
  • Correção:
    • no blur e no visibilitychange oculto: pause.open() e Tone.getContext().rawContext.suspend(); ao voltar, resume();
    • passar halted ? 0 : rdt ao world.update.

12. 🟠 Um erro depois do boot congela a imagem sem mensagem

  • Onde:
    • main.ts:303: fatal() procura #loading, que main.ts:295 já removeu, então só sobra o console.error;
    • main.ts:212: o próximo rAF é agendado antes do corpo do quadro.
  • Impacto:
    • a exceção se repete 60× por segundo, rp.render nunca roda e a tela congela no último quadro;
    • parece travado e não dá pista nenhuma (nem para o QA).
  • Correção:
    • envolver o corpo do quadro em try/catch;
    • criar o overlay de erro se ele não existir;
    • parar o loop depois de N erros seguidos.

13. 🟠 Fontes no Google Fonts: bloqueiam a renderização e quebram a promessa de um arquivo que abre em qualquer lugar

  • Onde: index.html:8-10.
  • Impacto:
    • em rede que trava o pedido (proxy corporativo, portal de Wi-Fi, firewall), nada aparece e main() só roda depois do timeout;
    • offline ou com o Google bloqueado, Monoton, Audiowide e Chakra Petch viram Arial Black, e a identidade anos 80 some;
    • o HANDOFF §7.3 confirma que as fontes nunca foram vistas nos testes;
    • os textos do mundo (letreiros, POW!) usam fontes do sistema ou pixel desenhado, então só a UI depende disso.
  • Correção: subset woff2 (latim + dígitos + símbolos usados) embutido em @font-face base64 (~60 a 120 KB no total), ou <link rel="preload" … onload> sem bloquear.

14. 🟠 Ferramentas de dev no pacote e montadas no boot

  • Onde:
    • src/tuning.ts:1-2: Tweakpane com import estático, 296 KB;
    • main.ts:140 → tuning.ts:107-150: monta ~230 bindings em 18 pastas com o painel escondido;
    • render.ts:247: stats-gl entra inline mesmo sem ?dev, +36 KB.
  • Impacto:
    • parse e criação de DOM no caminho crítico do boot, para um painel que o jogador não abre;
    • risco escondido: registerTuning aplica o que estiver salvo no localStorage por cima dos padrões novos (tuning.ts:77-82);
    • um ajuste caro salvo uma vez continua ligado para sempre sem ninguém ver: reflectAll, pixelHeight 540, substeps 4, clothIterations 16.
  • Correção:
    • registerTuning sem importar o Tweakpane;
    • import('./tuning-panel') só na primeira vez que a crase é apertada;
    • chave de versão nos valores salvos e um botão "restaurar" em Esc › Opções.

15. 🟡 Dependências mortas e build duplicado

  • Onde:
    • package.json: gsap (6,4 MB em node_modules), @pixiv/three-vrm (5,9 MB), troika-three-text (972 KB) e lil-gui (256 KB) nunca são importados e somam 0 KB no bundle;
    • scripts/artifact.mjs gera um dist/artifact.html de 6,6 MB que o HANDOFF §8 diz ser recusado.
  • Impacto: install mais pesado, confusão sobre o que é usado e 13 MB em dist.
  • Correção: remover os 4 pacotes e aposentar o artifact.mjs (o caminho bom é o build:artifact).

16. 🟡 Sombra 2048² renderizada todo quadro para uma imagem de 270 linhas

  • Onde: src/world/tokyo/index.ts:143-144 e src/arena.ts:70-71.
  • Medido: 260 draws e 44 k triângulos no passe de sombra, 4,2 M texels de profundidade por quadro. É também a maior alocação de GPU: ~16 MB, contra ~1 MB do alvo da cena.
  • Impacto:
    • um texel de sombra mede 2,9 cm (60 m / 2048), enquanto um pixel da tela a 10 m mede ~3,8 cm;
    • parte da resolução da sombra se perde no downscale, e o custo de preenchimento pesa em iGPU.
  • Correção:
    • 1024² com frustum de ±20 m: mesma densidade e ¼ do custo;
    • opção "baixo" com 512².

17. 🟡 Cadeia de pós com 15 passes de tela cheia, e o CRT em resolução nativa

  • Onde:
    • render.ts:394-414: pixel, brilho, 10 de blur, 2 de downsample;
    • render.ts:454 + shaders/crt.ts:89-117: ~10 leituras de textura por pixel, +8 no quadro pop-art.
  • Impacto:
    • o bloom roda em alvos minúsculos, mas 15 trocas de render target pesam em GPU de tiles (Apple Silicon, celular);
    • o CRT custa ~10 leituras por pixel: em 4K (8,3 MP) são ~83 M leituras por quadro, alguns ms em Intel UHD (estimativa).
  • Correção:
    • bloom dual-Kawase em 4 passes;
    • um preset "leve" que desliga o halo largo (NIGHT.wideBloom) e o vazamento de cor (bleed, 4 leituras).

18. 🟡 Pools de partículas enviam todos os buffers todo quadro, mesmo vazios

  • Onde: src/fx/points.ts:173.
  • Medido: 5 pools × 1.704 partículas × 12 floats ≈ 82 KB por quadro (11,8 MB/s em 144 Hz) e 5 draws com zero partículas vivas.
  • Correção: se alive === 0, obj.visible = false e sem needsUpdate. No resto, addUpdateRange até o maior índice vivo.

19. 🟡 Renderiza a 100 % no título e na pausa, e em 144 Hz faz 2,4× o trabalho que a simulação precisa

  • Onde: main.ts:291 sempre renderiza.
  • Impacto: a ventoinha do notebook sobe e a bateria cai com o menu parado. Em 144 Hz, 900 draws vezes 144, com a simulação a 60.
  • Correção:
    • 30 fps no título e na pausa;
    • opção "limitar a 60 fps";
    • de quebra, rdt hoje pode ficar negativo: falta Math.max(0, …) em main.ts:214.

20. 🟡 DPR limitado a 2: pixel art borrado em celular e em notebook 4K com 250–300 % de escala

  • Onde: render.ts:202, 262. Não há ResizeObserver/devicePixelContentBoxSize nem matchMedia para mudança de DPR ao trocar de monitor.

  • Impacto:

    • com DPR 3, o buffer sai com 2× e o navegador estica 1,5× com filtro bilinear;

    • o upscale inteiro, que é o coração do visual, vira borrão desigual.

    • Os casos comuns estão corretos, conferidos com o algoritmo de render.ts:273-278:

      Tela Escala Alvo baixo
      1366×768 3 456×256
      1920×1080 4 480×270
      2560×1440 5 512×288
      3440×1440 5 688×288
      3840×2160 8 480×270
      MacBook Air 2940×1912 7 420×274
      Celular 390×844 @3 3 sobre 780×1688 260×563, com borrão de 1,5×
  • Correção:

    • usar o pixelRatio real (buffer = pixels do dispositivo) e controlar o custo do CRT pelos efeitos;
    • image-rendering: pixelated no #c como rede de segurança;
    • ouvir a mudança de DPR.

21. 🟡 FOV vertical fixo: ultrawide estica, celular em pé fica com visão de túnel

  • Onde: camera.ts:184, 503 (55° vertical).
  • Impacto:
    • 21:9 dá ~100° de FOV horizontal e 32:9 dá ~123°, com bordas esticadas e mais coisa (mais draws) em vista;
    • celular em pé fica com ~27° horizontais e não vê os lados;
    • os sprites de FX alinham na grade round(pxH × aspect) (fx/sprites.ts:74), que pode diferir do lowW real em 1 px: o POW! perto da borda direita pode perder ou duplicar uma coluna de texel.
  • Correção:
    • limitar o FOV horizontal (Hor+ até ~90°, depois Vert-) e garantir um mínimo no modo retrato;
    • passar o rp.pixelGrid.width para as FX.

22. 🟡 Alocações pequenas em todo quadro (coleta de lixo frequente)

  • Onde:
    • world/tokyo/index.ts: :340 (focus.clone()), :380 (new Vector3), :381 (2 Matrix4 + Set);
    • audio.ts:195: objeto literal;
    • camera.ts:622: spread;
    • hud.ts:81-98: 5 template strings, além de offsetWidth forçando layout no meio do quadro a cada golpe (:86, 109, 117);
    • player/parkour/index.ts: dezenas de new Vector3/clone por passo durante o parkour (:205-240 detectLedge, :820-870 pendurado).
  • Impacto: coletas da geração jovem a cada poucos segundos, de ~0,5 a 1 ms (estimativa). Não chega a ser grave sozinho, mas some com folga de quadro em máquina fraca.
  • Correção: temporários no nível do módulo e reescrever o style.transform só quando o valor muda.

23. 🟡 Reflexo da chuva: varredura da cena a cada 1 s e matriz do mundo recalculada 2× por quadro

  • Onde: render.ts:467-475 e :505. O reflexo só liga com chuva, que vem desligada (TUNE.rain = 0).
  • Impacto: com chuva, percorre 662 malhas por segundo para re-etiquetar. O segundo r.render chama scene.updateMatrixWorld() de novo sobre 787 objetos.
  • Correção:
    • etiquetar REFLECT_LAYER na montagem e ao adicionar FX ou props;
    • scene.matrixWorldAutoUpdate = false durante o passe do reflexo.

24. 🟡 O teste de desempenho não vê os problemas acima

  • Onde:
    • tests/e2e/perf.mjs: só mede tempo de quadro em swiftshader com relógio virtual;
    • main.ts:297-300: o __rubro não expõe o RenderPipeline;
    • renderer.info.autoReset zera a cada um dos 15 render() do quadro;
    • em tests/reports/ só existe o baseline da wave 1, gerado de um build de 1,2 MB. A wave 2 (6,6 MB, inimigos novos, máquinas, tecidos) nunca foi medida.
  • Correção:
    • expor o rp e usar info.autoReset = false com reset manual por quadro, para registrar draw calls, programas, geometrias e texturas;
    • cenário de "engasgo": o maior quadro no 1º golpe, no 1º estouro de máquina e no 1º vanish;
    • teste de cadência em 144 Hz: variação do deslocamento dos inimigos por quadro;
    • memória: performance.measureUserAgentSpecificMemory.

25. 🟡 A cápsula do player na física dá trancos quando o fps cai

  • Onde: physics/index.ts:269-289 e :333-337. A posição é gravada 1× por quadro, mas MoveKinematic roda a cada passo.
  • Impacto:
    • a 30 fps, o 1º passo leva a cápsula até o alvo com velocidade dobrada e o 2º com velocidade zero, então as latas e os cones voam mais longe;
    • com o jogo a 6 passos e a física limitada a 4, abaixo de 15 fps os props andam em câmera lenta em relação ao combate.
  • Correção: interpolar o alvo da cápsula por passo, do anterior para o atual, e alinhar os dois limites.

Verificado e sem problema

  • Pipeline pixel e CRT:
    • escala inteira escolhida por proximidade logarítmica (render.ts:273-278);
    • ceil + setViewOffset ampliado deixam o pixel quadrado e alinhado no canto superior esquerdo (render.ts:353-354), e toUv do CRT bate com isso (crt.ts:36-38);
    • scanline na última linha de cada linha baixa;
    • funciona em ultrawide; o único defeito é o DPR acima de 2 (item 20).
  • Troca de mapa: ?mapa=plato recarrega a página, então não há texturas ou geometrias vazando entre mapas.
  • Liberação de recursos:
    • inimigos liberam os materiais próprios e as capas (enemies/look.ts:178-181, enemy.ts:1472-1476);
    • as FX são pools fixos.
  • Listeners: sem vazamento encontrado. O diálogo, a pausa e os inimigos removem os seus (npc/dialog.ts:132/142, ui/pause.ts:278-280, enemies/index.ts:291).
  • Texturas: as máquinas de venda só repintam o canvas em evento, nunca por quadro.
  • Espiral da morte: protegida no loop e na física (item 7).
  • Fora da minha área, para quem cuida da IA: o navmesh cobre só ±19,6 m em volta de fightCenter (enemies/index.ts:94-97), mas os inimigos nascem em x = ±43 (world/tokyo/index.ts:251-252). Vale conferir se o crowd do recast pega esses agentes.

Notebook fraco com iGPU: o que esperar (estimativa a partir das contagens)

  • CPU: ~900 draws, mais simulação, mais física com 10 tecidos (1 a 3 ms por passo), mais XState e crowd dos inimigos. O gargalo é a CPU, e é provável ficar entre 30 e 50 fps numa onda cheia.
  • GPU: sombra 2048² mais CRT em resolução nativa, folgado em 1080p e apertado em 4K.
  • Engasgos: primeiro golpe, primeira máquina destruída, primeiro vanish e primeira capa. São os momentos de maior emoção.
  • O que mais rende, nesta ordem:
    1. item 5 (uma linha);
    2. item 4 (aquecer os shaders);
    3. item 7 (interpolar inimigos);
    4. item 8 (congelar tecidos longe);
    5. item 3 (fundir as peças dos personagens), que exige o antes/depois visual.
Câmera: causas medidas e como deveria ser

19/10/2026. Cópia só de leitura em /tmp/kavo-analise/apps/rubro-arena; nada nela foi editado.

Como eu medi

  • Li inteiros:
    • src/camera.ts;
    • o laço de src/main.ts;
    • src/input.ts;
    • src/collision.ts;
    • src/world/tokyo/layout.ts;
    • a trava de direção em src/player/controller.ts:347-372;
    • a mira em src/combat/fighter.ts:288-320 e src/combat/lockon.ts.
  • Simulei com as classes reais, em Node, a 60 Hz, só no teclado:
    • a rua de Tóquio de verdade (createWorld('tokyo')), com colisão BVH;
    • Player + Parkour;
    • o Input real, com as teclas apertadas por um relógio falso;
    • a CameraRig real;
    • no combate, os Enemies reais (IA XState, fichas de ataque, navmesh recast) e o Fighter real.
    • A ordem do laço é a mesma do main.ts: passos fixos, depois a câmera com o tempo real.
  • O que eu não medi:
    • pixels: "na tela" quer dizer que o ponto cai dentro do quadro 16:9, com FOV 55°;
    • o mouse;
    • a física Jolt;
    • o combate humano: o bot de combate é simples: anda até o inimigo mais perto e aperta J.
  • Scripts e saídas:
    • /workspaces/art-and-see-monorepo/.local-state/kavo-analise/agentes/cam-sim/, com harness.ts, run-a.ts, run-bcdfg.ts, run-e.ts, run-lock.ts, run-ladder*.ts e run-sidewalk.ts;
    • as saídas ficam em cam-sim/out/.
    • Para rodar: cd /tmp/kavo-analise/apps/rubro-arena && npx vite-node <script> -- <cenário>.
    • Com CAM=patched roda o protótipo da correção (cam-sim/patched-camera.ts).
  • Já conhecido, não repito:
    • câmera do diálogo (combate #16);
    • inimigos sem interpolação (combate #17);
    • placas mescladas sem pontilhado (combate #11, camera.ts:556);
    • giro lento do mouse não muda o W (movimento #14);
    • câmera de pendurado nunca liga no pendurado SM64 (movimento #17).

Resumo em uma frase

A câmera só gira sozinha quando você já está apertando W, e aí ela já está atrás. Toda virada com A ou D deixa o Kavo de lado na tela para sempre. Os atalhos que deveriam consertar isso (Q/E e Shift) giram o Kavo junto, enquanto a tecla de direção está segurada. Na rua de 9 m, a mira travada e as escadas de incêndio somam aproximações bruscas e ângulos quase de cima.


Por que você vive ajustando

1. 🔴 A câmera nunca vem para trás sozinha quando você vira com o teclado

O que você vê: você aperta D para seguir a rua. O Kavo vira e corre de lado na tela, e a câmera fica olhando o perfil dele até você girar com Q/E.

O que medi:

Teste (sem Q/E) Câmera girou sozinha Quadros de perfil Visão à frente (mediana) Volta para trás?
Segurar D 4 s pela rua (a3) 0° 100% 4,5 m nunca
W 1 s e depois D 3 s (a4) 0° 75% 0,5 m nunca
Na passagem do trem, virar com A para o corredor dos trilhos (b1) 32° (pelo empurrão de parede, não pelo seguir) — 4 m nunca: ainda 58° fora depois de 3 s
Correr com W no meio da rua, câmera atrás (c5) 0° 0% 40 m já está atrás
  • Visão à frente: quantos metros do caminho cabem no quadro sem parede na frente. De lado, a 8,6 m/s, isso é meio segundo de visão. Com a câmera atrás, são 40 m.

Por quê:

  • O "giro automático" (camera.ts:347-359) é multiplicado por sideW. Esse peso só vale 1 quando a tecla aponta para longe da câmera: lateral < 0,3 (camera.ts:306-308, CAM.sideStart/sideEnd = 0,3/0,7 em camera.ts:32-33).
  • O teclado tem 8 direções:
    • W: lateral 0, peso 1;
    • W+D: lateral 0,707, peso 0;
    • D: lateral 1, peso 0.
  • Ou seja, a câmera só segue quando você aperta W, e com W ela já está atrás. O "recentrar depois de 3 s correndo reto" (camera.ts:311, 354, CAM.recentreAfter) tem a mesma trava.
  • A trava foi posta para evitar "correr em círculos segurando D" (auditoria antiga #12). Esse problema já é resolvido por outra peça: a trava de direção do controle (controller.ts:363-371). Enquanto a tecla segue segurada, ela mantém a direção no mundo, mesmo com a câmera girando devagar (< 1,9 rad/s).
  • Benchmark:
    • Nesky #22 "Relying on players to control the camera all the time";
    • Nesky #23 "Leaving the camera yaw alone while the player is running";
    • no SM64, o Lakitu "trails Mario with no fixed point": ele é arrastado atrás do Mario.

Prova de que é isto: o protótipo (cam-sim/patched-camera.ts) faz 2 mudanças:

  • seguir também de lado, a até 1,6 rad/s;
  • a trava de direção segue só os giros manuais.

Resultado do protótipo:

Teste Hoje Protótipo
D 4 s pela rua (a3) nunca volta; 4,5 m de visão atrás em 1,03 s; 40 m de visão; o Kavo corre reto, sem círculo
Virada no corredor do trem (b1) nunca; distância cai a 3,6 m atrás em 0,67 s; distância nunca abaixo de 6,75 m
W e depois D (a4) nunca atrás em 0,98 s

2. 🔴 Q/E ou Shift com uma tecla de direção segurada giram o Kavo junto: meia-volta ou giro no lugar

O que você vê: você tenta consertar a câmera em movimento, e o Kavo vira para outro lado, roda ou entra na parede.

O que medi:

Teste O que aconteceu
Correndo de lado com D, segurar E 1,5 s (f3) A câmera girou 221°, e o Kavo girou 218° junto. Ele continua de lado (−95°), bate na fachada, a câmera chega a 0,18 m e o Kavo sai da tela.
Correndo de lado com D, um toque de Shift sem inimigo (f1) A câmera vai para trás em 0,27 s, e logo depois o Kavo dá meia-volta de 173° e corre para trás.
Virou no corredor do trem com A e tocou Shift (b3) O Kavo volta para a rua. A câmera fica a 0,47 m, com o Kavo fora da tela (y = −1,6). 33% dos quadros ficam abaixo de 3 m.
Segurar Shift + D sem inimigo, 3 s (f2) O Kavo gira 1.406° (quase 4 voltas) num círculo de ~1 m.
Jeito que funciona hoje (b5) Soltar a tecla, segurar E 0,6 s, apertar W: atrás em 0,97 s, e o Kavo freia. São 3 passos em toda esquina.

Por quê:

  • controller.ts:366 trata qualquer giro de câmera acima de 1,9 rad/s como manual e gira a direção da tecla junto.
  • O recentrar do Shift (camera.ts:338-343) gira 90° em 0,3 s, ≈ 5 rad/s.
  • O "Shift segurado sem alvo" (camera.ts:344-346, ease(dt, 12)) também passa do limite.
  • Q/E gira a 2,6 rad/s (camera.ts:299).
  • Isso forma um laço: a câmera persegue as costas do Kavo, e as costas perseguem a câmera.
  • Benchmark: no Ocarina of Time, Z sem alvo deixa a câmera "fixed behind Link until the button is released", e o toque recentraliza (Zelda Wiki). Ali, recentrar nunca muda para onde o Link anda. Nesky #18 "Letting cuts remap directional controls" e #19 "Breaking the player's sense of direction".

No protótipo (a trava segue só Q/E e mouse):

  • Shift + D (f2) corre reto, 23 m, sem girar;
  • o toque de Shift na virada (b3) leva a câmera para trás em 0,62 s, sem meia-volta.

3. 🔴 Mira travada (Shift) na rua estreita: a câmera gira demais, chega perto e esconde quem bate

O que medi: 4–5 inimigos com IA real, 40 s por rodada, 3 sementes por variante.

Variante Todos os inimigos na tela Atacante fora da tela (% dos quadros de ataque) Ataques que começam fora da tela Câmera < 3 m Inclinação máx. Tempo girando > 30°/s Giros rápidos a cada 10 s
Sem mira, 4 inimigos 89–97% 0% 0 0–0,7% 22° 0–1,2% 0–0,4
Sem mira, 5 inimigos 77–98% 0% 0 0–5,5% 21° (1 rodada 81°) 0–6,8% 0–1,3
Com mira, 4 34–76% 0–48% 0–2 12–15% (mín. 0,56 m) 59–71° 23–31% 4,5–8,3
Com mira, 5 41–79% 0–48% 0–3 5–17% (mín. 0,38 m) 57–74° 17–27% 3,3–6,6
Com mira + troca Q/E, 5 16–24% 35–47% 5–6 0–7,5% 36–73° 24–38% 4,5–6,8

Troca de alvo com inimigos parados (run-lock.ts):

  • cada Q/E gira a câmera 125–168° em ~0,75 s, com pico de 327–344°/s;
  • ao trocar para o inimigo de trás, os outros dois somem da tela;
  • circular o alvo com Shift + D por 2 s gira a câmera 197°, chega a 1,8 m e inclina até 62°.

Por quê:

  • Travada, a câmera se põe na linha alvo→Kavo, atrás do Kavo (camera.ts:314-334), e gira até 6 rad/s (camera.ts:329).
  • Na rua de 9 m (layout.ts:6-14), essa linha quase sempre cruza a calçada e enfia a câmera na fachada. Com alvo, o desvio girando fica desligado (camera.ts:443: canYaw exige !target), e só sobra o guindaste de +0,45 ou +0,9 rad (camera.ts:450). Por isso a câmera fica quase de cima.
  • A troca de alvo vira um pan inteiro até o novo inimigo (camera.ts:266-277, 328; fighter.ts:302-308).
  • O enquadramento de ameaças é desligado quando travado (camera.ts:531, (1 - lb)).
  • As fichas de ataque favorecem quem está fora da tela:
    • um inimigo fora da tela que não ataca há 3,5 s rouba a ficha de um que está na tela (brain.ts:117-124, enemies/index.ts:76, tune.ts:30);
    • somado à câmera travada que esconde gente, isso dá golpes "do nada".
    • O dono disso é o combate, mas o efeito aparece na câmera.
  • Benchmark:
    • Nesky #31 "Rotating excessively to look at nearby targets" e #8 "Letting independent forces compete to push the camera";
    • God of War 2018 manteve a câmera fechada. Para compensar, pôs setas de ameaça fora da tela: branco = perto, vermelho piscando = golpe vindo, roxo = projétil (Prima Games);
    • na palestra de combate de GoW na GDC 2019, um resumo de terceiros diz que os testadores "felt anxious when enemies went off-screen" (GDC Vault, resumo).

Nota justa: sem mira, a câmera de combate está boa no teste. O inimigo mais perto fica fora da tela em 0–4% dos quadros, com distância média de 6,9–8,1 m. O enquadramento de ameaças funciona. O estrago está na mira.

4. 🟠 Escadas de incêndio e postes: a câmera "bombeia" e o Kavo agarra a escada sem querer

Agarra sozinho:

  • correndo pela calçada com W, o Kavo agarra a escada de incêndio quando passa por ela;
  • aconteceu nos 3 de 3 trechos de calçada com escada (x ≈ −36, −26 e +1,2).
  • A regra "andar contra ela já agarra" (parkour/index.ts:619-621, intent·toAx > 0,6) vale também para quem passa ao lado. O dono disso é o movimento, mas é a câmera que muda de modo.

A câmera bombeia:

  • No modo hang, a distância alvo vai a 13,6 m: 1,4 × 7,6 m (camera.ts:83) mais até 3 m de "enquadrar a próxima beirada" (camera.ts:415).
  • Com a câmera paralela à fachada, cada sacada corta o braço.
  • Resultado: 6 aproximações bruscas em 6,8 s, de 13,7 m a 0,12 m, com salto de até 10,5 m num quadro. O corte duro é camera.ts:459. O Kavo fica fora da tela 6% do tempo (run-ladder-side.ts).
  • Subindo a mesma escada de frente para a fachada, está tudo bem: 6,4–13,6 m, sem saltos.

Poste da rota para o telhado da konbini (d4):

  • inclinação de −69° a +79°;
  • a câmera fica na frente do Kavo (−175°) durante o giro no poste;
  • distância mínima de 0,45 m.

5. 🟠 Olhando para o outro lado da rua, o braço de 7,6 m não cabe e a câmera sobe e desce sozinha

  • A rua tem 9 m de fachada a fachada (layout.ts:6-14). A câmera padrão fica a 7,6 m e 19° (tuning.ts:50-51), ou seja, 7,2 m na horizontal.
  • Toda vez que a câmera fica de lado (causa 1), o braço bate na fachada da frente.
  • No teste a3 (D pela rua):
    • a distância fica em 4,0–4,9 m;
    • a inclinação pula entre 20° e 42° quatro vezes em 4 s, porque o guindaste liga e desliga (camera.ts:442-455);
    • são 27,5 mudanças bruscas de distância a cada 10 s.
  • Benchmark:
    • Nesky #4 "Using a default camera distance that's likely to break line-of-sight";
    • Nesky #16 "Shifting pitch, distance, and field-of-view independently";
    • Nesky #14 "Using the same camera distance for all angles".

6. 🟠 Calçada colada nas fachadas: aproximações bruscas

  • Com a câmera atrás, em calçadas sem escada (run-sidewalk.ts):
    • 0–2 aproximações bruscas por trecho de 15–27 m, até 6 a cada 10 s;
    • mínimo de 1,84 m;
    • 0–7,5% dos quadros abaixo de 3 m.
  • Por quê:
    • a câmera entra em ~0,07 s (taxa 14/s) e ainda tem o corte duro em free.hard (camera.ts:457-459);
    • ela sai em ~1 s (2,2/s);
    • não existe "tempo mínimo de bloqueio", então qualquer coisa que passa um quadro no raio já puxa;
    • os 4 raios de canto ficam a ±0,22 m (camera.ts:660-667) e raspam a fachada quando o Kavo anda a 0,7 m dela.
  • Benchmark:
    • o Cinemachine Collider tem "Minimum Occlusion Time: Do not take action unless the occlusion has lasted at least this long" e "Damping When Occluded: How quickly to move the camera to avoid an obstacle" (Unity docs);
    • Nesky #9 "Excessively moving the camera to prevent unimportant items from breaking line-of-sight" e #46 "Rapidly transitioning to a new camera position".

7. 🟠 Parede atrás empurra a câmera de lado, sozinha

  • Com o braço curto, a câmera gira até 1,2 rad/s para o lado mais aberto, 2 s depois do último Q/E (camera.ts:451-453).
  • Exemplos:
    • b1: a câmera girou 32° sozinha e parou a 58° do ideal;
    • a5: girou 43° com W+D.
  • É a única rotação automática que acontece de fato hoje.
  • Além dela, a câmera tem mais 4 forças independentes:
    • o puxão de parede;
    • o guindaste;
    • o olhar à frente;
    • a ameaça.
  • Benchmark: Nesky #12 "Swinging the camera sideways when occluders come from behind" e #8.

8. 🟡 A marquise da konbini liga o modo "interior" do lado de fora

  • A causa:
    • a zona da loja usa a caixa com a varanda de 1 m (world/tokyo/index.ts:244-246 usa konbini.interior);
    • o próprio módulo avisa que essa caixa é "the full footprint incl. the porch" (konbini/index.ts:42);
    • existe a caixa inside, só até o vidro (konbini/index.ts:185).
  • O efeito: sob a marquise, no gachapon, com o Kavo ainda fora do vidro (z = −5,1, g1), a câmera muda sozinha:
    • distância de 6,9 m para 4,1 m;
    • FOV de 55° para 61°;
    • inclinação de 19° para 11°.
  • Dentro da loja, a câmera é estável (4,0–4,2 m), mas com A/D nos corredores ela fica de lado 100% do tempo (causa 1).
  • Saindo com S (correndo para a câmera):
    • você vê só 2–2,5 m à frente;
    • lá fora, o Kavo atravessa a rua e agarra a escada do prédio da frente (x ≈ 2), com a câmera a 0,3–0,9 m e o Kavo fora da tela (g3).

9. 🟡 A lente "respira"

  • O FOV variou de 46° a 61° no combate. As fontes:
    • soco de zoom −9° (camera.ts:508);
    • ameaça +5° (camera.ts:504);
    • interior +6° (camera.ts:81);
    • corrida até +6°.
  • A ameaça também afasta a câmera 1,6 m (camera.ts:421).
  • Sem mira, são 0,3–1,6 "respiradas" (entrada de ameaça) a cada 10 s.
  • Benchmark: Nesky #42 "Rapidly shifting field-of-view".

Fora da lista (está bom)

  • Pulo simples: a câmera não sobe (0,01 m), e o Kavo sobe na tela até y = 0,41 (o centro é 0, a borda de cima é 1,0).
  • Pulo triplo (4,95 m de altura): a câmera sobe 2,66 m suave (máx. 0,14 m/quadro). O Kavo chega a 0,74 da borda de cima, onde 1,0 é a borda: perto, mas dentro. Isso segue o Nesky #45 "Translating or rotating up and down when the avatar jumps".
  • Meio da rua com a câmera atrás: perfeito. 7,1 m, 21°, nenhuma mudança, 40 m de visão.

Como deveria ser

Cinco regras

  1. A câmera vem para trás sozinha sempre que o Kavo corre numa direção, inclusive de lado. O teclado não tem analógico para "ir virando". Exceção: quando ele corre em direção à câmera. (SM64: "Lakitu trails Mario with no fixed point"; Nesky #22, #23.)
  2. A direção que você segura só muda se você girou a câmera de propósito (Q/E ou mouse). Seguir sozinha, recentrar com Shift, desviar de parede e a mira nunca mudam para onde o Kavo está correndo. (Nesky #18, #19; o Z do Ocarina.)
  3. Uma força por vez, nesta ordem: manual > mira > colisão > seguir > enquadrar ameaça. Quem perde fica parado; ninguém soma vetores. (Nesky #8.)
  4. Parede atrás:
    • primeiro, subir um pouco (até +12°);
    • depois, aproximar devagar;
    • nunca girar de lado sozinha enquanto o Kavo anda;
    • ignorar bloqueios rápidos (< 0,15 s) e coisas finas.
    • (Cinemachine "Preserve Camera Height", "Minimum Occlusion Time", "Damping When Occluded"; Nesky #9, #10, #12.)
  5. O manual sempre ganha e é respeitado por 1,2 s. Depois, o automático volta entrando aos poucos (0,5 s). (Nesky #34, #35.)

Por momento

Momento Como deve ser (números) Referência
Explorando a rua
  • Distância 6,0 m (hoje 7,6), pivô a 1,3 m, inclinação 20° (0,35 rad), FOV 55° (~86° na horizontal em 16:9).
  • Seguir: giro = 1,5 rad/s × (velocidade ÷ 8,6) × mín(1, erro ÷ 30°), também de lado, só no chão, com 50% no ar.
  • Não segue com erro > 150° (correndo para a câmera) nem em parkour.
  • Começa 1,2 s depois de Q/E ou mouse e entra em 0,5 s.
  • Meta: 90° de virada → câmera atrás em ≤ 1,2 s (protótipo: 0,67–1,03 s).
  • Mantém o olhar à frente de hoje (0,24 s, até 1,8 m).
SM64 (Lakitu arrastado); Nesky #4, #22, #23
Calçada estreita (Kavo a < 1 m da fachada)
  • Mesma distância.
  • Raios de canto a ±0,15 m (hoje 0,22).
  • Puxar só se o bloqueio durar ≥ 0,15 s e a coisa tiver ≥ 0,5 m de largura e ≥ 1 m de altura.
  • Entrar em 0,2 s (hoje 0,07 s); voltar em 0,6 s.
  • Corte duro só para não atravessar a parede.
  • Antes de encurtar, subir até +12°. Nunca passar de 45° de inclinação total.
  • Meta: < 3 m em ≤ 2% dos quadros, ≤ 1 aproximação brusca a cada 10 s.
Cinemachine Collider; Nesky #9, #46
Virando esquina / beco / corredor do trem
  • Segue como na rua.
  • Não usa os "bigodes" que giram a câmera de lado enquanto o Kavo anda.
  • Bigodes só com o Kavo parado há > 1 s, a ≤ 0,4 rad/s.
Nesky #12
Pulo / triplo
  • Manter a zona morta vertical de hoje (está boa).
  • Se o Kavo passar de 60% da altura da tela, o pivô sobe para segurá-lo ali.
  • Ao pousar, o pivô desce em ≥ 0,3 s.
Nesky #28, #45
Parkour: escada, pendurado, tirolesa
  • Distância máxima de 6,5 m (hoje até 13,6), "enquadrar a próxima beirada" até +1 m (hoje +3), FOV +3°.
  • Na escada ou pendurado, a câmera vai devagar (0,8 rad/s) para um ângulo de 30–45° em relação à parede. É o ângulo que funcionou no teste de frente.
  • Agarrar a escada só se o Kavo empurrar contra a parede, não ao passar ao lado.
Nesky #14; teste run-ladder.ts
Giro no poste
  • Yaw da câmera congelado durante o giro: ela não persegue o Kavo rodando.
  • Distância fixa.
  • Só volta a seguir quando ele solta.
Nesky #8, #46
Combate sem mira, vários inimigos
  • Manter o enquadramento de ameaças (funciona), mas no máximo +1 m e +2° de FOV (hoje +1,6 m e +5°), em ≥ 0,6 s.
  • Com inimigo a < 8 m, o seguir cai para 0,6 rad/s.
  • Manter no quadro os 2 inimigos mais perto e qualquer um em preparação de golpe, afastando até +1,5 m e girando ≤ 0,5 rad/s.
  • Setas de ameaça fora da tela no estilo GoW (branco = perto, vermelho = golpe vindo, roxo = tiro).
  • Inimigo fora da tela só ataca depois de 0,5 s de aviso.
GoW 2018 (Prima), GDC 2019
Mira travada (Shift)
  • A câmera fica atrás do Kavo, com o alvo dentro do quadro e órbita ≤ 25°.
  • Giro máximo de 2,5 rad/s (hoje 6).
  • Escolhe o lado da órbita com mais espaço livre; o desvio de parede vale também travado.
  • Guindaste no máximo +15°.
  • Distância de 6–8 m pela separação.
  • Mantém até 2 ameaças extras no quadro (afasta até +1,5 m, FOV +3°).
  • Meta: todos na tela ≥ 80%, atacante fora ≤ 10%, < 3 m ≤ 2%, inclinação ≤ 45°.
Nesky #31; Ocarina (Z)
Troca de alvo (Q/E travado)
  • Pan máximo de 60° por troca, em 0,4 s.
  • Se o novo alvo está atrás (> 100°), a câmera não dá a volta: a seta indica, e o Kavo vira para ele.
  • Na rua, preferir os alvos ao longo da rua.
Nesky #31; Sifu: corredor de Oldboy em enquadramento lateral
Shift sem alvo
  • Toque: atrás em 0,25 s, sem mudar a direção da tecla segurada.
  • Segurar: câmera presa atrás, o Kavo continua para onde estava indo.
Ocarina, Z sem alvo
Konbini (interior)
  • O modo liga só depois do vidro (caixa inside).
  • Transição de 0,5 s.
  • Distância de 4,2 m, inclinação de 24°, FOV +4° (hoje +6°).
  • Seguir a 1,2 rad/s nos corredores.
  • Ao sair pela porta, depois de 1 m do lado de fora, recentrar atrás do Kavo em 0,8 s.
Nesky #16, #42
Diálogo Ver o item 16 de combate-inimigos-camera.md: plano por cima do ombro, mais alto e mais aberto. —
Lente (geral)
  • FOV entre 52° e 62°.
  • Soco de zoom ≤ 4° e ≤ 0,15 s (hoje 9°).
  • Um dono por vez para o FOV.
Nesky #42, #43

Benchmarks citados (fontes)


O que mudar no código

Em ordem de impacto. Os itens 1 e 2 juntos são o protótipo testado.

  1. Seguir de lado (camera.ts:347-359):
    • tirar && sideW > 0 e * sideW;
    • trocar o limite de 2.3 por 2.6 rad;
    • giro = 1.5 * autoK * clamp(|speed|/runSpeed) * min(1, |d|/0.5);
    • CAM.manualHold de 2 para 1.2 (camera.ts:30, 302), com rampa autoK = clamp((manualT-1.2)/0.5);
    • apagar o rastreador de "reto por 3 s" (camera.ts:306-312, 354), que vira redundante.
  2. Trava de direção segue só o manual:
    • a câmera expõe manualYawDelta por quadro, só Q/E e mouse (camera.ts:298-299);
    • o controle troca a regra "rate > 1,9" (controller.ts:363-368) por "soma só o manualYawDelta";
    • recentrar (camera.ts:338-343), Shift segurado (344-346), bigodes (453) e mira (329) nunca entram;
    • isso também resolve o movimento #14 (mouse lento);
    • até isso entrar, o seguir precisa ficar abaixo de 1,9 rad/s (o protótipo usa 1,6).
  3. Mira travada (camera.ts:314-334, 441-455):
    • giro máximo de 6 para 2,5 rad/s (:329);
    • lockSide (:270-273) passa a escolher o lado com mais espaço, testando freeAlong dos dois lados;
    • permitir desvio também travado (:443, tirar !target);
    • guindaste no máximo +0,26 rad (:450);
    • CAM.lockOrbit de 0,5 para 0,45;
    • acrescentar "manter 2 ameaças" quando travado (hoje (1 - lb) zera isso, :531).
  4. Troca de alvo: pan máximo de 60° por troca (camera.ts:266-277, 328). Em fighter.ts:302-308, preferir o próximo alvo dentro do cone de ±100° da câmera.
  5. Colisão (camera.ts:442-461, 651-669):
    • bigodes só com o Kavo parado há > 1 s, a 0,4 rad/s (:451-453);
    • entrada de 14 para 5 (:458);
    • "tempo mínimo de bloqueio" de 0,15 s antes de puxar;
    • corte duro (:459) só pelo raio central e só se a câmera fosse atravessar;
    • raios de canto de 0,22 para 0,15 (:662);
    • guindaste antes de aproximar, até +0,21 rad;
    • inclinação total máxima de 1,35 para 0,8 rad (:423).
  6. Distâncias:
    • camDistance de 7,6 para 6,0 (tuning.ts:50);
    • SHOTS.hang.dist de 1,4 para 0,9 e parkour de 1,2 para 1,0 (camera.ts:82-83);
    • teto do frameExtra de 3 para 1 m (camera.ts:415).
  7. Poste e pendurado: congelar o yaw automático enquanto pk.kind for poleSwing (camera.ts:347; poleSwing já está em CLIMB, mas a mira e os bigodes ainda giram).
  8. Konbini:
    • a zona da câmera usa this.konbini.inside (world/tokyo/index.ts:244), não interior;
    • o áudio pode continuar na caixa da varanda;
    • SHOTS.interior.fov de 6 para 4 (camera.ts:81).
  9. Lente:
    • ameaça +5° → +2° (camera.ts:504);
    • afastar por ameaça 1,6 → 0,8 m (camera.ts:421);
    • soco de zoom 9° → 4° (camera.ts:508).
  10. Fora da câmera, para os donos certos:
    • movimento: agarrar escada só empurrando contra a parede (parkour/index.ts:619-621, exigir intent·(−nL) > 0,8);
    • combate:
      • a ficha roubada por inimigo fora da tela (brain.ts:117-124) só depois de 0,5 s de aviso;
      • setas de ameaça no HUD.

Como conferir depois de mexer

Rodar os scripts de cam-sim/ (com CAM=orig) e exigir:

Cenário Meta
a3 / b1 (virada de 90°) atrás em ≤ 1,2 s; visão à frente ≥ 20 m
f1 / f2 / f3 (Shift e Q/E com tecla segurada) o Kavo vira ≤ 10° a mais do que a tecla pede; nunca gira no lugar
run-sidewalk.ts < 3 m em ≤ 2%; ≤ 1 aproximação brusca a cada 10 s
run-ladder-side.ts nenhuma aproximação maior que 1,5 m em 0,1 s; Kavo sempre na tela
run-e.ts lock5 e lockswitch5 todos na tela ≥ 80%; atacante fora ≤ 10%; inclinação ≤ 45°; giro > 30°/s em ≤ 10% do tempo
run-lock.ts pan por troca ≤ 60°
g1 (marquise) o modo continua default até o vidro
Seus pedidos, um por um (248 itens)

Data: 19/10/2026. Cópia só de leitura: /tmp/kavo-analise/apps/rubro-arena (caminhos abaixo são relativos a ela). Nada foi editado.

Como conferi

  • Li HANDOFF.md, docs/plano-2.0.md, plano-3.0.md, plano-4.0.md, movement-audit.md, wave2.md, wave2-requests/*.md, src/main.ts inteiro e as partes do código citadas em cada linha.
  • npx tsc --noEmit passa limpo.
  • Rodei as classes reais (Enemies, Enemy, Collision, Player, Parkour, createWorld e buildInteract) com vite-node, fora do repo:
    • .local-state/kavo-analise/agentes/pedidos-scripts/fisica.ts: empurrão, massa, inimigo contra inimigo, hit-stop e inércia. Saída em fisica-saida.md.
    • .local-state/kavo-analise/agentes/pedidos-scripts/mundo.ts: monta a rua como o main.ts faz e conta o que o parkour e as interações acham nela. Saída em mundo-saida.md.
    • Para rodar: cd /tmp/kavo-analise/apps/rubro-arena && npx vite-node <script>.
  • Não abri navegador.
  • Bug que já está nos outros relatórios eu só cito, sem repetir a análise. Os nomes: combate #N (combate-inimigos-camera.md), mov #N (movimento-parkour.md), mundo #N (mundo-npc-ui-audio.md) e render #N (render-perf-carregamento.md).

Legenda

  • ✅ feito, funciona, e o jogador chega lá pelas teclas no jogo montado pelo main.ts
  • ⚠️ feito mal ou pela metade
  • 🔌 existe no código, mas não está ligado: sem tecla, sem chamada, desligado por padrão ou sem conteúdo no mapa
  • ❌ não feito

Placar (248 pedidos e promessas)

Fonte ✅ ⚠️ 🔌 ❌
A. HANDOFF §2 (44) 31 10 2 1
B. A conversa e os "o que você recomenda" (66) 19 11 2 17
C. Planos 2.0, 3.0 e 4.0 + movement-audit (138) 85 30 8 32
Total 135 51 12 50

Resumo: o que o dono pediu com as próprias palavras (A) está quase todo no jogo, mas quase um em cada três itens tem defeito ou falta. Das recomendações que o Claude prometeu (B e C), quase um terço não existe ou não está ligado. A física de grupo do vídeo, que era o que ele mais queria, é o maior buraco (última seção).


A) HANDOFF §2: os requisitos do dono

Pedido Status O que falta / está errado Evidência (file:line)
Herói original tokusatsu-inseto (Kavo, rig procedural) ✅ — src/player/rig.ts:31-33, main.ts:60-62
Visual pixel low-res 3D (lado curto ~270 px, escala inteira) ✅ — render.ts:14, render.ts:127; render "Verificado e sem problema"
TV de tubo anos 80 sutil: scanline, chroma, bleed, grain, bloom, tracking, sem curvar a tela e sem vidro ✅ Nunca foi visto numa GPU real (HANDOFF §7.3) shaders/crt.ts:1-2 ("no curvature, no glass, no tube-frame vignette"), render.ts:15-33
VFX pop art com onomatopeias em pixel (POW!, BAM!, KLANG!...) ✅ Efeitos saem em dobro (combate #22) fx/lettering.ts:4-9 (alfabeto pixel próprio), fx.ts:384 (impact), quadro pop art de 2 tons crt.ts:133, disparado em fighter.ts:932
SFX com cara de anos 80 ✅ Ninguém ouviu. O som pode não ligar (mundo #12), o apito não existe e os menus são mudos (mundo #16) audio/sfx.ts:5, audio.ts:51-53
Trilha synthwave original ✅ A trilha troca para a da arena na passagem de nível (mundo #11) audio/music.ts:8-12, audio/city.ts:8 (108 BPM)
SM64: freada ✅ Sim: solta o W a 8,6 m/s → run > brake > idle, para em 0,47 s e 2,08 m movement/states.ts:98-112, params.ts:40-41
SM64: meia-volta (derrapagem) ✅ — controller.ts:730-733, states.ts:114-127
SM64: pulo triplo (o 3º especial) ⚠️ Funciona no chão aberto: cambalhota, faísca, 16,3 m/s. Rente à fachada, o pulo vira wall run (mov #7) e o parkour de chão rouba o Espaço (mov #8, #13). A rua é estreita, então isso acontece muito controller.ts:851-883 (corrente em :875-881), params.ts:48-51
Pulo duplo no ar (pedido explícito) ✅ Apertado 1–3 quadros antes de encostar na parede, ele gasta o wall kick (mov #6) controller.ts:810-817, controller.ts:885-893
SM64: pulo longo ✅ Altura 1,39 m por escolha (mov "Confirmações") states.ts:59-62, controller.ts:856-860, controller.ts:896-902
SM64: mortal para trás ✅ — states.ts:132, controller.ts:904-909
SM64: mortal lateral ✅ Mesma altura do mortal para trás (14,6 m/s) states.ts:121, controller.ts:911-918, params.ts:57-60
SM64: pisão (ground pound) ⚠️ O movimento existe, mas o pisão não acerta nada. O evento groundPound só vai para o áudio, physics.blast nunca é chamado fora de src/physics/, e nem inimigo nem objeto reage. A lista de golpes promete "cai com tudo e quebra o que estiver embaixo". Também há a disputa C→J (mov #22) states.ts:70-72, states.ts:207-215, controller.ts:977-983, ui/moves.ts:12; grep: groundPound só em audio.ts:51
SM64: wall kick ⚠️ Só sai se o Espaço vier depois do contato. Quem aperta cedo, que é o comum, gasta o pulo duplo (mov #6) states.ts:65, controller.ts:956-966
SM64: agarrar borda ⚠️ Dois sistemas de pendurar brigam, e Espaço no pendurado SM64 faz um "salto de gato" antes de subir (mov #1). A câmera de pendurado não liga nesse caso (mov #17) controller.ts:544-575, states.ts:232-242
SM64: mergulho e deslize de barriga 🔌 Os estados e os clips existem, mas ninguém chama startDive() e o combate wave-2 não tem diveAtk (mov #5) controller.ts:938-949, states.ts:160-171, states.ts:201-204
Clean-room (não copiar o n64decomp) ✅ Só números de comportamento, com fonte params.ts:3-7, docs/movement-audit.md
Mira travada estilo Ocarina of Time (Shift), troca de alvo ✅ Shift esbarra nas Teclas de Aderência (mundo #19) fighter.ts:289-319, Q/E troca o alvo em fighter.ts:302-305, enquadramento em camera.ts:316-333
Câmeras estilo Zelda64/Mario64, refeita depois de "meio burra" ⚠️ Tem Lakitu, controle manual, pontilhado, ameaças e modos. Problemas: antenas tapam a atendente (combate #16), pendurado SM64 sem câmera (mov #17), giro lento do mouse não muda o W (mov #14) e inimigos tremem (combate #17) camera.ts:100-111, camera.ts:347-355, main.ts:268-277
MvC: lançador ✅ — C+J em fighter.ts:416, moves.data.ts:56
MvC: juggle ⚠️ O combo aéreo cai no 3º golpe: dois limites de juggle em escalas diferentes (combate #1) enemies/tune.ts:26 contra combat/rules.ts:14
MvC: quiques (no chão e na parede) ✅ A 2ª cravada depois da Explosão vira knockdown (combate #4) spike moves.data.ts:62; kiRush e vanish moves.data.ts:65,68; receptor enemy.ts:376-395
MvC: deslize (sliding knockdown, "1 por combo") 🔌 O receptor sabe deslizar, mas nenhum golpe do Kavo usa react: 'slide' enemy.ts:365-372; moves.data.ts:50-80 não tem 'slide'
MvC: OTG ✅ Bater no caído sem OTG conta como acerto falso (combate #5) moves.data.ts:58, fighter.ts:424, enemy.ts:265-269
MvC: super ✅ — J+K em fighter.ts:343-349, moves.data.ts:75
MvC: super encadeado ✅ — J+K de novo: fighter.ts:344
Combos "físicos e impactantes" ⚠️ O hitstop, o tremor e o flash existem (seção final). Mas golpe que não conecta dá combo e energia (combate #5), efeitos saem em dobro (#22) e a câmera lenta é global (#10) fighter.ts:921-944
Segurar para carregar energia (3 níveis) ✅ — fighter.ts:573-574, fighter.ts:795-801, nível 3 = raio em fighter.ts:1450-1465
Bola de energia ✅ Atravessa paredes (combate #23). Andando para a frente, K vira investida (combate #24) kiBall fighter.ts:437
Especial ✅ Sai de graça com a barra vazia (combate #7) kiRush/kiUpper moves.data.ts:65-66
Teclado: 3 botões J/K/L e combinações ✅ O Espaço tem ~30 significados (mov #13), e L logo depois de acertar vira Vanish (combate #6) input.ts:13-29, contracts.ts CHORDS/CHORD_MS
Rua de Tóquio ✅ — world/tokyo/index.ts; main.ts:47 (mapa padrão)
7-Eleven real em que dá para entrar ✅ — konbini/brand.ts:31-41, porta e zona interior em main.ts:252-260
...e comprar ⚠️ Só 5 itens, pela atendente. O caixa fica mudo quando ela está perto (mundo #2), café e isotônico não fazem nada (combate #3, mundo #1) e os preços divergem (mundo #4) npc/brain.ts:10-16, interact/index.ts:50-54
Máquinas de venda: comprar ✅ Toca o bipe do caixa (mundo #16), e o prêmio abre um menu no meio da luta (mundo #22) interact/vending.ts:100-125, roleta ATARI vending.ts:295-315
Máquinas de venda: quebrar ✅ Quebrar rende mais que comprar (mundo #20) vending.ts:150-190; latas como corpos Jolt em interact/debris.ts:47
Máquinas de venda: erguer e arremessar (Shizuo) ✅ Depois de assentar, a máquina vira fantasma: não colide mais (mov #11) J+L em fighter.ts:960-971 → carry.ts; acerta inimigo em carry.ts:160-175
NPC na 7-Eleven para conversar (Sra. Hoshino) ✅ Só roteiro (o Claude fica desligado, ver B). A "fala livre" é por palavra-chave (mundo #15) npc/index.ts, main.ts:128-129
Capa mole nos inimigos ✅ Só capitão (da onda 2) e chefe (ondas 4 e 8). A onda 1 não tem capa enemies/cape.ts, enemy.ts:175, enemy.ts:472-516, enemies/index.ts:121-125
Interação com o ambiente nível parkour de Assassin's Creed ⚠️ O módulo é grande (vault, mantle, pendurar, quina, wall run, poste, fio, tirolesa, escada, trampolim). Mas tem conflitos (mov #1, #7, #8, #9, #13, #18), barra e deslizar por baixo não têm onde usar na rua (C3), e o degrau no inimigo não existe (mov #5) player/parkour/index.ts; mundo-saida.md
Link real para jogar em outro navegador ⚠️ Só existe um artifact privado do claude.ai, que pede login HANDOFF §0 e §8
"Não quero que esteja ligado no Claude pra abrir" ❌ Nada publicado fora do Claude HANDOFF §7.1 e §8
Máximo de agentes em paralelo ✅ 15 agentes docs/wave2.md

B) A conversa original: as palavras do dono e as respostas "o que você recomenda que eu nem pensei?"

B1. O vídeo do Threads e o "feel" de física

Pedido Status O que falta / está errado Evidência (file:line)
Engenharia reversa do jogo do vídeo (arena top-down de mago com capas Verlet), focando na jogabilidade e física, não na capa ⚠️ Virou outro jogo: 3ª pessoa, rua, SM64 e MvC. Dos 5 ingredientes que o Claude listou, 3 vieram bem; o empurrão em grupo, que é o centro do vídeo, não veio (linhas abaixo) —
(1) Empurrão com massa e atrito: o inimigo desliza, não para seco ⚠️ Desliza, com atrito exponencial em pé e 11 m/s² no chão. Mas é curto nos golpes comuns (0,3–0,5 m), e não há massa: o empurrão não depende do peso atrito enemy.ts:693, enemy.ts:892; empurrão sem peso enemy.ts:328, fighter.ts:913; sim §1
(2) Hit-stop ✅ hitstop(0,12) congela 8 quadros. Senões: as teclas usam o relógio real (combate #11) e a câmera lenta é global (#10) time.ts:9-19, tabela moves.data.ts:47, main.ts:217-223; sim §5
(3) Tremor de tela e flash no golpe ✅ Saem em dobro em vanish, parry e Explosão (combate #22) fx.ts:384- (impact: vítima → câmera → anel → faísca → palavra), camera.ts:186-216 (zoom), enemy.ts:282 + enemies/look.ts:264 (flash branco), render.ts:236-238 (quadro pop art e tranco do CRT)
(4) Inimigos que colidem entre si e se empurram em grupo; inimigo jogado nos outros derruba os outros ❌ Só o arremesso do agarrão derruba outros (J+L, depois K ou J+L). Lançado, chutado, voando de parede ou nocauteado, o corpo atravessa os outros sem efeito. De pé, eles só se afastam, sem reagir. O "boliche" dos inimigos existe e vem desligado separação enemies/index.ts:433-447; boliche desligado enemies/tune.ts:17 + index.ts:264; só o arremesso fighter.ts:1043, fighter.ts:1052-1069, combat/config.ts:25; sim §2 e §3
(5) Movimento com inércia curta: responde, mas tem peso ✅ 0→8,6 m/s em 0,90 s (arranque rápido até 3 m/s). Solta: freada com derrapagem, 0,47 s e 2,08 m params.ts:32-44; sim §6
Diferença de massa entre inimigos ⚠️ O bruto tem hitstun ×0,6, superarmadura e empurra mais o Kavo. Mas desliza igual ao recruta (0,31 contra 0,33 m) e voa mais longe no golpe de parede (22,3 contra 17,7 m). Entre inimigos, a separação é 50/50 enemy.ts:62, enemy.ts:194, enemies/index.ts:424, enemies/index.ts:443; sim §1 e §4
O mundo reage aos corpos (cone, lixeira, produto, máquina) ❌ Inimigo não é corpo físico. Nenhum golpe aplica impulso em prop. onBodyImpact só quebra as pedras do platô: em Tóquio, world.destructibles = 0. Os props só se mexem empurrados pela cápsula do Kavo main.ts:84-94, main.ts:279; grep: nenhum addBody em src/enemies, nenhum applyImpulse de IBody fora de src/physics/; mundo-saida.md

B2. "Nas descartadas você foi naysayer. E se o jogo crescer?" (plano-4.0 §1)

Pedido Status O que falta / está errado Evidência (file:line)
Jolt entra e substitui o Rapier: um motor só para tecido, corpos rígidos, ragdoll e veículos ⚠️ Entrou só para tecido e props. O ragdoll continua Verlet próprio e não há veículos. Custa 3,47 MB, 54 % do jogo (render #2) physics/jolt.ts:1, enemies/ragdoll.ts, world/tokyo/index.ts:313-328
ECS (koota ou bitECS) na rua de Tóquio ❌ Não está no package.json package.json:19-35
@pixiv/three-vrm para NPCs ❌ Instalado e nunca importado. A Sra. Hoshino é procedural package.json:20; render #15; npc/model.ts
Theatre.js só no modo dev (direção de câmera) ❌ Nem instalado package.json
GSAP (cinemáticas, finisher) ❌ Instalado, nunca importado package.json:23; render #15
troika-three-text (HUD dentro do CRT) ❌ Instalado, nunca importado package.json:32; render #15
Yuka e wawa-vfx (reserva) ✅ Reserva, sem gatilho: ok não ter —
recast-navigation (crowd) ⚠️ Ligado, mas o navmesh cobre só ±19,6 m em volta do centro da luta. Spawn do Kavo em x=−32 e dos inimigos em x=±43 (nota no fim de render) enemies/nav.ts:25-38, enemies/index.ts:94-97, layout.ts STREET.fightCenter/fightRadius
XState, three.quarks, Tweakpane, stats-gl ✅ — enemies/brain.ts, npc/brain.ts, fx/bursts.ts, tuning.ts, render.ts:247

B3. "Coloque uma capa mole no inimigo. O que você recomenda que eu nem pensei?" (plano-4.0 §2)

Pedido Status O que falta / está errado Evidência (file:line)
Capa mole (Jolt soft body, com Verlet de reserva) ✅ — enemies/cape.ts:329-340
1. A capa infla antes do especial (aviso) ✅ Só no investida e no pisão do inimigo enemy.ts:915-917
2. Chicoteia na direção do golpe ✅ — enemy.ts:522
3. Rasga com o dano (a vida fica legível) ✅ — enemy.ts:529-538
4. Pega fogo com energia; o inimigo entra em pânico ✅ Qualquer golpe de energia acende, não só o carregado enemy.ts:60, enemy.ts:523-527, enemy.ts:1050
5. Agarrar a capa pelas costas (puxar, girar, arremessar, cobrir a cabeça) 🔌 O código existe, mas o startGrab do combate nunca chama (combate #21) enemy.ts:545-590, fighter.ts:960-973
6. A capa indica a patente ✅ — enemy.ts:175, enemy.ts:473-474
7. Vento do mundo (trem, corredor) em capas, noren, cortinas e varais ⚠️ Capas e noren mexem. Os varais são caixas estáticas e não há cortinas. O noren perde as letras (mundo §7 #9) enemies/index.ts:305-316, world/tokyo/index.ts:378-380, buildings.ts:278-285

B4. "Rua de Tóquio com 7-Eleven real para entrar e comprar, máquinas na porta para comprar ou quebrar. O que você recomenda?" (plano-4.0 §3.6)

Pedido Status O que falta / está errado Evidência (file:line)
1. Perigos da rua: jogar o inimigo na frente do trem ou do carro ⚠️ O trem atropela inimigos e o Kavo, mas o empurrão no Kavo sai invertido (combate #9). Não há carros enemies/index.ts:331-346, main.ts:233-236, world/tokyo/rail.ts:10-16
2. Pegar objeto automaticamente perto dele (Yakuza) ❌ Precisa de J+L. Só 2 bicicletas e 3 máquinas podem ser erguidas main.ts:53 (combatOwnsCarry), interact/index.ts:207; mundo-saida.md (pickup = 2 bike)
3. Briga dentro da loja: prateleiras tombam, produtos voam, porta de vidro quebra, atendente se esconde ⚠️ A atendente se esconde. As prateleiras são colisão estática. Os produtos (corpos Jolt) só se mexem quando o Kavo passa encostado: golpes e corpos não os movem. A porta não quebra, e inimigos atravessam o vidro (mundo #23) npc/index.ts:396-407, konbini/interior.ts:66,110,186 (col(...)), world/tokyo/index.ts:328
4. Pedestres com rotina (Shenmue) ❌ — —
5. Campainha 80s original (não a da FamilyMart) ✅ — audio/world.ts:10, audio/world.ts:168-176

B5. "Na 7-Eleven tem um NPC para conversar. O que você recomenda?" (plano-4.0 §4)

Pedido Status O que falta / está errado Evidência (file:line)
NPC movido a Claude dentro do artifact (sample + db) 🔌 O código existe, mas o build publicado usa KAVO_RUNTIME=off. Fora do claude.ai, que é o link que o dono quer, nunca liga (mundo #15) npc/runtime.ts:38-47, vite.config.ts:9-10, npc/llm.ts
Regras de segurança (estado fora do LLM, propostas validadas, texto do jogador como não confiável) ✅ Valem mesmo com o LLM desligado npc/llm.ts:26-46
Retrato pixel, máquina de escrever, voz "balbucio", Enter livre ou 1–4 ✅ O 1–4 usa e.key (mundo #18) npc/portrait.ts, npc/dialog.ts:246,259, npc/voice.ts
Falas-chave com voz real (TTS "Irasshaimase!") ❌ Só balbucio sintetizado e balão de texto npc/bark.ts:4, fx/palette.ts:88
Falas comuns em cache ❌ cache: false (pesa pouco enquanto o LLM está desligado) npc/llm.ts:162
1. Ela reage ao mundo (máquina quebrada, Kavo ferido, onda limpa) ✅ — npc/index.ts:327-360, npc/index.ts:409-
2. Mestra aposentada que ensina golpe secreto ⚠️ Ensina a Palma Hoshino, mas o golpe some ao recarregar e não volta (mundo #3) npc/index.ts:238-239, npc/brain.ts:77,137
3. Missões: oden ao velhinho, gata no telhado, gangue na porta ⚠️ As 3 existem. A gangue não vem na porta e se completa se você morrer (mundo #5), e não há marcador nem relógio (mundo #7) npc/quests.ts:79-121
4. Amizade = desconto e itens secretos ✅ — npc/index.ts:151-157, npc/index.ts:213-217
5. Bater nela chama a polícia ✅ Um golpe perto do balcão já conta (mundo #9), e o apito é mudo (mundo #16) npc/index.ts:364-380 → enemies/index.ts:199-214
6. Ensina japonês e fofoca do próximo chefe (tutorial disfarçado) ✅ Só para quem pergunta npc/index.ts:147, npc/index.ts:244, npc/script.ts:44-51
7. room: ver o Kavo dos amigos na mesma rua ❌ — —

B6. "A lógica dos movimentos estilo Mario 64 ainda está nas coxas" (plano-4.0 §5 + movement-audit)

Detalhes na tabela C4. Resumo: 11 das 13 correções entraram. Falta ligar o mergulho → barriga → rolamento (🔌). A borda tem dois sistemas brigando (⚠️). Do que a auditoria não previu, o parkour criou problemas novos que estragam o SM64 perto de paredes: mov #6, #7, #8 e #13.

B7. "Melhore a interação com o ambiente, nível parkour de Assassin's Creed. O que você recomenda?" (plano-4.0 §6.5)

Pedido Status O que falta / está errado Evidência (file:line)
1. Degrau no inimigo (renova o pulo duplo) ❌ A lista de golpes promete; ninguém zera usedDouble ao pisar num inimigo (mov #5) ui/moves.ts:25
2. Ataque do alto (cair da borda no inimigo = finalização) ❌ Só o golpe aéreo comum grep sem resultado
3. Puxar o inimigo da borda, pendurado ❌ — grep sem resultado
4. Wall run + chute; vault por cima do inimigo (Arkham) ❌ O vault só lê geometria grep sem resultado
5. Máquina de venda como degrau e toldo como trampolim ✅ 5 toldos-trampolim, e a máquina leva ao telhado da konbini. A máquina arremessada vira fantasma (mov #11) buildings.ts:161-165, script.ts:48; mundo-saida.md (springboard 5)
6. Gramática visual discreta (fita amarela nas bordas escaláveis) ❌ — grep sem resultado
7. Desafios de rota integrados às missões ⚠️ O oden tem 120 s pelos telhados, mas o tempo não aparece na tela (mundo #7) npc/quests.ts:109, layout.ts NPC_SPOTS

C) Os planos, item por item

C1. plano-2.0 (com o que o 3.0 mudou)

Pedido Status O que falta / está errado Evidência (file:line)
§1 3 botões, com remapeamento salvo ✅ — input.ts:13-44, ui/pause.ts
§1 nada de Ctrl/Alt; Espaço não rola a página ✅ — input.ts:38-41, input.ts:118
§1 pausa automática ao perder o foco ✅ O menu abre (blur → onPress('pause') → pause.open()). O render #11 diz o contrário; o menu abre, sim. O que segue rodando são o áudio e o trem (render #11) input.ts:123-125, main.ts:171
§1 aviso das Teclas de Aderência (5× Shift) ✅ Chega tarde, só no 5º toque (mundo #19) input.ts:143-146, main.ts:141
§1 teste de ghosting na tela de remapear ✅ Desenha um QWERTY fixo (mundo #18) ui/pause.ts:38, ui/pause.ts:247
§2.3 hitstun decay, gravity scaling, damage scaling ✅ — combat/rules.ts:8-13, combat/rules.ts:34-46
§2.3 1 quique no chão, 1 na parede, 1 OTG por combo ✅ O deslize não tem golpe (A) rules.ts:76-84
§2.3 limite de juggle ⚠️ Dois limites em escalas diferentes (combate #1) rules.ts:14, enemies/tune.ts:26
§2.3 janelas de cancel com buffer de 8 quadros ✅ A janela de fora roda no relógio real (combate #11) moves.data.ts:94-103, combat/config.ts:17
§2.3 magnetismo (correção de mira 3D) ✅ — config.ts:9-10, lunge em moves.data.ts:50-58
§2.3 hitstop por golpe + zoom punch ✅ — moves.data.ts:47, camera.ts:186-216
§2.4 guarda com raspão e pushback ✅ É infinita (combate #18) fighter.ts:1228-1240
§2.4 rebate de 6 quadros + câmera lenta + contra garantido ✅ L no meio do combo vira Vanish (combate #6) fighter.ts:1223, config.ts:18
§2.4 esquiva perfeita com câmera lenta ⚠️ Nunca dispara contra golpe corpo a corpo (combate #2) fighter.ts:1224
§2.4 pushblock (frente+L na guarda) ❌ — grep sem resultado
§2.5 tabela de golpes, grafo de cancel, Tweakpane ✅ O painel congela valores antigos (mundo #13, mov #12) moves.data.ts, tuning.ts
§2.5 editor de hitbox no jogo (TransformControls) ❌ — grep sem resultado
§2.5 XState no fluxo do jogo e na IA ⚠️ Só na IA e na NPC. O fluxo do jogo é uma variável mode enemies/brain.ts, npc/brain.ts, main.ts:144
§3 HUD mínimo (vida, energia em 3 segmentos, combo com dano e escala, mira) ✅ Não mostra contra pronto, turbo nem missão (combate #15, mundo #7) hud.ts:7-23, main.ts:287-290
§3 dicas no contexto ("C+J lança" depois do 3º J) ❌ O jogo inteiro tem 3 dicas main.ts:141, main.ts:153, npc/index.ts:394; o exemplo de ui/index.ts:17 nunca é chamado
§3 lista de golpes com mini-demo animado do Kavo ⚠️ Só texto, e incompleto (mov #13, mundo #6) ui/moves.ts:7-
§3 tutorial jogável (combo trials) ❌ — —
§3 modo treino (input display, frame data, hitbox, boneco, vida infinita) ❌ — —
§3 segurar ou alternar; "modo simples" (J repetido) ✅ — ui/pause.ts:37, auto combo fighter.ts:392-397
§3 prefers-reduced-motion nos flashes ⚠️ Vale só no HUD e na UI. O quadro pop art, o tranco e o glitch do CRT ignoram hud.ts:28, ui/dom.ts:88, npc/dialog.ts:57; nada em render.ts nem fx.ts
§3 HUD dentro do pipeline CRT (troika ou canvas) ❌ O HUD é DOM, sem scanline hud.ts:41; troika nunca importado
§4 replay determinístico por input ❌ — —
§4 vitest ✅ Só cobre combate e tempo, com inimigo falso src/combat/*.test.ts
§4 stats-gl ✅ Entra no pacote mesmo sem ?dev (render #14) render.ts:247
§4 Spector.js ❌ — —
§4 animação CC0 (Quaternius/Kenney) e Magnific text-to-motion ❌ Rede bloqueada; tudo é procedural HANDOFF §7 roadmap
§4 camadas de animação (base + aditiva + IK) ⚠️ Camadas procedurais e IK de mão e pé existem. A reação a golpe é um clip inteiro, não uma camada aditiva animator.ts:60, anim/ik.ts, anim/combat.ts:286
§4 spring bones nas antenas ✅ — rig.ts:14-25, animator.ts:103
§4 animação "em 2s" (12 fps) 🔌 Existe, mas vem desligada (twos: false); só o painel ` liga animator.ts:14
§4 smear frames ✅ — animator.ts:14, animator.ts:422
§4 Theatre.js ❌ — —
§4 three.quarks ✅ — fx/bursts.ts
§4 contorno e rim light no pass pixel ✅ — shaders/pixel.ts:2, pixel.ts:29-36, render.ts:38-44
§4 paleta travada + dithering Bayer 🔌 Existe e vem desligado (dither: false, paletteLock: false) shaders/pixel.ts:6, render.ts:45-48
§4 toon ramp + sombra blob ✅ Tem sombra real 2048² em vez de blob (render #16) shaders/toon.ts
§4 decalques (crateras, queimado) ❌ Só o texto pintado na rua world/tokyo/street.ts:55
§4 color script por onda ❌ Só o HUD ouve wave hud.ts:60
§4 teste de silhueta em escala de cinza ❌ — tests/
§4 camadas por golpe (sample CC0 + cauda synth) ⚠️ Só síntese; nenhum sample audio/sfx.ts:5
§4 escada de tom por golpe do combo ✅ — audio.ts:325-332
§4 sidechain e ducking ✅ — audio.ts:389
§4 vozes (gritos do Kavo; locutor "K.O.!", "COMBO!", "SUPER!") ❌ — grep sem resultado
§4 Panner3D ✅ O aviso de ataque não usa (combate #13) audio/voices.ts:7, main.ts:283
§4 bots, regressão visual ✅ — tests/e2e/smoke.mjs, tests/baselines/
§4 orçamento: build reprova acima de 16 ms ❌ O teste de desempenho só compara tests/e2e/perf.mjs:5
§5 #1 combos autorados, ragdoll só no K.O. ✅ — enemy.ts:289-294
§5 #3 animação em 2s como identidade 🔌 Desligada (acima) animator.ts:14
§5 #4 replay; #5 combo trials ❌ — —
§5 #6 hitstop variável + zoom punch + flash invertido de 1 quadro ✅ O flash é o quadro pop art de 2 tons crt.ts:133, render.ts:311-315
§5 #8 fichas de ataque (1–2 atacam por vez) ✅ A IA trava com ficha presa (combate #12) enemies/brain.ts, enemies/index.ts:250-252
§5 #9 medidor de estilo que muda a mixagem ⚠️ O texto de rank existe (BOM → KAVO!!). A música só tem 2 estágios: vira "intense" com combo ≥10 combat/moves.ts:4-5, hud.ts:120, audio.ts:345
§5 #10 Vanish ✅ Atravessa parede (combate #14, mov #4) fighter.ts:365-367
§5 #11 beam clash (raio contra raio) ❌ — —
§6 3 supers (DHC solo) ⚠️ São 2 supers mais a Explosão moves.data.ts:75-77
§6 finisher em painéis de gibi ❌ — —
§6 chefe rival, voo, transformação, crateras, modo foto, gamepad ❌ Nenhum getGamepads grep sem resultado
§6 clima ⚠️ A chuva existe e vem desligada, sem som (mundo #21) world/tokyo/index.ts:33

C2. plano-3.0

Pedido Status O que falta / está errado Evidência (file:line)
§0 troca de alvo em Q/E ✅ — fighter.ts:302-305
§0 reaproveitar o RenderPixelatedPass ✅ — shaders/pixel.ts:2
§0 three r186 ✅ — package.json:28
§0 GSAP no lugar do Theatre.js ❌ Instalado, nunca usado render #15
§1.3 segurar ou alternar na mira, guarda e carga ✅ — ui/pause.ts:37, input.ts:230-232
§1.3 pausa no blur, remapear, teste de teclado ✅ Ver C1 —
§2.2 rota AUTO: J J J J → lançador + super pulo sozinho ✅ Sobe sozinho mesmo cercado (combate #24) fighter.ts:392-397
§2.2 rota MANUAL até "J+K de novo → J+K+L → repete" ⚠️ O combo aéreo cai (combate #1), e a Explosão trava e não devolve os quiques (#4) —
§2.3 hitstun mínimo de 8 quadros, ragdoll só no K.O. ✅ — rules.ts:9, rules.ts:34-37
§2.4 Witch Time ⚠️ combate #2 e #10 —
§2.4 dodge offset (segurar J na esquiva mantém o combo) ✅ — fighter.ts:687-688, fighter.ts:1087-1091
§2.4 indicador de golpe iminente ✅ Não diz "agora" nem "de onde" (combate #13) enemies/look.ts:261-264
§2.4 "socar através da sala" (warp até o alvo) ✅ — fighter.ts:425 (warpStrike), config.ts:12
§2.5 golpe no tempo: mais dano, onomatopeia maior, nota extra ✅ É sorte, sem sinal na tela (combate #20) fighter.ts:903-910, fighter.ts:945-948, audio.ts:457-464
§2.5 poses-chave no beat ❌ O animador não lê o beat grep sem resultado em player/
§3.1 as 5 técnicas de hitstop do Sakurai ✅ — fx/shake.ts:55-60 (decai, distância da câmera, ar/chão), hitbox congela em main.ts:217-223
§3.2 empurrão de câmera + shake que decai ✅ — fx.ts:57-61, fx.ts:384
§3.2 recuo de 1–2 quadros do membro que bateu ❌ — grep sem resultado
§3.3 hierarquia hitstop → som → câmera → flash → partículas → palavra ✅ — fighter.ts:921-944, fx.ts:384
§4 assets Quaternius, Sonniss, Freesound e Magnific ("disponível agora") ❌ Nada gerado nem importado; tudo sintetizado —
§5 mitigar o pixel creep (snap no passe final) ❌ — grep sem resultado em render.ts e shaders/pixel.ts
§5 color script por onda; teste de silhueta ❌ Ver C1 —
§6 fase 3.1 (controles, pausa, remapear, HUD) ✅ — —
§6 fase 3.2 (motor de combo; bot com combo de 30+) ⚠️ O bot chegou a 23 golpes (HANDOFF §5.4) —
§6 fase 3.3 (game feel + áudio em camadas Magnific) ⚠️ Sem Magnific e sem samples —
§6 fase 3.4 (defesa, tokens com "roubo" fora da tela, bruto e voador) ⚠️ Tokens e roubo funcionam (brain.ts:117); defesa com bugs (combate #2, #6, #18) —
§6 fase 3.5 (camadas aditivas, 2s, smear, poses no beat, movimento CC0) ⚠️ 2s desligado; sem poses no beat; sem CC0 —
§6 fase 3.6 (supers, finisher em painéis, quebra de cenário, choque de raios) ⚠️ Supers sim. Sem painéis, sem choque de raios, e a quebra de cenário só existe no platô main.ts:84-94
§6 fase 3.7 (treino, trials, replay VHS) ❌ — —
§6 fase 3.8 (chefe rival, voo, transformação, clima, WebGPU, gamepad) ❌ Só o r186 entrou —

C3. plano-4.0 (o que não está em B)

Pedido Status O que falta / está errado Evidência (file:line)
§3.1 a marca num arquivo só (trocar pela paródia em 1 minuto) ✅ — konbini/brand.ts:31-41
§3.2 postes com fios escaláveis ✅ 15 postes viram 30 features (duplicadas, mov #23) e 21 fios street.ts:111-115; mundo-saida.md
§3.2 passagem de nível com sinos, cancela e trem ✅ O trem roda no relógio real (mundo #10) rail.ts:10-16, rail.ts:81-97
§3.2 faixa de pedestre com piyo-piyo ✅ O botão não tem função, porque não há carros (mundo "Lacunas") audio/world.ts:260-270, world/tokyo/index.ts:253
§3.2 escadas de incêndio, ar-condicionado, varais, telhados ✅ 8 escadas; varais estáticos buildings.ts:204-206, buildings.ts:323-329
§3.2 gachapon, bicicletas, cones, lixeiras, noren ⚠️ Cones e lixeiras são corpos de física, mas não dá para erguê-los (mundo, tabela HANDOFF §5.9). O noren não tem letras world/tokyo/index.ts:325-326, street.ts:252-274
§3.2 chuva opcional com poças refletindo ⚠️ Começa desligada e sem som (mundo #21) world/tokyo/index.ts:33, main.ts:261-266
§3.3 interior: bebidas, onigiri e bentô, freezers, estufa, oden, café, ATM, copiadora, revistas ✅ É enfeite; só a atendente vende konbini/interior.ts
§3.3 "Irasshaimase!" ao entrar ✅ — npc/index.ts:332-336
§3.3 "esquentar?" para o bentô no caixa 🔌 Existe só no caixa, que fica mudo (mundo #2) interact/items.ts:34-35, items.ts:54
§3.4 inimigos derrubam dinheiro; comida cura ✅ — enemy.ts:616, items.ts:48-57
§3.4 comida "aumenta atributos para sempre" (River City Ransom) ❌ Nenhum item dá bônus permanente items.ts:38-75
§3.4 oden = cura lenta + resistência ⚠️ Só a cura lenta items.ts:50
§3.4 café (energia mais rápida) e isotônico (esquiva) ⚠️ Não fazem nada (combate #3) —
§3.4 mangá de técnica desbloqueia golpe 🔌 Não dá para comprar (mundo #2), e o golpe some ao recarregar (#3) items.ts:55-56, moves.data.ts:71
§3.4 gachapon com figurinhas ✅ Não rende nada (mundo "ciclo") interact/gachapon.ts:13-20
§3.4 a lata comprada é objeto físico e pode ser arremessada ❌ A lata é só animação na bandeja; o efeito entra na hora vending.ts:270-283
§3.5 moeda, "clonc", roleta piscando com chance de grátis ✅ — vending.ts:120-125, vending.ts:295-315
§3.5 quebrar: amassa → trinca → estilhaça → destrói, latas e moedas espirram ✅ — vending.ts:150-190, debris.ts:47
§6.2 Espaço = parkour para cima, C = para baixo, A/D pendurado ✅ — parkour/README.md, ui/moves.ts:20-25
§6.2 W contra a parede não sobe sem querer (lição do AC Unity) ⚠️ O Espaço ainda segurado da decolagem já dispara wall run (mov #7), e o parkour de chão rouba pulos a 1,9 m (mov #13) parkour/index.ts:276, parkour/index.ts:306
§6.3 vault (speed e kong) e mantle ✅ Atravessam coisas no caminho (mov #18) parkour/index.ts:362-377
§6.3 pendurar, shimmy, contornar quina, saltar de borda em borda ✅ — parkour/index.ts:850-911
§6.3 wall run vertical ✅ — parkour/index.ts:439-452
§6.3 wall run lateral ⚠️ Pega qualquer pulo rente à parede (mov #7) parkour/index.ts:471-481
§6.3 balanço em barras (semáforo, canos) 🔌 O módulo tem; a rua não tem nenhuma tag bar (só a demo) parkour/index.ts:566-587; mundo-saida.md; demos/parkour.ts:60
§6.3 equilíbrio no fio ⚠️ O Kavo anda até 0,55 m acima do fio (mov #15) parkour/index.ts:632-637
§6.3 tirolesa descendo os fios ✅ 3 tirolesas street.ts:195; mundo-saida.md
§6.3 deslizar por baixo (porta de aço, carro) 🔌 O planner existe. Varri a rua inteira (51.684 testes) e há 0 lugares onde ele aceita: as portas de aço vão até o chão e não há carros parkour/index.ts:488-512, buildings.ts:120-123; mundo-saida.md
§6.3 escada de incêndio ✅ — buildings.ts:323-329
§6.3 cano de esgoto ❌ Só desenho, sem colisão nem tag buildings.ts:274-275
§6.3 rolamento ao pousar ✅ — parkour/index.ts:659
§6.3 trampolim no toldo ✅ — buildings.ts:161-165
§6.3 cat leap ✅ É ele que rouba o Espaço no pendurado SM64 (mov #1) parkour/index.ts:429
§6.4 bordas dinâmicas, motion warping + IK, buffer na beirada, momentum ✅ — parkour/index.ts:362-377; controller.ts:869-874; parkour/index.ts:371
§6.4 câmera de parkour (enquadra a próxima borda) ✅ Não liga no pendurado SM64 (mov #17) main.ts:269-276, camera.ts:82-83
§7 fase 4.1 (consertar o movimento SM64) ✅ Ver C4 —
§7 fase 4.2 (parkour) ⚠️ Ver acima —
§7 fase 4.3 (Jolt + capa com todas as mecânicas) ⚠️ Agarrar a capa 🔌 —
§7 fase 4.4 (rua, trem, faixa, chuva, máquinas) ✅ Chuva desligada —
§7 fase 4.5 (konbini, ienes, itens, mangás, briga na loja) ⚠️ Caixa, mangás e buffs quebrados; briga na loja pela metade —
§7 fase 4.6 (NPC com Claude, roteiro, missões, memória) ⚠️ Claude desligado; memória só no localStorage —
§7 fase 4.7 (pedestres com ECS + recast + VRM, perigos de rua, presença room) ❌ Só o trem —

C4. movement-audit: os 13 problemas e as correções

Pedido Status O que falta / está errado Evidência (file:line)
1. Buffer de pulo de 150 ms; perto do chão, Espaço vai para a corrente do triplo ✅ Perto da parede o duplo ainda come o wall kick (mov #6) params.ts:52-56, controller.ts:810-817
2. Pouso pesado só acima de 9 m e nunca em triplo, mortal, chute ou pulo longo; o triplo pousa correndo ✅ — controller.ts:46, controller.ts:1008-1016, params.ts:81
3. Impulsos 9,9+0,25·v / 12,3+0,25·v / 16,3 / 14,6 / pulo longo 7,1 ×1,5 ✅ Gravidade do pulo longo 0,7, por escolha params.ts:46-66
4. Gravidade 28,4 simétrica, terminal 17,7, sem flutuar, corte ×0,25 ✅ Os golpes de combate mantêm 30/46 params.ts:73-76, states.ts:79, params.ts:100-102
5. No ar o corpo não gira; a direção só empurra ✅ A deriva lateral liga e desliga de uma vez (mov #25) controller.ts:791-801
6. Mergulho → deslize de barriga → rolamento ligados de verdade 🔌 Inalcançável (mov #5) controller.ts:938
7. Combate como camada: o golpe guarda o estado do pulo e devolve ✅ — controller.ts:241-262
8. Velocidade máxima em 0,8–1,0 s, sem sprint, estado de freada com poeira ✅ Sim: 0,90 s. O animador ainda tem sprint 11,5 fixo (mov #20) params.ts:34-42, states.ts:98-112
9. Uma regra só de meia-volta; o turn mantém velocidade ✅ — controller.ts:730-733, params.ts:44
10. Beirada de 0,25 a 1,2 m caindo; probe de pouso ≤0,3 m ⚠️ Os números estão certos, mas o parkour pendura de 1,3 a 2,3 m por cima disso, com dois sistemas (mov #1). O teste e2e falha por causa do harness (mov #3) params.ts:19, params.ts:83-84
11. Rampas (escorrega abaixo de 38°, sobe mais devagar, desce mais rápido) e degrau suavizado em 3 quadros ⚠️ Implementado; contra a base de uma rampa íngreme ele anima corrida parado no lugar (mov #10) controller.ts:602-611, controller.ts:738-746
12. Câmera gira no máximo 0,5 rad/s e não gira andando de lado ✅ — camera.ts:31, camera.ts:347-355
13. Mortal lateral da mesma altura do mortal para trás ✅ — params.ts:57-60
Arquitetura: máquina de estados explícita, um buffer de intenção, combate por cima, um filtro de teclado, eventos físicos para o animador, altura de pico ✅ — states.ts:5-10, controller.ts:84, controller.ts:328
Extra: teclado vira analógico (rampa de 0,12 s) ✅ — input.ts:45-48
Extra: squash e stretch, poeira proporcional à queda, cambalhota no triplo ✅ — animator.ts:14, controller.ts:1022
Extra: som no topo do triplo ❌ O evento apex existe e ninguém toca som nele controller.ts:328; nada em audio*
Extra: tolerância de 3 quadros para o C do pulo longo ✅ — states.ts:59-62, params.ts:66
Extra: bonk suave na parede ✅ — controller.ts:700-705
Extra: câmera Lakitu; sem Alt para andar devagar ✅ — camera.ts:349-355

Promessas dos planos que não viraram jogo

Prometido e ligado a nada (🔌)

  • Mergulho, barriga e cambalhota: o dono pediu com essas palavras. Estado, clip e física prontos, mas nenhuma tecla chama (controller.ts:938).
  • Agarrar a capa (puxar, girar, jogar sobre a cabeça): o código existe e o combate não chama (enemy.ts:545-590).
  • Mangás de técnica, karaagê, bentô e "esquentar?": só existem no caixa, que fica mudo ao lado da atendente (interact/index.ts:50-54).
  • Deslize de knockdown (MvC): o inimigo sabe, nenhum golpe pede (enemy.ts:365-372).
  • Animação em 2s, paleta travada e Bayer: vêm desligados por padrão (animator.ts:14, render.ts:45-48). A "identidade visual única" do plano-2.0 §5 #3 não aparece para quem joga.
  • Barra para balançar e deslizar por baixo: o parkour tem, a rua não tem onde usar (0 lugares).
  • NPC movido a Claude: desligado no build e impossível no link sem Claude que o dono quer.
  • Boliche dos inimigos (corpo voando derruba os outros): existe e vem desligado (enemies/tune.ts:17).

Prometido e não feito (❌)

  • Treino e didática: modo treino, combo trials, replay VHS determinístico, editor de hitbox, lista de golpes com demo animado, dicas no contexto.
  • Supers e cinema: finisher em painéis de gibi, choque de raios, 3º super, chefe rival, voo, transformação, gamepad, modo foto, color script por onda, crateras e decalques.
  • Rua viva: pedestres com rotina (ECS + VRM), carros, presença room, pegar objeto automaticamente, a lata comprada como objeto, comida com bônus permanente.
  • Parkour de combate estilo AC/Arkham: degrau no inimigo (já anunciado na lista de golpes), ataque do alto, puxar da borda, wall run + chute, vault por cima do inimigo, gramática visual das bordas, cano para escalar.
  • Áudio com voz: vozes do Kavo e do locutor, falas reais da atendente, samples CC0 e Magnific, som no topo do triplo.
  • Visual: HUD dentro do CRT, prefers-reduced-motion nos flashes, mitigação do pixel creep, teste de silhueta, orçamento de 16 ms no build.
  • Bibliotecas prometidas no plano-4.0 ("e se crescer?"): ECS, VRM, Theatre.js, GSAP e troika. Três estão instaladas e nunca são importadas.
  • Publicação: link fora do Claude.

Prometido, feito e quebrado (⚠️ com bug nos outros relatórios)

  • Esquiva perfeita (combate #2) e Explosão "repete" (#4).
  • Juggle aéreo (#1) e buffs da loja (#3).
  • Golpe secreto que some (mundo #3) e gangue na porta (#5).
  • Os dois pendurados (mov #1).
  • Wall kick (mov #6) e o pulo triplo perto de paredes (mov #7, #8).

Novo neste relatório (ainda não estava nos outros)

  1. O pisão não acerta nada: nem inimigo, nem máquina, nem prop. A lista de golpes diz que "quebra o que estiver embaixo". Só o áudio ouve o evento, e physics.blast nunca é chamado.
  2. Nenhum golpe do Kavo usa o deslize de knockdown, embora a regra "1 deslize por combo" esteja no plano e no receptor.
  3. Corpos lançados e nocauteados atravessam os outros inimigos sem efeito. Medido: 0,01–0,08 m de distância, sem reação e sem dano.
  4. O peso do inimigo não muda o empurrão. O bruto voa mais longe que o recruta no golpe de parede: 22,3 contra 17,7 m.
  5. Golpes e corpos não movem os props físicos (cones, lixeiras, produtos). Em Tóquio, onBodyImpact não tem o que quebrar.
  6. Barra e deslizar por baixo não têm onde existir na rua.
  7. A animação em 2s e a paleta e o dither vêm desligados.
  8. O render #11 erra: o menu de pausa abre, sim, ao perder o foco. O resto do item 11 (áudio e trem seguem) vale.

O que o dono mais valorizou (jogabilidade e física): como está de verdade

Ele disse: "a capa não me importa muito, o que eu mais gostei foi da JOGABILIDADE e FÍSICA". A resposta prometeu 5 ingredientes. Medido com as classes reais (pedidos-scripts/fisica-saida.md):

# Ingrediente prometido Veredito O que a medida mostra
1 Empurrão com massa e atrito (desliza, não para seco) ⚠️ Desliza, mas curto e sem peso. Golpes comuns: 0,31–0,48 m em ~0,4 s, com atrito exponencial (enemy.ts:693). Golpes fortes: 4,9 m (voadora), 8,4 m (arremesso), 17,7 m (investida). O empurrão não divide pelo peso (enemy.ts:328): recruta 0,33 m contra bruto 0,31 m num jab, e o bruto voa mais longe na investida (22,3 contra 17,7 m)
2 Hit-stop ✅ 0,05–0,2 s por golpe, com tabela do Sakurai (moves.data.ts:47): 0,12 s = 8 quadros congelados. Senões: teclas no relógio real (combate #11), câmera lenta global (#10)
3 Tremor de tela e flash ✅ Tremor da vítima (ar/chão, decai, escala com a distância), empurrão e zoom da câmera, flash branco no inimigo, quadro pop art de 2 tons e tranco do CRT. Problema: tudo sai em dobro em vanish, parry e Explosão, e a câmera satura num super (combate #22)
4 Inimigos que colidem e se empurram em grupo; um inimigo jogado derruba os outros ❌ É o ingrediente central do vídeo e não veio. De pé, eles só se afastam sem reagir (B deslocado 0,16–0,21 m). Lançado, voadora, investida e nocauteado passam através do vizinho (menor distância 0,01–0,08 m, nenhum dano, nenhum estado). Só o arremesso do agarrão (J+L e depois K) derruba outros, pelo código do combate (fighter.ts:1052-1069). O boliche dos próprios inimigos vem desligado (tune.ts:17). E nenhum corpo mexe em cone, lixeira ou produto
5 Movimento com inércia curta: responde, mas tem peso ✅ 0 → 3 m/s quase na hora, 8,6 m/s em 0,90 s. Ao soltar: freada com derrapagem e poeira, 0,47 s e 2,08 m. Sem giro no ar e sem pouso pesado nos pulos bons. O que estraga o feel não está aqui: está nos conflitos com o parkour perto de paredes (mov #6, #7, #8, #13)

Em uma frase: hit-stop, tremor e inércia estão bons. A física de grupo que fez o dono gostar do vídeo não existe: empurrão sem peso, corpos que atravessam uns aos outros e um mundo que não reage aos golpes. Para chegar lá:

  1. Ligar o boliche para qualquer corpo acima de ~6 m/s, não só o arremessado: bowling() já existe em enemies/index.ts:349-385.
  2. Dar massa por tipo, dividindo kb e o deslize pelo peso.
  3. Fazer corpos e golpes empurrarem os props Jolt (inimigo como corpo cinemático na camada character, ou IBody.applyImpulse no golpe).
  4. Ligar physics.blast no pisão e nas explosões.
Parkour preso: 6.230 tentativas medidas

Resposta curta: são cinco defeitos de código, não um problema de "sensação". Em ordem de impacto:

  1. Pendurado num telhado ou numa sacada, nenhuma tecla faz o Kavo subir. Isso aconteceu em 0 de 222 tentativas.
  2. O giro no poste joga o Kavo para dentro do prédio. Os postes ficam a 0,3 m das fachadas.
  3. Entre os prédios há frestas de 0,4–0,5 m, mais estreitas que o Kavo (0,7 m). Andando pelo telhado, ele cai nelas e escorrega até o chão, entalado.
  4. Quem chega na escada de incêndio em diagonal fica preso no topo para sempre.
  5. O vanish teleporta o Kavo para dentro da parede.

Um sexto detalhe transforma os casos 2 e 5 num "atravessar o prédio inteiro": a colisão não empurra para fora quem já está dentro (collision.ts:201).

Método:

  • Simulei o jogo real no Node com 6.230 tentativas de teclado, em todos os prédios, escadas, postes, fios, tirolesas e telhados de Tóquio, com câmera real.
  • Usei as classes verdadeiras: createWorld('tokyo'), as máquinas de venda, Collision, Player, Parkour, Input e CameraRig. Nada foi editado em /tmp/kavo-analise.
  • Não rodei o browser.

Legenda: 🔴 quebra o jogo · 🟠 atrapalha muito · 🟡 polimento.

O que já estava em movimento-parkour.md (itens 1, 4, 7, 8, 9, 11, 13, 16, 18, 19, 23) e camera.md (item 4) não é repetido aqui. Esses itens só aparecem quando os números novos mudam a gravidade deles.


1. Números

Como medi

Detector Definição
PRESO Uma direção (W/A/S/D) segurada ≥ 0,5 s e os pés andam < 5 cm, sem estar empurrando uma parede de frente. Pendurado com W conta, porque o jogador quer subir.
Empurrando parede Mesma coisa, mas com o corpo encostado de frente numa parede. É o normal de qualquer jogo; aparece aqui só para separar dos casos acima.
Não sai andando No fim de cada roteiro: solta as teclas e tenta andar 0,5 s em cada uma das 4 direções. Conta quando nenhuma move ≥ 0,4 m (antes de tentar Espaço ou C).
DENTRO O centro da cápsula fica ≥ 4 cm dentro de uma caixa de colisão (teste exato de ponto contra caixa). Entre parênteses, quando fica ≥ 0,36 m dentro, ou seja, o corpo inteiro.
Encaixado A cápsula de parede fica > 12 cm dentro de algo, fora de um movimento de parkour.
Fresta O corpo fica dentro de uma das frestas entre prédios, abaixo do telhado mais baixo.
LIMBO Um movimento de parkour dura > 3 s com < 0,3 m de progresso e tecla apertada, ou o estado alterna ≥ 6 vezes em 1 s.
FALHA AO SUBIR O jogador tenta subir algo alcançável e não fica de pé em cima em 2,5 s. Vale para beirais ≤ 2,3 m (ou passar por cima, se o obstáculo tiver ≤ 1 m de fundo), escadas, telhado vizinho mais alto e "pendurado → subir".

Resultados por elemento

Elemento Tentativas PRESO Empurrando parede Não sai andando DENTRO (corpo todo) Encaixado Fresta LIMBO FALHA AO SUBIR
Fachadas (22 prédios + konbini) 864 4,6% 56,3% 1,9% 15,6% (4,2%) 8,2% 2,7% 1,9% –
Beirais baixos ≤ 2,3 m (AC, caixa-d'água, degraus, máquinas, cerca) 1.709 8,9% 13,3% 1,8% 3,4% (1,7%) 10,6% 7,7% 4,0% 32,9% (556/1.688)
Postes finos ≤ 2,3 m (sem topo para ficar) 67 1,5% 46,3% 0% 11,9% (6,0%) 34,3% 3,0% 4,5% –
Escada de incêndio (subir) 120 29,2% 26,7% 0% 13,3% (3,3%) 29,2% 3,3% 3,3% 52,5% (63/120)
Escada (passar correndo ao lado) 79 0% 8,9% 0% 17,7% (0%) 0% 0% 0% –
Postes de luz (15) 883 3,4% 44,2% 0,6% 54,2% (3,1%) 4,8% 3,2% 0,8% –
Tirolesas (3) 108 7,4% 86,1% 0,9% 7,4% (7,4%) 8,3% 0% 7,4% –
Fios 43 53,5% 27,9% 0% 2,3% (2,3%) 2,3% 0% 30,2% –
Telhado → telhado vizinho 598 23,4% 67,2% 0% 0,8% (0,8%) 22,6% 22,6% 6,0% 86,5% (257/297, só subidas)
Descer do telhado para a rua 384 12,2% 78,1% 4,2% 3,4% (1,0%) 4,2% 0,5% 6,0% –
Pendurado (parapeito / sacada) → subir 400 70,8% 0% 2,3% 16,0% (0%) 0% 10,0% 37,5% 100% (222/222)
Corrida na parede 516 0,4% 34,9% 1,2% 18,2% (1,4%) 6,2% 1,0% 2,9% –
Vanish / blink perto da parede 459 0% 0,9% 0% 71,9% (39,4%) 73,2% 0% 0% –
Total 6.230 12,2% 34,8% 1,3% 19,7% (4,9%) 14,1% 5,9% 5,5% 47,2% (1.098/2.327)

Resumo geral:

  • 47,0% das tentativas (2.927/6.230) tiveram pelo menos um problema.
  • 51 tentativas terminaram o roteiro ainda dentro de um prédio.
  • Erros lançados: 0. O parkour nunca se desligou (pkFailed = false nas 6.230), então o item #23 do movimento não é a causa do "preso".
  • Câmera:
    • Repeti as 864 tentativas de fachada com a câmera parada (CAM=fixed) e os números não mudaram: dentro 15,7% contra 15,6%, pendurado 8,3% contra 8,1%. O problema é movimento e colisão, não câmera.
    • A cápsula Jolt também não entra na conta. Ela só copia player.pos (physics/index.ts:269-289, 333-337) e nunca devolve nada ao player.

Quem põe o Kavo dentro do prédio (estado no primeiro quadro dentro, em todas as tentativas):

Causa Tentativas Corpo todo dentro
Giro no poste 681 58
Vanish/blink 330 181
Escada 73 0
Volta na quina (cornerWrap) 60 8
Pendurado SM64 (ledgeHang) 20 19
Tirolesa 13 11
Subida SM64 (ledgeClimb) 10 8
Corrida para cima na parede (wallRunUp) 9 4
Salto entre bordas (ledgeJump) 8 6

2. Causas, em ordem

🔴 1. Pendurado no telhado ou na sacada, nenhuma tecla faz o Kavo subir

  • O que o jogador vê:
    • Kavo pula, agarra a beirada do telhado (o parapeito) ou o parapeito da sacada e fica lá.
    • Espaço não faz nada. W não faz nada. W+Espaço não faz nada.
    • Só saem C (solta e cai) ou S+Espaço (pulo para trás, de volta para a rua).
    • É o "não sobe direito" e o "fica preso toda hora" ao mesmo tempo.
  • Números:
    • Pendurado em parapeito de telhado: subiu 0 de 174.
    • Pendurado em parapeito de sacada: subiu 0 de 48.
    • Em todos os roteiros que terminaram pendurados (837), o teste "W 0,5 s, depois W+Espaço" subiu em 34 (4%). Só com W, subiu em 2.
    • Os 34 que subiram eram todos beirais com standOk = true, como a fachada da konbini e a caixa-d'água.
  • Causa:
    • parkour/index.ts:233-243: a "espessura" do topo é medida só até o topo mudar de altura. Ela precisa ser ≥ 0,5 m, e o lugar onde o Kavo ficaria em pé (stand) é um ponto ≤ 0,45 m atrás da borda.
    • Todo prédio tem parapeito de 0,25 m × 0,4 m (world/tokyo/buildings.ts:107), com o telhado 0,4 m abaixo dele. A espessura medida dá 0,33, então standOk = false.
    • Parapeito de sacada mede 0,03 de espessura. Ar-condicionado de parede mede 0,32–0,34.
    • A subida a partir do pendurado exige standOk (parkour/index.ts:810). Sem ela, procura um salto para cima (findLedgeJump('up')), não acha e não faz nada, sem nenhum aviso.
    • W pendurado nunca sobe: stepHang só usa a frente da tecla para o salto para trás (parkour/index.ts:788-805).
  • Repro: npx vite-node <pasta>/repro.ts -- parapet
    • Pés em (−16,48; 0; 1,5), olhando para −z (fachada N[−22,−14]), segurando W.
    • Espaço por 4 quadros quando z < −3,7 e de novo 0,3 s depois. O Kavo corre na parede, chuta, faz o duplo e pendura com os pés em 4,7 e o parapeito em 6,8 (espessura 0,33, standOk false).
    • Depois disso, Espaço, W, W+Espaço ou Espaço segurado 2 s: continua pendurado em todos.
  • Benchmark: AC e Dying Light sobem por cima de parapeito fino e de grade de sacada, que é o caso mais comum numa cidade. No AC Syndicate a Ubisoft até anunciou como novidade "entrar pela janela sem bater e cair no parapeito" (GameWatcher).

🔴 2. Giro no poste: o Kavo entra no prédio e pode atravessá-lo inteiro

  • O que o jogador vê:
    • Pula perto de um poste na calçada e o Kavo gira em volta dele. Durante o giro, metade do corpo some dentro da fachada.
    • Às vezes sai do giro voando para dentro do prédio: passa pela parede, cai no chão lá dentro, corre no escuro e sai pelo outro lado, na fresta entre prédios.
  • Números:
    • De 457 giros, 456 (100%) puseram o centro do Kavo dentro do prédio, até 0,20 m; 26 com o corpo todo (até 1,79 m).
    • Em 2 casos, terminou dentro da konbini (poste x = −6,6).
    • É também a maior porta de entrada em outros roteiros: 95 tentativas de fachada e 77 de corrida na parede entraram no prédio por um giro de poste. Com a parede a 0,5 m, 66 de 172 corridas na parede acabaram num poste.
  • Causa:
    • Os postes ficam em |z| = 4,2 (layout.ts:23, poleZ). As fachadas começam em |z| = 4,5 (layout.ts:14, curbHalf). Isso é 0,3 m de distância.
    • O giro é uma órbita cinemática de raio 0,5 m em volta do eixo do poste (parkour/index.ts:610, 1014), sem colisão (controller.ts:466). Então o centro passa 0,2 m dentro da parede e o corpo, 0,55 m.
    • A saída (parkour/index.ts:1020-1023) é um pulo na direção da tangente, alinhada com a tecla. Com W apontando para o prédio, o Kavo sai para dentro.
    • Como o eixo já passou do plano da parede, a colisão não empurra (causa 6).
    • Agravante já conhecido: pegar o poste no ar com qualquer pulo próximo (movimento-parkour.md #9). Agora a gravidade sobe para 🔴.
  • Repro: repro.ts -- pole2
    • Pés em (−43,32; 0; −4,2), olhando para +x, W segurado, Espaço por 6 quadros quando x ≥ −43,13.
    • Giro no poste N x = −42: a órbita chega a z −4,70.
    • Sai em pulo em (−42,40; 0,83; −4,50) e voa 1,65 m para dentro de N[−45,−37,5]. Pousa em y = 0 dentro do prédio.

🔴 3. Frestas de 0,4–0,5 m entre os prédios: o Kavo cai nelas e escorrega até o chão

  • O que o jogador vê:
    • Andando ou correndo pelo telhado na direção do prédio vizinho mais alto, o Kavo sobe sozinho no parapeito e cai na fresta.
    • Desce 6–9 m entalado entre as duas paredes (estado wallSlide), metade dentro de cada uma.
    • Pousa no beco escuro atrás dos prédios ou na boca da fresta na calçada. Daí só sai virando para o lado certo.
    • É exatamente o "fica preso nos prédios".
  • Números:
    • Caiu na fresta em 370 tentativas (5,9%); 135 delas na travessia de telhado (22,6%).
    • Telhado → vizinho mais alto só com W: 0 de 71 subiram e 37 caíram na fresta.
    • Com Espaço tocado: 5 de 76 subiram e 29 terminaram pendurados no parapeito do vizinho, sem conseguir subir (causa 1).
    • Com Espaço segurado: 30 de 76.
    • Para o vizinho mais baixo, só W funciona sempre: 78 de 78.
  • Causa:
    • world/tokyo/index.ts:48-75: quase todos os vizinhos têm 0,4–0,5 m de fresta. São 17 frestas, e mais duas de 0,4 m em volta da konbini.
    • A cápsula tem 0,7 m de diâmetro (params.ts:10, RAD 0,35) e não cabe. O empurrão das paredes é só horizontal e tem 2 passadas (controller.ts:670-690, collision.ts:191), então as duas paredes se anulam e nada segura o Kavo na vertical.
    • O parapeito tem 0,4 m (buildings.ts:107), menos que o degrau automático de 0,45 m (params.ts:15, STEP_H). Andar contra ele sobe nele sozinho, e o passo seguinte é o vazio da fresta.
    • O linkRoofs só põe degraus no telhado mais baixo quando a diferença é ≥ 1,5 m (buildings.ts:347, index.ts:260-270) e nunca fecha a fresta.
  • Repro: repro.ts -- gap
    • Pés em (−31,5; 6,4; −11,96), no telhado de N[−37,−29,5], olhando +x, W segurado.
    • Em x = −29,35 cai na fresta −29,5..−29,0 e escorrega de 6,4 para 2,3 m em 1,1 s.
    • Na varredura, chega a y = 0 em z −14,4, atrás dos prédios.

🔴 4. Escada de incêndio: chegando em diagonal, o Kavo fica preso no topo para sempre

  • O que o jogador vê:

    • Encosta na escada em diagonal; ela agarra sozinha. Sobe até a última barra e para a 1 m do topo.
    • W não faz mais nada e não há jeito de passar para o telhado. Espaço joga o Kavo de costas, de 12 m de altura.
  • Números (chegou ao telhado, por ângulo de chegada):

    Ângulo Chegou ao telhado Preso no topo
    0° 14 de 16 –
    ±30° 32 de 32 –
    ±60° 1 de 32 31
    Qualquer ângulo + Espaço no meio da subida 10 de 40 – (jogado de costas)
    • Passando ao lado correndo, a 0,4–1,0 m da fachada, a escada agarrou sozinha em 58 de 63. A 1,2 m, nunca. Isso confirma camera.md #4 com números.
  • Causa:

    • Ao agarrar, a frente da escada vira a direção do jogador até o eixo (parkour/index.ts:621-623), e não a normal da escada que o prédio já grava (buildings.ts:325, normal: outward).
    • Chegando a 60°, essa "normal" sai torta, (−0,58; 0,81).
    • No topo, o teste do chão à frente (parkour/index.ts:1126) cai fora do telhado. Então a altura fica presa em topo − 1 m (parkour/index.ts:1138).
    • Espaço na escada sempre pula para trás com 3,5 m/s (parkour/index.ts:1114-1118).
  • Repro: repro.ts -- ladder60

    • Escada x = −24,95 de N[−29,−22,5]. Começa a 1,6 m, a 60°, só W por 10 s.
    • Resultado: estado ladder com os pés em 12,4 para sempre (telhado 12,4, topo da escada 13,4).

🔴 5. Vanish/blink: a quantificação do #4 do movimento

  • O que o jogador vê: inimigo prensado na parede, depois o vanish. A câmera e o Kavo aparecem dentro do prédio.

  • Números:

    Distância inimigo→parede Dentro Corpo todo dentro
    0,25 m 96% (87/91) 96%
    0,4 m 96% 73%
    0,6 m 96% 29%
    0,8 m 63% 0%
    1,0 m 10% 0%
    • Na konbini dá 0%: o vidro fica 1 m recuado sob a fachada.
  • Causa: fighter.ts:1133-1142 põe o destino 1 m atrás do alvo e fighter.ts:1151-1160 chama place() sem nenhum teste.

  • Repro: repro.ts -- vanish

    • Inimigo em (−18; 0; −4,2), o Kavo 3 m na frente. O vanish põe os pés em z −5,2, 0,7 m dentro de N[−22,−14].
    • Andar para a rua tira o Kavo de lá; andar para dentro leva mais fundo.

🔴 6. O amplificador: quem passa do plano da parede não é mais empurrado para fora

  • O que o jogador vê: em vez de "raspar" na parede, o Kavo atravessa e anda dentro do prédio até sair do outro lado. Foram 51 roteiros terminando dentro e 5% com o corpo todo dentro.
  • Causa:
    • collision.ts:201 ignora faces vistas por trás.
    • Os raios só pegam a frente das faces (collision.ts:152). Lá dentro não existe chão até o chão da rua, que passa por baixo de todos os prédios (street.ts:52). Então o Kavo pousa em y = 0 dentro do prédio.
    • Isso só não se nota porque o movimento normal nunca põe o eixo atrás da parede. Todo movimento cinemático põe: poste, vanish, quina, escada torta, corrida para cima atravessando sacada.
  • Sem repro próprio: aparece em todos os repros das causas 2 e 5.
  • O que conserta: depois de todo movimento cinemático, e depois do place(), testar se o ponto está dentro de algum sólido e empurrar para fora pela menor distância. Ou impedir o movimento antes (causa 2).

🟠 7. As rotas "de propósito" para o 1º andar e para o telhado da konbini não funcionam

  • Pilha de ar-condicionado (buildings.ts:333-342):
    • O comentário diz que é a "rota de vault/mantle para o 1º andar", mas falhou em 57 de 70 (81%).
    • O ar-condicionado de parede, de 2,0 a 2,6 m, fica bem em cima do de chão (0,95 m). O ponto onde o Kavo ficaria em pé não cabe a cápsula, e a beirada alta tem 0,34 m (standOk false).
    • O que o jogador vê: o pulo agarra o aparelho de cima e o Kavo fica pendurado com os pés a 0,5 m do chão e as pernas dentro da caixa de baixo. Daí não sobe (causa 1).
    • repro.ts -- acstack: 0 de 6 em cima.
  • Máquinas de venda:
    • As de x = −8,4 e −9,5 ficam sob a laje da sacada, com 1,42 m de altura livre. Não sobe: 0 de 4.
    • A de x = −10,6 sobe pelo agarrar do SM64: 2 de 2.
    • repro.ts -- vending
  • Telhado da konbini pela frente: só 4 de 72 tentativas de fachada chegaram lá.
  • Gravidade: as dicas da NPC (layout.ts:47-53) mandam o jogador justamente por essas rotas: o poste com degraus em x −6,6 (causa 2), a tirolesa, as máquinas de venda e a pilha de AC em x −12,6.

🟠 8. Espaço vira pulo comum se não cair no quadro certo, e o toque curto corta o pulo

  • O que o jogador vê: corre até o beiral, aperta Espaço e o Kavo dá um pulinho de ~1 m ao lado do objeto, em vez de subir.
  • Números:
    • Das 556 falhas ao subir beirais ≤ 2,3 m, 253 (46%) não tiveram nenhum movimento de parkour: o Espaço virou pulo comum.

    • Taxa de falha por jeito de apertar:

      Jeito Falha
      Toque 26%
      Duplo 30%
      Segurado 0,4 s 46%
      Repetido 27%
    • Segurado falha mais porque também liga a corrida lateral na parede (#7 do movimento).

  • Causa:
    • O parkour só aceita o Espaço no quadro em que a beirada está dentro do alcance (parkour/index.ts:276, alcance 0,85 + 0,12·v). Não há antecipação: o mesmo aperto, 1 quadro depois, já virou pulo do SM64 (controller.ts:297 e states.ts:41).
    • Um toque de 100 ms corta o pulo para 25% (states.ts:79, jumpCut), o que dá ~1,1 m.
  • Benchmark:
    • AC Unity segura um gatilho para o parkour e usa botões separados de "subir" e "descer" (Stevivor, GameSpot).
    • O Arno "não tenta escalar nada acima da cintura sem um aperto deliberado" (GamingTrend).

🟠 9. Descer do telhado com Espaço agarra o prédio do outro lado da rua ou o fio

  • Números (chegou à rua):

    Roteiro Chegou à rua Observação
    Só W 84 de 96
    W+toque 58 de 96 12 pousaram no fio e ficaram parados (LIMBO)
    W+Espaço segurado 29 de 96 25 terminaram pendurados (sacada ou parapeito da frente; a corrida + pulo atravessa os 9 m de rua)
  • Causa: o pendurado no ar pega qualquer parede à frente quando a tecla aponta para ela (parkour/index.ts:311, 321-324). No fio, só a tecla ao longo do fio anda (parkour/index.ts:1046); W perpendicular fica parado (30% de LIMBO nos fios).

🟡 10. Volta na quina, andar pendurado e tirolesa entram um pouco na parede

  • Números:
    • 133 de 148 deslocamentos pendurados até a quina dão a volta (cornerWrap); 44 deles passam 4–12 cm para dentro da fachada.
    • Dando a volta na quina perto de uma fresta, o Kavo entra pendurado na fresta (10% do roteiro pendurado).
    • A tirolesa pôs o Kavo dentro de algo em 13 tentativas (11 com o corpo todo): 8 das 108 da própria varredura de tirolesa e 6 de beirais. Das 108 tentativas de pegar a tirolesa, só 54 andaram nela.
  • Causa:
    • parkour/index.ts:845-882 faz um arco sem colisão.
    • Depois disso, detectLedge passa a "achar beiradas" pelas faces internas das lajes coladas na fachada: a plataforma da escada e a sacada.

🟡 11. Pendurado do SM64 em obstáculo baixo põe os pés debaixo da rua

  • Números: raro, 4 casos. Numa pilha de AC de 0,95 m, os pés ficaram em −1,17.
  • Causa: controller.ts:570-573 sempre pendura 2,12 m abaixo do topo, sem o teste de chão que o parkour tem (parkour/index.ts:322-326).

🟡 12. Toldo (trampolim) que alterna sem parar

  • Número: 33 tentativas (11 de fachada, 15 de corrida na parede, 7 de poste) alternaram corrida↔trampolim↔pulo, com ≥ 6 trocas por segundo.
  • Causa: o toldo pula sozinho ao cair nele (parkour/index.ts:646-647).

3. Por que "não tem física e não é fluido"

  1. Todo movimento de parkour é um trilho, não um corpo.
    • A entrada zera velocidade, vy e deslize (controller.ts:460).
    • Depois o parkour escreve a posição direto, "sem colisão de parede nem de chão neste passo" (controller.ts:466-486).
    • O caminho são 3–4 pontos ligados em linha reta; só a altura tem curva (parkour/index.ts:690-703).
    • A duração sai de fórmula fixa, não da animação (parkour/index.ts:370, 395-398).
  2. O impulso morre na entrada e na saída.
    • Velocidade antes → depois, e quanto das transições perde ou ganha mais de 4 m/s num quadro:

      Transição Velocidade Δv > 4 m/s num quadro
      Corrida → escada 7,7 → 1,3 m/s 97%
      Pulo → pendurado 5,3 → 0,9 84%
      Queda → tirolesa 8,3 → 2,4 97%
      Corrida → vault 7,0 → 5,7, mudando de direção 100%
      Corrida → toldo 7,5 → 0 100%
    • Subir de pendurado e sair do topo da escada termina em 0 m/s (parkour/index.ts:810, velocidade 0).

    • A corrida para cima na parede sem beirada sempre termina num chute para trás de 4 m/s (parkour/index.ts:454).

  3. Dentro do movimento a velocidade dá saltos impossíveis.
    • Um corpo sob gravidade muda no máximo 0,47 m/s por quadro (28,4 m/s² a 60 Hz).

    • Pior salto de velocidade num quadro, em média:

      Movimento Média Máximo
      Subir (mantle) 3,0 m/s –
      Giro no poste 5,9 m/s –
      Corrida para cima na parede 8,5 m/s –
      Corrida lateral na parede 18,6 m/s 40,8 m/s
    • Na tela isso aparece como encaixes e "teleportes" curtos: o corpo é puxado para a parede, o pendurado encaixa em 0,22 s (parkour/index.ts:324).

  4. A animação é pose desenhada, não movimento capturado.
    • Cada movimento é um clipe de poucas poses (anim/parkour.ts) tocado pelo progresso do trilho.
    • A mão só gruda na beirada em janelas (anim/parkour.ts:245-256); o pé só gruda pendurado ou na escada.
    • Não existe mistura que parta da velocidade real do corpo.
    • Os jogos citados fazem o contrário:
      • Motion warping (Unreal) entorta o root motion de uma animação capturada até o ponto-alvo. A cápsula continua andando com colisão: "root motion leva a cápsula junto, as colisões continuam funcionando e o personagem não atravessa paredes" (docs da Epic).
      • Motion matching (Simon Clavet, For Honor, GDC 2016) procura a cada instante, no banco de captura, o quadro que combina com a pose atual e com a trajetória desejada. O impulso se conserva por construção.
    • O cabeçalho do código promete "motion-warped" (parkour/index.ts:7-11), mas aqui o "warp" é o trilho sem colisão. É ele que causa as causas 2, 6 e 10.
  5. O controle também tira a fluidez.
    • Espaço decide tudo no quadro exato (causa 8).
    • W pendurado não sobe (causa 1), embora o pendurado do SM64 suba com W (states.ts:238).
    • Escada e poste agarram sozinhos quem só está passando.
    • Dying Light 2 trocou ~50 mil beiradas colocadas à mão por detecção em tempo real (TechRadar, PCGamesN). O Kavo já detecta em tempo real, mas falha nos dois casos que a cidade mais tem: parapeito fino com telhado logo atrás e grade de sacada (causa 1).

4. Ferramentas e como reproduzir

Pasta: /workspaces/art-and-see-monorepo/.local-state/kavo-analise/agentes/parkour-sim/. Rodar sempre a partir de /tmp/kavo-analise/apps/rubro-arena, com npx vite-node <pasta>/<arquivo>.ts.

Arquivo O que faz
harness.ts Mundo real, Collision, Player, Parkour, Input em relógio falso, CameraRig real e máquinas de venda (como main.ts:55). Grava cada caixa de colisão (kit.Solids.box, konbini, máquinas) para o teste exato de "dentro".
lib.ts Detectores: preso, dentro, encaixado, fresta, LIMBO, teste de fuga em 4 direções, teste "pendurado → W → W+Espaço" e transições de velocidade.
attempt.ts Executa uma tentativa: valida o ponto de partida (nunca começa dentro de nada), roda o roteiro, aplica os detectores, faz a fuga e grava um registro JSON.
run-facade.ts 23 fachadas × pontos com semente × andando/correndo × 6 roteiros (W, Espaço 1×/2×/3×, segurado, pulo longo) × ângulo 0/±30/±60°. CAM=fixed repete com a câmera parada.
run-ledges.ts Mapa de alturas a cada 0,25 m (rua e telhados) → 178 grupos de beirais de 0,5–2,3 m × 8 jeitos de chegar.
run-elements.ts Escadas, postes, tirolesas, fios, telhado→telhado, descer, pendurado+quina, corrida na parede e vanish. SWEEP=pole, DUMP=<id>, SUF=-x.
report-data.cjs, report-extra.cjs, agg.cjs Geram as tabelas deste relatório a partir de out/*.jsonl.
repro.ts Repros mínimos, rodados com repro.ts -- <nome>: parapet, pole2, gap, ladder60, vanish, acstack, vending.

Limites:

  • É Node, não o browser. O passo é fixo a 60 Hz, a ordem é a de main.ts, e o toque de Espaço dura 100 ms.
  • Não simulei combate nem inimigos. O vanish foi reproduzido chamando startAction + place() como fighter.ts faz.
  • Cones e lixeiras (só Jolt) não colidem com o player de qualquer jeito (movimento #19).

Fontes

Pesquisa de referências (com links)

Data da pesquisa: 19/10/2026.

Regras deste arquivo:

  • Toda afirmação vem de uma página que eu abri nesta sessão, com o link do lado.
  • (secundária) = abri só um resumo ou reportagem, não a fonte original (vídeo, palestra, tweet).
  • (medido) = número que eu mesmo medi baixando o arquivo da CDN.
  • Inferência = conta ou conclusão minha a partir dos dados, não algo que a fonte diz.
  • No fim de cada tópico há uma lista do que procurei e não achei. Não preenchi esses buracos com memória.

1. Primeiros 60 segundos / ensinar muitos golpes aos poucos

Como os jogos bons fazem

  • Uma ideia por fase, em 4 tempos (kishōtenketsu). Koichi Hayashida, diretor de Super Mario 3D Land, diz que cada fase tem um conceito central "carregado até o fim". A sequência é: aprender a mecânica → usá-la num cenário um pouco mais complicado → "algo maluco acontece" e muda o jeito de pensar nela → demonstrar domínio. Ele liga isso ao kishōtenketsu das tirinhas de 4 quadros e diz que foi Miyamoto quem levou o conceito para a Nintendo. Christian Nutt, Game Developer, 13/04/2012 — https://www.gamedeveloper.com/design/the-secret-to-i-mario-i-level-design
  • Tutorial só compensa quando a mecânica não se descobre sozinha. Estudo com mais de 45.000 jogadores, 8 desenhos de tutorial e 3 jogos. No jogo mais complexo, o tutorial aumentou o tempo de jogo em até 29%. Nos dois jogos simples, não houve ganho significativo. Conclusão dos autores: o investimento em tutorial "pode não se justificar" quando as mecânicas podem ser descobertas experimentando. Andersen et al., CHI 2012 — https://grail.cs.washington.edu/projects/game-abtesting/chi2012/
  • Celeste dá os verbos um por vez no prólogo. Andar e pular primeiro, depois escalar e dar dash. A maior parte do jogo se termina com esses 4 verbos no nível básico. Jonathan Kula, Stanford HCI Game Design blog, 11/04/2023 (secundária) — https://mechanicsofmagic.com/2023/04/11/jonathan-mda-8-kinds-of-fun/
  • Bayonetta transforma o loading em sala de treino. Ao carregar cada área ou capítulo, o jogo dá um tempo curto para praticar combos. Os comandos aparecem à direita da tela, para testar em tempo real. Nintendo Life — https://www.nintendolife.com/features/18-best-credits-sequences-and-loading-screens-that-let-you-do-stuff?page=2
  • DMC5: assistência que ensina, não um "modo fácil". O produtor Matt Walker diz que o Auto-Assist "não é um modo fácil". Ele mostra como fazer combos e subir o rank de estilo, para o jogador se sentir capaz e "querer tentar sozinho". Itsuno conta que um auto-esquivar anterior foi cortado porque "era um botão de modo fácil". DualShockers, 22/10/2018 — https://www.dualshockers.com/devil-may-cry-5-interview-ps4-xbox-one-pc
  • Hi-Fi Rush: o jogo sincroniza o golpe no ritmo pelo jogador. O diretor John Johanas diz que a ideia inicial era um sistema que "sincroniza suas ações com o ritmo para você". Ele não queria que quem não tem ritmo ficasse travado. O resultado tem dois níveis, "sucesso" e "sucesso maior", e não passa/falha. Press Start, 2023 — https://press-start.com.au/?p=142818
  • Arkham: menos combinações de botão para decorar. Sefton Hill (via IGN 2009) diz que o jogador "não fica limitado por ter que aprender uma série de combinações de botões" para fazer um golpe. VidaExtra (secundária, cita IGN e um AMA de 2017) — https://www.vidaextra.com/industria/conocemos-como-combate-batman-arkham-assassins-creed-quien-invento-realmente
  • Lista de golpes no pause. Super Mario Odyssey tem um "Action Guide" no menu de pause com "todos os golpes" disponíveis. iMore — https://www.imore.com/tips-and-tricks-nintendos-super-mario-odyssey

O que costuma dar errado

Não achei: fonte primária sobre o onboarding de Hades ou A Hat in Time (só achei material sobre Early Access de Hades). Também não abri nenhum vídeo do GMTK.

O que isso significa para o Kavo: nos primeiros 60 segundos, um verbo por vez num espaço que obriga a usá-lo (andar → pular → J → pulo duplo → K → L → Z), na ordem de 4 tempos do Mario. A lista completa de golpes fica no pause e, se houver tela de carga, ela vira treino de combo como em Bayonetta. Um texto explicando tudo de uma vez não segue esse padrão.


2. Game feel / "juice" e o perigo de exagerar

Receitas concretas

  • Vlambeer, um tiro de pistola em Nuclear Throne: som; cápsula ejetada a 2–4 px/frame; projétil a 16 px/frame; câmera recua 6 px; "+4 no screenshake", que decai rápido; coice da arma = 2; muzzle flash fingido no 1º frame; inimigo empurrado 3 px/frame; 1 frame branco ao ser atingido; o jogo congela ~10–20 ms a cada acerto. Tudo a 320×240 e 30 fps. Tom Armitage, Infovore, 22/10/2013, citando Jan Willem Nijman — https://infovore.org/?p=5275
  • Sakurai sobre hitstop: "os dois lados congelam" pelo mesmo tempo. Em geral, quanto mais dano, mais longo o hitstop. Quem apanha treme durante o hitstop, mas "as hurtboxes ficam paradas". Em Smash, quem bate também treme um pouco. A transição do flinch para a animação de dano leva 4 frames. Coluna da Famitsu traduzida por fãs (secundária: a própria tradução avisa que pode não ser exata) — https://sourcegaming.info/2015/11/11/thoughts-on-hitstop-sakurais-famitsu-column-vol-490-1/
  • Celeste, "perdão invisível": coyote time, jump buffering, gravidade pela metade no topo do pulo, correção de quina no pulo e no dash, janela de wall-jump de 2 px (a 320×180) e de super wall-jump de ~5 px. Tudo "fudged a tiny bit in the player's favor". Maddy Thorson — https://maddymakesgames.com/articles/celeste_and_forgiveness/
  • O que mais pesa na sensação de impacto: estudo de 2022 com comentários da Steam e um modelo de linguagem, comparando os 8 jogos de ação com melhor sensação de impacto contra os 8 piores. Os fatores que mais pesam são hit stop, coerência do som e controle de câmera. Descuidar de um deles já prejudica o resto. Lin, Duan, Wen & Cai (2022) — https://arxiv.org/abs/2208.06155
  • God of War 2018, câmera colada: nos testes, os jogadores ficavam ansiosos quando inimigos saíam da tela. A solução foram setas na borda (vermelha = ataque vindo, branca = inimigo parado) e um limite de só 2 inimigos agressivos ao mesmo tempo. Os inimigos agressivos ficam num crescente à frente do Kratos. Palestra GDC 2019 de Mihir Sheth. (secundária: resumo automático do vídeo) — https://videohighlight.com/v/hE5tWF-Ou2k (vídeo: https://www.youtube.com/watch?v=hE5tWF-Ou2k)
  • Spider-Man (Insomniac): um "attacker token" passa de inimigo em inimigo para ditar o ritmo, sem deixar o grupo amontoar no herói e sem deixar o ataque raro demais. Depois de uma esquiva, um timer curto deixa o jogador encaixar golpes. Inimigos fora da tela demoram mais para atirar, o que dá ao Spidey-sense uma fração de segundo a mais. Os testadores reclamavam de "golpes baratos" até ajustarem isso. Alissa McAloon, Game Developer, 18/03/2019 — https://www.gamedeveloper.com/programming/designing-ai-to-do-anything-a-spider-can-in-i-marvel-s-spider-man-i-

O perigo de exagerar

Não achei: estudo ou pós-morte sobre onomatopeia na tela atrapalhando a leitura. Também não abri a palestra original "The Art of Screenshake" nem o vídeo de hitstop do Sakurai (só a coluna traduzida).

O que isso significa para o Kavo: o hitstop deve crescer com o dano e o golpe precisa de som coerente com o impacto. No máximo 2 inimigos atacam por vez, com seta na borda para quem está fora da tela. Shake, CRT e onomatopeia ficam no nível "médio", ligados ao acerto do jogador, e cada um ganha um slider próprio. Os estudos mostram que o extremo piora o jogo.


3. Escopo: um loop excelente antes de muitos sistemas

Exemplo de muitos sistemas e pouca profundidade: Biomutant (2021)

Sobre IA e a sensação de progresso

  • No estudo controlado da METR (início de 2025), devs experientes levaram 19% mais tempo com ferramentas de IA. Eles previam ficar 24% mais rápidos e, depois, ainda achavam que tinham ganhado 20%. Foram 16 devs e 246 tarefas. Os autores dizem que o resultado é um retrato do início de 2025 e já está desatualizado. METR, 10/07/2025 — https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/
  • David Holland passou cerca de um ano construindo o visual 3D pixel-art "inspirado no t3ssel8r" e conclui: "literalmente não tenho gameplay no momento, então isso deveria vir a seguir". 24/09/2024 — https://www.davidhol.land/articles/3d-pixel-art-rendering/ (lido via cópia do Wayback, porque o site bloqueia robôs)

Não achei: pós-morte confiável de um jogo indie solo morto por excesso de features. Os resultados de busca eram blogs genéricos (wayline.io), que descartei.

O que isso significa para o Kavo: o loop de 30 segundos é "onda de inimigos → combo J/K/L com pulo duplo → arremessar máquina". Ele deve ficar excelente isolado (uma arena cinza, como o FreeFlow) antes de mais missões ou mais 7-Eleven, e a variedade vem de mudar o contexto, não de empilhar sistemas. Com 15 agentes em paralelo, o risco é o do Biomutant (muitos sistemas, lock-on frouxo, pouco feedback). A METR mostra que a sensação de "andou rápido" com IA pode enganar.


4. Visual 3D pixel-art em baixa resolução

  • A técnica do t3ssel8r explicada por quem a reproduziu (Godot):

    • Contornos por detecção de borda nos buffers de profundidade e normais, com kernel de 4 vizinhos para ter 1 pixel de espessura. Destaque só nas bordas convexas.
    • Câmera em 2 passos: (1) prender a câmera numa grade do tamanho do texel, alinhada à vista, o que elimina o "pixel creep"; (2) deslocar a imagem final na tela pelo erro desse encaixe, o que devolve o movimento suave.
    • Luz toon simples. No caso dele, o god-ray custou ~1,7 ms numa GTX 1060 a 640×360.

    David Holland, 24/09/2024 — https://www.davidhol.land/articles/3d-pixel-art-rendering/

  • Esse truque só funciona bem com câmera ortográfica. Na ortográfica, todo pixel se desloca igual e um único encaixe corrige todos. Na perspectiva, "pixels em profundidades diferentes andam em ritmos diferentes, e nenhum encaixe único corrige todas as profundidades". O artigo propõe o "texel splatting" (cubemap a partir de um ponto fixo), que ainda tem limitações em aberto. Dylan Ebert, arXiv, 15/03/2026 — https://arxiv.org/abs/2603.14587

  • O plugin UPixelator (Unity) confirma: "só a câmera ortográfica tem o benefício da redução de pixel creep". Objetos que se movem tremem, a menos que também sejam encaixados na grade. https://github.com/Radivarig/UPixelator_Documentation

  • A Short Hike (jogo lançado, feito quase sozinho): a ideia era usar "o menor número de pixels possível", com sombreamento chapado, sem anti-aliasing e com contorno suave para a leitura. Como alguns jogadores não gostaram, o dev adicionou uma opção para ajustar o tamanho do pixel. Adam Robinson-Yu, PlayStation Blog, 05/08/2021 — https://blog.playstation.com/2021/08/05/crafting-a-tiny-open-world-a-look-behind-the-scenes-at-the-creation-of-a-short-hike/

  • Contorno em tudo destrói a leitura. Um vídeo técnico sobre contornos em 3D pixel-art usa máscara por camada porque contornar todos os objetos "destrói a legibilidade". A silhueta vem da profundidade e o vinco das normais. Transcrição da série "Pixel Perfect" (autor não identificado na página) — https://sozai.app/transcript/drawing-the-line-on-3d-pixel-art-pixel-perfect-transcript/index.md

  • Contraexemplo, resolução baixa sem cuidado: Lethal Company roda a 860×520 internos. Um patch da comunidade para epilepsia existe para reduzir o "flicker de textura e geometria causado pela resolução interna muito baixa". https://thunderstore.io/c/lethal-company/p/GFG/EpilepsyPatch/

  • Paleta: o portfólio 2025 de Bruno Simon tira as cores de uma textura de paleta compartilhada, não de vertex colors. Awwwards — https://www.awwwards.com/brunos-portfolio-case-study.html

Não achei: fonte confirmando que Lone Ruin tem visual pixel. A página da Steam o marca como "2.5D, Isometric, Stylized" (https://store.steampowered.com/app/1452070/). Unrailed e Cassette Beasts não foram verificados e ficaram de fora. Também não abri os vídeos do próprio t3ssel8r.

O que isso significa para o Kavo: com ~270 px no lado curto e câmera de ação em perspectiva, encaixar a câmera na grade não elimina o tremido como no t3ssel8r, que usa câmera ortográfica. Para isso, os caminhos são câmera ortográfica (ou, por inferência minha, perspectiva com FOV bem baixo, que se aproxima dela) mais encaixe e deslocamento de subpixel. Fora isso, contorno só em personagens e objetos interativos, paleta fechada e um slider de tamanho de pixel como em A Short Hike. Antes de mexer, vale um teste de flicker numa panorâmica lenta.


5. Jogos 3D de navegador: tamanho, carregamento e desempenho

Tamanho e espera

Como os bons são montados

Suporte e hardware

Física em wasm (custo de download)

Não achei: confirmação de que o Krunker usa three.js (a página da Wikipedia deu 404 e as outras fontes eram fracas). Também não achei benchmark Rapier × Jolt no navegador.

O que isso significa para o Kavo: os 6,6 MB ficam abaixo dos 10 MB onde a Poki mediu perda de ~20% nos EUA. Mesmo assim, o jogo deve abrir jogável em menos de 10 s, com o mínimo primeiro e o resto depois. Movimento e física precisam usar dt real ou passo fixo (MDN, Fiedler). Inferências: se algo andar por frame, em 144 Hz fica 144/60 = 2,4× mais rápido. A ~270 px de altura, o preenchimento de pixels deixa de ser gargalo até em GPU integrada, e o custo passa a ser CPU, física e draw calls. Se o Kavo embute física wasm, ela sozinha pode responder por 0,7–1,1 MB comprimidos, e embutida como base64 cresce ~33%. Vale medir o que ocupa os 6,6 MB.


6. Esquema só de teclado para ação 3D

Não achei: evidência boa sobre jogos de luta tipo plataforma (Rivals, Brawlhalla) no teclado, só reclamações soltas em fóruns. Também não abri a página oficial da Capcom sobre os controles Modern.

O que isso significa para o Kavo: J/K/L + WASD tem precedente forte (2XKO), mas Z-target + W + J + espaço já são 4 teclas juntas. Isso cai no ghosting de teclados baratos (caso DMC5). O Kavo deve evitar golpes que exijam 3 ou mais teclas além do movimento e usar event.code. O Shift como tecla de jogo pode abrir o Sticky Keys, e golpes de giro com WASD em sequência são difíceis no teclado.


7. Hospedar um HTML único com link público e sem login

Serviço Limite que importa para 1 arquivo de 6,6 MB Fonte
itch.io Aceita um único .html autocontido sem zip, desde que ele não referencie outros arquivos. Num zip: index.html na raiz, até 1.000 arquivos, 200 MB por arquivo, 500 MB no total, nomes sensíveis a maiúsculas. Tem modo "embed na página" ou "clique para tela cheia". https://itch.io/docs/creators/html5
Cloudflare Pages Até 25 MiB por arquivo em todos os planos. Plano grátis: 20.000 arquivos e 500 builds por mês. Upload direto arrastando a pasta no painel, sem git (até 1.000 arquivos por esse caminho). https://developers.cloudflare.com/pages/platform/limits/ · https://developers.cloudflare.com/pages/get-started/direct-upload/
Cloudflare Workers (assets estáticos) "Requests to static assets are free and unlimited." https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/
GitHub Pages Site publicado até 1 GB, banda com limite flexível de 100 GB/mês, 10 builds/hora. Proibido usar como hospedagem grátis de negócio online ou e-commerce. https://docs.github.com/en/pages/getting-started-with-github-pages/github-pages-limits
Netlify (Free) 300 créditos/mês com limite rígido: deploy de produção = 15 créditos, 1 GB de banda = 20 créditos. Quando acabam, todos os sites ficam pausados com "Site not available". https://www.netlify.com/pricing/ · https://docs.netlify.com/manage/accounts-and-billing/billing/billing-for-credit-based-plans/how-credits-work/

Inferência (contas minhas, sem considerar compressão):

  • Netlify: 2 deploys por mês consomem 30 créditos. Os 270 que sobram dão ~13,5 GB, ou ~2.000 cargas completas de 6,6 MB. Depois disso o site sai do ar até o mês virar.
  • GitHub Pages: 100 GB por mês dão ~15.000 cargas.
  • Cloudflare: não tem teto de banda documentado para assets estáticos.

O que isso significa para o Kavo: o caminho mais seguro e sem custo é a Cloudflare (Pages com upload direto, ou Workers com assets estáticos): 6,6 MB cabem nos 25 MiB e requisição de asset estático é grátis e ilimitada. O itch.io é a segunda opção, porque aceita o .html único direto e já dá página de jogo pública. O Netlify Free é arriscado: um post que viralize pode pausar todos os sites da conta.

Pesquisa: câmera e transformação (com links)

Data: 19/10/2026. Toda afirmação abaixo vem de página que eu abri, com o link ao lado. Quando a fonte é secundária (resumo automático, guia de fã, fórum), está marcado. O que é proposta minha, e não fato de fonte, aparece como proposta ou inferência.

Como li: abri as páginas com WebFetch, que devolve um resumo, ou baixei o HTML com curl e li o texto bruto. As citações entre aspas em inglês saíram do texto bruto (livro do Haigh-Hutchinson, Iwata Asks, Critical Video Game Studies e TV Tropes). O resto está parafraseado. O TV Tropes bloqueia acesso direto, então li a cópia de 2024 do Wayback Machine. Cópias brutas das páginas usadas ficaram em agentes/pesquisa-cam-fontes/.

O que não achei ou não consegui abrir está no fim, na seção "Lacunas".


PARTE 1: CÂMERA

O dono diz que a câmera é "porca e confusa, toda hora preciso ajustar manualmente". As fontes concordam em três pontos:

  1. O jogador não deveria precisar mexer na câmera para jogar.
  2. A câmera boa é a que ninguém nota.
  3. Quase todo problema vem de movimento que o jogador não pediu.

1.1 Regras gerais

John Nesky, "50 Camera Mistakes" (GDC 2014, Journey)

Não existe transcrição oficial. A melhor fonte escrita é uma tradução em russo da palestra no Habr, em duas partes, com os 50 itens numerados. Uso a numeração do Habr. A Game Developer confirma a palestra, de 2014, e linka o vídeo gratuito: https://gamedeveloper.com/design/video-50-common-game-camera-mistakes----and-how-to-fix-them

Os itens que importam para o Kavo:

# Erro Correção do Nesky
4 Distância errada Ficar perto o bastante para que a linha câmera→personagem seja difícil de bloquear
5 Obstáculo dos lados Leque de raios para ver o obstáculo cedo e contornar suave, sem pulo
6 Afastar a câmera quando o jogador vira para a parede A intenção do jogador vem primeiro. Aproximar devagar até o obstáculo ficar atrás da câmera
8 Forças brigando pela câmera Agrupar por grau de liberdade (yaw, pitch, distância, FOV) e dar prioridade. O jogador ganha sempre
9 Poste fino tapando a vista Marcar o que "pode tapar" e o que "não pode". Atravessar ou ignorar os finos
12 Obstáculo atrás da câmera Raio para trás. Se houver algo, chegar mais perto do personagem, não girar de lado
13 Câmera colada corta o personagem Distância mínima segura. (A paráfrase da Skillbox acrescenta: deixar o personagem semitransparente)
14–16 Distância, FOV e inclinação soltos Distância e FOV como função da inclinação: olhando para cima, chega mais perto. Olhando para baixo, recua
18 Corte que muda o sentido dos controles Corte só em momento seguro, ou interpolar o sentido do controle entre os planos
20 Quebrar a regra dos 180° Não inverter o eixo. Depois de cutscene, voltar devagar para a câmera de jogo
22 Deixar o jogador cuidar da câmera Andar e girar câmera ao mesmo tempo é difícil. A câmera tem que prever o que ele precisa ver
23 Não girar enquanto corre Girar para a velocidade real do personagem, não para o input. Na parede invisível, girar para mostrar a saída
25 Câmera reta na beirada Raio para baixo à frente. Na beirada, inclinar para mostrar a queda
27 Regra dos terços na hora errada Personagem no centro enquanto se move (o jogador usa o centro para mirar a direção). Deslocar só parado, como em Journey
29 Só câmera procedural Dicas colocadas pelo designer, sobretudo em espaço fechado
31 Girar para alvo perto Transladar de lado ou para trás. Girar para alvo perto faz a câmera rodopiar
32 Transladar para alvo longe Para alvo longe, girar
34–35 Dar o controle e tomar de volta Esperar. Melhor: só retomar depois que o jogador começa a andar
40 Câmera solta demais Suavizar, mas com limite rígido de quanto o personagem sai do centro
42 FOV mudando rápido Evitar zoom brusco e FOV que abre na aceleração. Dá enjoo
43–44 Tremida Slider de intensidade. Tremida de passo, quase nula
45 Seguir o pulo na vertical Não seguir cada pulo. Segurar a altura ou esperar o pouso num nível diferente, como no Mario
46 Mover a câmera rápido Se tem que ir longe, corte seco. Movimento rápido enjoa tanto quanto corte
50 Um algoritmo universal Não resolver tudo com uma função de otimização. Iterar regra por regra, sabendo a prioridade de cada uma

Fontes: parte 1 (itens 1–25) https://habr.com/en/post/272401 · parte 2 (itens 26–50) https://habr.com/en/post/272825 · paráfrase em russo da Skillbox, com outra numeração https://skillbox.ru/media/gamedev/50-principov-vazhnyh-dlya-sozdaniya-horoshey-igrovoy-kamery/

Na Skillbox aparece um item que o Habr não destaca: em brawler com muitos inimigos, afastar a câmera para ela não ficar pulando de alvo em alvo.

Mark Haigh-Hutchinson, "Real-Time Cameras" (2009; câmeras de Metroid Prime)

Li o texto do capítulo 4, "Camera Design Guidelines" (pp. 132–136), numa cópia online do livro. As regras, literais:

  • "Attempt to keep the player character in view." Ocultação parcial ou breve pode. Total, não.
  • "A passable solution to avoid this problem is to apply transparency effects to the geometry in question." Isso quando a câmera encostaria na geometria.
  • "Do not require the player to manipulate the camera simply to play the game — unless it is a design requirement. For third person presentations, the camera system should be able to choose the most appropriate solution automatically."
  • "Minimize unintentional camera motion whenever possible." E também: "if camera motion is directly linked to that of the player, it often results in an 'unnatural' or 'stiff' feeling."
  • "Limit the reorientation speed of the camera." Reorientação instantânea só com um corte óbvio, mantido tempo bastante.
  • "Retain control reference frames after rapid or instantaneous camera motion." Se não, o personagem anda para onde o jogador não quis.
  • Fail-safe: reposicionar de vez é aceitável quando "the camera has lost sight of the player for an extended period (say 2 seconds)".
  • "Do not focus directly on the player character when it is moving … we need to be looking ahead of the character motion."
  • "Avoid enclosed spaces with complex geometry (third person cameras) … Small enclosed spaces will require the camera to be moved outside the environment."
  • Na introdução: "An ideal virtual camera system … is notable by the lack of attention given to it by the viewer." No cap. 1: "the simplest camera solution is usually the best."
  • No cap. 2: câmera "behind the character", com "rotational lag around the vertical axis", e "Visible feet", uma distância média que mostra os pés, "often important to judge jumping".

Trecho oficial sobre oclusão: raios que não chegam ao alvo empurram a câmera. Prefira molas amortecidas a restrições duras, porque restrição dura dá movimento descontínuo. A câmera é uma esfera um pouco maior que o near plane. Câmeras preditivas navegam melhor que as reativas. Game Developer, excerto do livro: https://gamedeveloper.com/design/real-time-cameras---navigation-and-occlusion · prefácio, com a lista de defeitos que as resenhas criticam ("cameras that stutter, lag, shake…, pass through walls, or point in the wrong direction"): https://www.gamedeveloper.com/programming/-real-time-cameras-and-mark-haigh-hutchinson

3Cs (Character, Controls, Camera)

A própria Nintendo admite que câmera é guerra

1.2 Câmera que segue em plataforma

  • Super Mario 64 (Lakitu): os Lakitu Bros. filmam "from the recommended angle", os botões C mudam o ângulo e o R troca o modo. Quando não dá para mexer mais: "If we can't adjust the view any further, we'll buzz." MarioWiki: https://www.mariowiki.com/Lakitu_Bros. · O 64 tinha "the C-Left and C-Right buttons to rotate Lakitu around Mario". Critical Video Game Studies: https://criticalvideogamestudies.com/retro-review-the-legend-of-zelda-the-ocarina-of-time/
  • Odyssey: Miyamoto diz que a câmera do 64 funcionava bem por juntar o automático com o ajuste nos botões C, e que é "very difficult to find the right balance". A equipe quis controles de câmera "really really smooth". MarioWiki (compilação, secundária): https://www.mariowiki.com/User:Shokora/Odyssey_development
  • Pulo não mexe a câmera:
  • Olhar à frente:
    • Haigh-Hutchinson manda olhar à frente quando o personagem se move.
    • Nesky #27 manda manter o personagem no centro enquanto há input.
    • Nesky #40 manda ter um limite rígido de quanto ele sai do centro.
  • Devlog de um jogo estilo N64 (Beeknighted):
    • O giro horizontal segue a direção do movimento, e "It turns more aggressively the harder the character turns".
    • Andar para a câmera ou para longe dela vira zoom, não giro.
    • A altura é suavizada, porque "It looks awkward if the camera bounces up and down in lock step".
    • Testaram deslocar a vista para a frente e cortaram: depois de uma curva fechada sobrava menos espaço visível, e o jogador brigava com a câmera.
    • "Tapping a button once to reset the camera behind the player is a faster method than any automatic solution."
    • Afastar da parede, segurar o seguir e recentrar por tempo também foram cortados.
    • https://microtonalmatt.itch.io/beeknighted/devlog/1083699/building-a-better-camera-part-1
  • A Hat in Time e Astro Bot: não achei fonte de design de câmera (ver Lacunas).

1.3 Jogador só no teclado (sem mouse e sem analógico direito)

1.4 Espaços apertados (ruas estreitas, becos)

  • Parede puxa, objeto pontilha:
    • Um rig de câmera para Godot usa "Snap in, ease out": entra em ~0 s, para nunca ver dentro da parede, e volta em 0,5 s.
    • Ignora oclusores finos (poste, grade), salvo se a câmera fosse acabar dentro da geometria.
    • Esfera de raio 0,2. Folhagem e vidro não puxam a câmera.
    • Quando a câmera está a menos de 1 m, o personagem começa a sumir (some de todo a 0,4 m).
    • https://www.strayspark.studio/docs/godot-camera-system/third-person-and-deocclusion
  • Pontilhar em vez de transparência: o componente SeeThrough do Needle faz o objeto entre a câmera e o alvo sumir por "alpha hashing", e assim "the object can stay opaque" na renderização. https://engine.needle.tools/docs/api/@needle-tools/engine/5.0.10/classes/Built-in_Components.SeeThrough.html
  • Haigh-Hutchinson:
    • Transparência na geometria próxima.
    • Fail-safe de 2 s com o personagem escondido.
    • Fugir de espaço fechado com geometria complexa já no level design.
  • Nesky #12: obstáculo atrás da câmera, chegar mais perto. Ir de lado desorienta.
  • Yakuza 0: "Once an encounter begins, bystanders form a fighting arena, locking you in until the enemy is defeated or you are." A briga de rua ganha borda natural, e a câmera ganha um espaço conhecido. Ludo.guide: https://www.ludo.guide/guide/yakuza-0/basic-combat
  • Sifu: usa móveis e mesas como parte da luta ("tables you can leap across") e quer o jogador "surrounded and challenged, but not overwhelmed". PlayStation Blog: https://blog.playstation.com/?p=356872

1.5 Combate com muitos inimigos

God of War (2018), a fonte mais completa

Câmera colada, com várias soluções em volta. Fonte: resumo automático do vídeo da palestra GDC 2019 de Mihir Sheth (secundária): https://videohighlight.com/v/hE5tWF-Ou2k (vídeo: https://www.youtube.com/watch?v=hE5tWF-Ou2k)

  • Ansiedade: nos testes, os jogadores ficavam ansiosos quando o inimigo saía da tela.
  • Inimigo só sai da tela por ação do jogador. Inimigo agressivo fora da tela não volta sem ser reconhecido e guarda o quadrante onde estava.
  • Setas na borda: vermelha para ataque vindo, branca para inimigo parado.
  • Posição dos inimigos: os agressivos ficam num crescente à frente do Kratos e os outros fazem roda. Só 2 agressivos ao mesmo tempo, por fichas.
  • "Camera strafe assist" mantém os alvos relevantes na tela durante o movimento.
  • "Strike assist": inimigo atingido gira para a direção da câmera, e golpes seguidos o puxam para o quadro.
  • Mira suave: se existe inimigo viável, o Kratos vai nele. O "suck to target" diminui com o ângulo.
  • Lock tardio: com flick do analógico para trocar de alvo.

PlayStation Blog, outros estúdios sobre o GoW: "a further pulled back camera to enable greater field of view … it greatly improves legibility of targets", e "Camera shake is also significantly reduced". https://blog.playstation.com/?p=370399

Outros jogos

1.6 Diálogo e interior

  • Diálogo:
    • Regra dos 180° (Nesky #20).
    • Corte em vez de varrida rápida (Nesky #46).
    • Volta lenta para o jogo, mantendo o sentido do controle (Nesky #18, #20, e Haigh-Hutchinson).
  • Interior pequeno: o Haigh-Hutchinson diz que espaço pequeno pede câmera "fora do ambiente" e transparência na geometria próxima. Ele lista câmera "Path-based", presa a um trilho, e "Stationary" como soluções clássicas. O Nesky #29 pede dicas do designer em espaço fechado.
  • Nintendo: câmera fixa a distância fixa "less chance of getting lost" (3D World), e câmera que se move sozinha pode dar enjoo (Galaxy).

1.7 O que eu vi no código atual (inferência minha, não testei jogando)

Li src/camera.ts e src/player/controller.ts da cópia em /tmp/kavo-analise/apps/rubro-arena (pode sumir no restart). A câmera já tem muita coisa certa:

  • pulo sem quique;
  • olhar a beirada;
  • parede puxa e objeto pontilha;
  • Shift recentra;
  • trava do sentido do controle (yawLatch).

Mesmo assim, as fontes apontam cinco causas prováveis para o "toda hora preciso ajustar":

  1. A câmera não segue quando você anda de lado ou na diagonal.
    • O auto-giro só age com o input apontando para longe da câmera (sideW, camera.ts:307–357). Segurar A ou D nunca traz a câmera para trás.
    • No teclado, que tem 8 direções, isso acontece o tempo todo.
    • O controle já trava a direção enquanto a tecla está apertada (controller.ts, yawLatch). Então girar a câmera nessa hora não faria o personagem andar em círculo.
    • Nesky #23: girar para a velocidade real. Unity, "estilo Zelda": travar a direção e girar devagar.
  2. O giro é lento e cheio de condição:
    • no máximo 0,5 rad/s, ou seja, ~29°/s, e uma curva de 90° leva mais de 3 s;
    • 2 s de espera depois de cada Q/E;
    • recentrar só depois de 3 s em linha reta.
    • Nesky #35 sugere retomar quando o jogador volta a andar, não por relógio.
  3. Muitas forças ao mesmo tempo:
    • "bigodes" que giram o yaw no beco;
    • inclinação para as ameaças (até 2,5 m no ponto de mira);
    • +1,6 m de distância e +5° de FOV com ameaça;
    • FOV que abre no sprint;
    • punch de −9° de FOV;
    • recuo na velocidade, dicas e tremida.
    • Nesky #8 e #50 dizem que forças sem prioridade clara deixam a câmera errática, e o #42 condena FOV mudando rápido.
  4. No beco, a câmera gira de lado para fugir da parede (camera.ts:437–454). O Nesky #12 manda chegar mais perto, e o Haigh-Hutchinson manda minimizar o movimento que o jogador não pediu.
  5. A câmera gira o ponto de mira na direção do inimigo que está perto (threatBias). O Nesky #31 diz que girar para alvo perto faz a câmera rodopiar. Para alvo perto, transladar ou recuar.

Como a câmera do Kavo deveria ser

Spec por momento. Os números marcados proposta são ponto de partida para afinar, não valores tirados de fonte.

Princípio: uma câmera simples (Haigh-Hutchinson, "the simplest camera solution is usually the best"). Prioridade fixa: jogador > colisão > lock > seguir > enfeite (Nesky #8). FOV fixo fora de cutscene (Nesky #42).

A. Andando e correndo (hominho ou Kavo)

  • Seguir pela velocidade, em qualquer direção, inclusive de lado, com o sentido do controle travado enquanto a tecla estiver apertada (Nesky #23; Unity "estilo Zelda"; Haigh-Hutchinson).
  • Velocidade de giro proporcional à curva (devlog Beeknighted). Proposta: até ~1,5 rad/s correndo, com 0,3 s de atraso no começo da curva ("rotational lag").
  • Não girar quando o personagem vem para a câmera. Aí vira zoom (Nesky #12; devlog).
  • Parado, a câmera fica parada (Haigh-Hutchinson, minimizar movimento).
  • Depois de Q/E, a câmera volta a seguir quando o jogador anda de novo, não depois de 2 s (Nesky #35).
  • Q/E: toque gira 45° com easing, como os botões C girando o Lakitu (CVGS). Segurar gira contínuo. Proposta: 45°.
  • Shift sem alvo: recentra atrás em 0,3 s, o Z do Ocarina. Já existe, manter.
  • Olhar à frente: sim, com limite de quanto o personagem sai do centro (Haigh-Hutchinson; Nesky #40). Com tecla apertada, personagem perto do centro (Nesky #27).
  • Distância: o hominho é menor e mais lento, então câmera mais perto que a do Kavo. Proposta: ~0,8× a distância do Kavo ("visible feet" e Nesky #4: perto o bastante para a rua não tapar).
  • Sombra sempre no chão, embaixo do personagem e dos inimigos no ar (regra dos Mario 3D).

B. Pulo e parkour

  • Altura parada no pulo. Só sobe ou desce quando pousa noutro nível ou sai da faixa (Nesky #45; platform snapping do Keren). Já existe, manter.
  • Na beirada, inclinar para mostrar a queda (Nesky #25). Já existe.
  • Escalada: sem auto-giro. Enquadrar a próxima pegada com dica do designer (Nesky #29). Virar para a rota só com o jogador em movimento (Nesky #35).

C. Rua estreita e beco

  • Parede atrás: chegar mais perto e subir (grua), sem girar de lado (Nesky #11, #12). Snap in de ~0 s e ease out de ~0,5 s (rig Godot).
  • Poste, placa, fio, toldo, máquina de venda: pontilhar, marcados como "não tapa" (Nesky #9; Needle; Haigh-Hutchinson).
  • Câmera a menos de ~1 m: pontilhar o próprio personagem (Nesky #13; rig Godot).
  • Fail-safe: personagem escondido por 2 s, corte para um ponto livre mantendo o sentido do controle (Haigh-Hutchinson).
  • Level design: rua com largura de pelo menos a distância mínima da câmera e sem cantos fechados (Haigh-Hutchinson; Nesky #2).

D. Dentro do 7-Eleven

  • Câmera num trilho ao longo do corredor principal, alta, em três quartos (Haigh-Hutchinson, "path-based"). Teto e a parede mais próxima pontilhados, prateleiras também.
  • Sem auto-giro. Q/E troca entre 2 a 4 ângulos fixos (lógica do 3D World: câmera fixa perde menos o jogador e enjoa menos).
  • Entrar e sair: corte seco, nunca varrida, com a porta alinhada para que W continue querendo dizer "para dentro" (Nesky #18, #46; Haigh-Hutchinson).

E. Combate sem lock

  • Recuar um pouco para caber o grupo (PlayStation Blog sobre o GoW; Skillbox sobre brawlers). Proposta: +20% de distância, sem mudar o FOV.
  • Não girar para o inimigo perto. Transladar ou recuar (Nesky #31).
  • Setas na borda: vermelha para ataque vindo, branca para inimigo parado (GoW). Ícone acima da cabeça de quem vai bater (Arkham).
  • IA: no máximo 2 agressivos e os outros esperam (GoW; Koizumi no Ocarina). Inimigo fora da tela não ataca sem aviso. Inimigo jogado para fora da tela gira de volta para o quadro (GoW, strike assist).
  • Mira suave: J vai no inimigo da direção do input, com puxão que diminui com o ângulo (GoW).
  • Tremida pequena. Impacto pelo hit-stop e pelo som, não pelo FOV (GoW reduziu a tremida; Nesky #42).

F. Com lock (Shift segurado)

  • Atrás do jogador, na linha jogador→alvo, com uma órbita curta para os dois ficarem visíveis. Já existe. Movimento vira círculo em volta do alvo, a kusarigama do Ocarina.
  • Trocar de alvo com Q/E, que ficam livres durante o lock: o próximo à esquerda ou à direita (flick do GoW, L3 do DMC5, troca rápida do Sifu). Proposta.
  • Troca de alvo com panorâmica de ~0,25 s, sem corte. Ao soltar o lock, a câmera fica onde está, sem estalo de volta (Haigh-Hutchinson, limitar reorientação).

G. Diálogo

  • Corte para um plano por cima do ombro, com o rosto da atendente livre. O plano atual põe as antenas na frente dela (auditoria #16). Regra dos 180° (Nesky #20).
  • Ao sair, voltar devagar para o jogo, sem mudar o sentido do controle (Nesky #18, #20).

H. Henshin

  • 1ª vez no jogo: plano autoral de frente, em contra-plongée. Pode pular (ver Parte 2).
  • Depois: a câmera de jogo não se mexe, sem girar e sem FOV. Só flash e partículas, para não quebrar o sentido do controle no meio da onda (Haigh-Hutchinson; Nesky #46).

I. Opções (XAG 117): seguir automático ligado ou desligado, com velocidade; tremida de 0 a 100%; FOV; câmera perto ou longe.


PARTE 2: TRANSFORMAÇÃO (o hominho vira Kavo)

Decisão do dono, tomada como dada: o protagonista é um hominho normal e só ganha os poderes depois do henshin.

2.1 Gramática do henshin no tokusatsu

  • Kamen Rider (1971):
    • Takeshi Hongo é "a biochemistry lab student" que corre de moto, sequestrado pela Shocker e transformado num "grasshopper-themed cyborg". O 2º Rider, Hayato Ichimonji, é "freelance photographer".
    • A série puxou o "Henshin Boom" dos anos 70. A sequência é: o herói "performs ritualistic poses and shouting a keyword to transform".
    • Wikipedia: https://en.wikipedia.org/wiki/Kamen_Rider_(1971_TV_series)
  • Cinturão:
    • O cinturão de transformação, o "Driver", vem com pose e o grito "Henshin!".
    • Nos Riders da era Shōwa, a pose serve "to stimulate the mechanical workings inside the belts".
    • Os Riders têm forma de inseto com capacete fechado, a mesma família do Kavo: olhos compostos, antenas.
    • Wikipedia: https://en.wikipedia.org/wiki/Kamen_Riders
  • Ultraman:
    • Hayata aperta o botão da Beta Capsule.
    • O Color Timer fica "linked directly to his heart".
    • "His time limit was stated to be three minutes."
    • Com pouca energia, passa de azul-esverdeado a vermelho, pisca e toca um alarme. Sem energia, "goes out and turns black".
    • A identidade "remains a secret to the rest of the SSSP members even until the series finale".
    • Wikipedia: https://en.wikipedia.org/wiki/Ultraman_(character)
  • Super Sentai: "Typically a Sentai team would all activate their Transformation Trinkets to do a group transformation, before striking out in a cool pose." Kamen Rider é "the Trope Namer and the Trope Codifier", e "Most Kamen Riders have 'Henshin!' as the changing command". TV Tropes, Henshin Hero: https://tvtropes.org/pmwiki/pmwiki.php/Main/HenshinHero
  • Definição do trope: o Henshin Hero tem poderes "only some of the time … the character's powers are all turned off while they are in their Secret Identity". Normalmente usa um objeto ativado por "command phrase". (mesma página)
  • Gavan (Metal Heroes): grita "Jōchaku" e a armadura se forma "within 0.05 seconds". Na vida civil vive "incognito" como Retsu Ichijouji, num clube de jovens. Wikipedia: https://en.wikipedia.org/wiki/Space_Sheriff_Gavan
  • Shazam:
    • Billy é um menino normal e diz "SHAZAM!". Em 1940, um raio o atinge na troca.
    • Dizer a palavra de novo o faz voltar.
    • Na versão New 52, o raio do peito "appears as an opening into his body with magical energy visibly inside of him".
    • Wikipedia: https://en.wikipedia.org/wiki/Shazam_(DC_Comics)

2.2 Os inimigos esperam? E nos jogos?

O trope chama-se "Transformation Is a Free Action": o personagem "will obligingly wait for another character to finish his transformation". Na maioria das vezes, "the transformation really is a Free Action in that it effectively takes near zero time in-universe", e a sequência longa é exagero para encher tempo. Quando um herói contraria o trope, o normal é "making the initial triggering of the transformation the part that can be interrupted instead of the sequence proper".

Exemplos em jogos, todos da mesma página (https://tvtropes.org/pmwiki/pmwiki.php/Main/TransformationIsAFreeAction):

  • Devil May Cry: o Devil Trigger é quase instantâneo e "acts as a Combo Breaker that grants you invincibility frames". O Sin Devil Trigger do DMC5 leva ~6 s, com o tempo lento em volta, energia que lança os inimigos ao ar e o Dante imune a flinch.
  • Kingdom Hearts II: as Drive Forms são instantâneas e jogam os inimigos longe. No Birth by Sleep, ~1 s invulnerável, mas vulnerável logo depois.
  • Majora's Mask: transformar congela o jogo. "After the first transformation, you can skip the transformation sequence."
  • Final Fantasy XIII: a sequência do Paradigm Shift não para a luta, e "This only happens the first time every battle … Every other time after, the Paradigm Shift is much quicker."
  • Sonic: nos 2D, só 1–2 s suspenso. Nos 3D, ~10 s de cutscene. "This transformation goes quicker when transforming during normal gameplay in Sonic Colors and Sonic Generations."
  • Shantae: no original (2002), apanhar no meio da dança obriga a recomeçar. No Risky's Revenge, tudo pausa durante a dança.
  • Skyrim (contraexemplo): o lobisomem leva vários segundos e você fica vulnerável, então "it is highly advised that you activate it before initiating combat".
  • Dragon Ball: no Legacy of Goku II, apanhar interrompe a transformação, "a pain in the ass to pull off during boss fights". No Budokai, ela é instantânea e atordoa quem está perto.
  • Neptunia: a coluna de luz da transformação "can be seen for miles", e ela entrega o disfarce do grupo.

Sobre a repetição na TV: "If a particular transformation sequence occurs multiple times in one episode, a shortened version will often be used after the first time." E "Rarely is there a special-effects sequence for changing back." TV Tropes, Transformation Sequence: https://tvtropes.org/pmwiki/pmwiki.php/Main/TransformationSequence

Repetição em jogo indie: o dev de FLORA admite que a transição "does get a liiiiiittle tedious after the first few times seeing it" e vai encurtar ou deixar pular. https://itch.io/post/10556905

2.3 Jogos com forma normal e forma poderosa

Jogo Gatilho Quanto dura / o que acaba O que só a forma poderosa faz Fonte
Viewtiful Joe Medidor VFX cheio. "Henshin-a-go-go, baby!" Os poderes gastam o medidor. Vazio, volta ao "normal Joe", com menos ataque e defesa, até recarregar Slow, Mach Speed, Zoom In https://en.wikipedia.org/wiki/Viewtiful_Joe_(video_game)
(idem) Recarga: sozinho com o tempo, ou garrafas de "VFX Juice" (idem)
Devil May Cry 4 Apertar com o medidor cheio Temporário Golpes novos, mais dano, "slow but steady health regeneration" https://en.wikipedia.org/wiki/Devil_May_Cry_4
Bayonetta 2 Medidor de magia cheio (Umbran Climax) Curto Golpes mais fortes e cura durante o uso https://en.wikipedia.org/wiki/Bayonetta_2
Asura's Wrath Burst começa vazio em cada encontro e enche batendo, com dano e QTE Burst é um golpe final. Unlimited é dano extra temporário Finalizador que leva a história adiante https://en.wikipedia.org/wiki/Asura%27s_Wrath
Sonic 2 7 Esmeraldas + 50 anéis "loses one ring per second and reverts to normal when his rings are depleted" Quase invencível, mais rápido, pula mais https://en.wikipedia.org/wiki/Sonic_the_Hedgehog_2
Yakuza 0 (Kiryu, homem normal) Heat enche batendo Esvazia ao apanhar, ser agarrado, cair ou ficar sem atacar Heat Actions https://www.ludo.guide/guide/yakuza-0/basic-combat
(idem) Staminan Royale recupera vida e Heat. Vende em drogaria https://gamersdecide.com/articles/yakuza-0-best-healing-items
inFAMOUS Sempre com poder, mas precisa de eletricidade Recarrega "by draining electricity from powered sources or from living beings". Não encosta em água Raios. Também escala prédios e cai de alto sem dano https://en.wikipedia.org/wiki/Infamous_(video_game)
Prototype Disfarce da última pessoa consumida "The disguise will only last as long as Alex remains inconspicuous" Detector viral da Blackwatch vê através do disfarce https://en.wikipedia.org/wiki/Prototype_(video_game)
Spider-Man (2018) Trechos fixos como Peter, MJ ou Miles Peter faz puzzles. MJ e Miles fazem stealth Críticas mistas: "hit-and-miss … a bit grating at times" (VideoGamer, sobre esses trechos). A EGM diz que o stealth em geral é "rarely mandatory". Há opção de pular os puzzles https://en.wikipedia.org/wiki/Spider-Man_(2018_video_game)
The Wonderful 101 Desenhar a forma (Unite Morph) Gasta bateria, que recarrega batendo ou com baterias dos inimigos Mais heróis recrutados = morph mais forte. Civis viram heróis quando recrutados. Wonder-Red é o professor Will Wedgewood https://en.wikipedia.org/wiki/The_Wonderful_101
Persona 5 Vida escolar de dia e Metaverso à noite Laços sociais do mundo real aumentam a força das personas nas masmorras (descrição do autor da matéria) https://vice.com/en/article/precious-moments-hype-and-high-school-a-conversation-with-persona-5-director-katsura-hashino
Kamen Rider: Memory of Heroez Troca de forma "on the fly" em combate Cada troca custa "a big chunk of your AP meter, limiting your ability to pull off some insane combos". O EX gauge libera o finalizador Formas com golpes próprios. A resenha acha repetitivo "get surrounded by a barrier, and defeat enemies" https://onemoregame.ph/?p=77208

2.4 As perguntas de design, com evidência

O que o civil pode fazer? Andar, conversar, comprar e escalar. O Cole do inFAMOUS escala tudo. Os trechos de Peter e MJ são puzzle e stealth. Persona põe o dia a dia como metade do jogo. A evidência do Spider-Man pede trechos civis curtos e quase nunca obrigatórios.

O que só o herói faz? Nos jogos acima, a forma poderosa ganha dano, golpes novos, cura ou quase invencibilidade (DMC, Bayonetta, Sonic) e poderes únicos (Viewtiful Joe). A forma normal fica mais fraca (Joe) ou exposta (Sonic com 0 anéis).

O que dispara?

  • Medidor cheio e um botão: DMC, Bayonetta, Viewtiful Joe, Yakuza.
  • Recurso coletado: os anéis do Sonic.
  • Palavra ou objeto: Kamen Rider, Ultraman, Shazam.
  • História ou lugar: os trechos do Spider-Man.

Quanto dura e o que acaba?

  • Relógio fixo: Ultraman, 3 min.
  • Dreno contínuo: Sonic, 1 anel/s.
  • Dreno por uso: Viewtiful Joe, bateria do Wonderful 101.
  • Dreno por apanhar ou ficar parado: Heat do Yakuza.
  • Recarga sozinha (Joe), por bebida (VFX Juice, Staminan) ou drenando a cidade (inFAMOUS).

Quanto pode durar a animação antes de irritar?

  • Na ficção é quase instantânea (Gavan, 0,05 s; trope "near zero time in-universe").
  • Em jogo: instantânea com invencibilidade (DMC, KH2); longa só na 1ª vez (Majora, FFXIII, TV); ~6 s só quando é rara e paga com força grande (Sin DT); ~10 s em cutscene, mas mais curta no gameplay (Sonic).
  • Animação interrompível irrita (Legacy of Goku II, Shantae 2002).

Identidade secreta:

  • Ultraman esconde do próprio time até o fim. Gavan vive incógnito.
  • Will Wedgewood, o Wonder-Red, é professor primário.
  • O disfarce do Prototype dura só enquanto ninguém nota.
  • A luz da Neptunia entrega o grupo.
  • No inFAMOUS, os civis ajudam se o carma é bom e jogam pedra se é ruim.

O que isso significa para o Kavo

Quem é quem

  • Hominho (civil):
    • anda, corre, conversa, compra no 7-Eleven e aceita missões;
    • faz parkour humano estilo AC: escalar, vaultar, agarrar beirada;
    • não luta de verdade, como o "normal Joe": no máximo esquiva e empurrão;
    • pulo simples e nenhum golpe de energia.
  • Kavo (depois do henshin):
    • os combos J/K/L;
    • os movimentos sobre-humanos do SM64: pulo duplo, mergulho, wall-kick;
    • energia (K);
    • levantar máquina de venda;
    • corre mais e cai de alto sem dano, como o Cole.

Gatilho (proposta): no civil, segurar K por ~0,4 s, já que o civil não tem golpe de energia. Só funciona com o raio carregado acima de ~30%. É o "medidor cheio + botão" do DMC e do Joe, mais a pose que "aciona o cinturão" dos Riders Shōwa. Não usar combinação de 3 teclas, por causa do ghosting (pesquisa de benchmarks).

Animação (proposta, apoiada em 2.2):

  • 1ª vez no jogo: sequência completa de ~3–4 s, com pose, grito "Henshin!" e o raio caindo no peito (o raio do Shazam). Pode pular.
  • Da 2ª em diante: ~0,5 s em gameplay, com invencibilidade e onda de choque que empurra os inimigos perto (DMC, KH2, Budokai). Câmera parada (Parte 1, H).
  • Opção "henshin completo: sempre / só a primeira / nunca".
  • Voltar a ser civil não tem animação longa (TV Tropes: "Rarely is there a … sequence for changing back").

Raio amarelo do peito = Color Timer (proposta, baseado no Ultraman):

  • Amarelo fixo com energia sobrando. Laranja pulsando a ~50%.
  • Vermelho piscando com alarme abaixo de ~25%. Apagado quando volta a ser civil.
  • Em pixel-art baixa, o raio tem que ser grande, e vale um espelho no HUD.

Duração e fim (proposta):

  • O raio é tempo de herói e drena sozinho, como o Sonic. Proposta: ~90 s cheio.
  • Gasta mais com golpes de K.
  • Enche batendo (DMC, Asura, Yakuza).
  • Vazio, volta ao civil na hora, mais fraco e vulnerável (Joe, Sonic). Uma onda não pode ser vencida sem reabastecer.

O 7-Eleven como posto de recarga:

  • bebida energética e café enchem o raio, como o VFX Juice do Joe e o Staminan do Yakuza;
  • como Kavo, encostar numa máquina de venda e drenar a eletricidade dela, como o Cole drena "powered sources";
  • missões da atendente aumentam o raio máximo, como os laços do Persona fortalecem as personas.

Identidade:

  • A Sra. Hoshino trata o hominho como freguês e fala do "Kavo" como de um estranho, como o time do Hayata nunca descobre.
  • Transformar na frente dos outros tem consequência: a luz denuncia, como na Neptunia, e o disfarce cai, como no Prototype. Transformar escondido no beco não tem.

Coisas que o dono talvez não pensou

  1. O henshin pode ser a saída de emergência. No DMC, o Devil Trigger é Combo Breaker com invencibilidade. No KH2, joga os inimigos longe. Transformar no meio do aperto vira jogada, não só troca de visual. (TV Tropes, Transformation Is a Free Action)
  2. Decidir onde fica a fraqueza: no gatilho ou na sequência. O trope diz que o normal é o gatilho ser interrompível, não a sequência. Legacy of Goku II e Shantae (2002) mostram que sequência interrompível irrita. (mesma página)
  3. Longa só na primeira vez. Majora deixa pular depois da 1ª. No FFXIII a longa é só na 1ª de cada luta. Na TV, a cena repetida é encurtada. O Gavan "leva" 0,05 s. (TV Tropes; Wikipedia Gavan)
  4. O trecho civil não pode ser castigo. A VideoGamer chamou as partes sem poder do Spider-Man de "hit-and-miss … a bit grating", e o jogo deixa pular os puzzles. Os trechos civis precisam ser curtos e divertidos sozinhos. (Wikipedia, Spider-Man 2018)
  5. A cidade é a bateria. O Cole drena eletricidade de qualquer fonte. O Joe bebe VFX Juice. Kiryu toma Staminan. Máquinas de venda e o 7-Eleven viram posto de energia e dão um motivo para voltar à loja entre as ondas. (Wikipedia inFAMOUS e Viewtiful Joe; Gamers Decide)
  6. Ser visto se transformando muda o mundo. Prototype tira o disfarce quando você chama atenção. No inFAMOUS, os civis ajudam ou jogam pedra conforme o carma. A luz da Neptunia entrega o grupo. Os NPCs de Koenji podem reagir diferente ao hominho e ao Kavo. (Wikipedia Prototype e inFAMOUS; TV Tropes)
  7. Civis podem virar força. No Wonderful 101, civis recrutados reforçam o golpe. Para o Kavo, salvar um civil ou ajudar a atendente pode encher o raio. (Wikipedia, The Wonderful 101)
  8. A plateia faz a arena. No Yakuza, os curiosos formam o ringue da briga. Isso resolve o limite da arena na rua estreita e ajuda a câmera. Já a barreira mágica repetida de Memory of Heroez foi chamada de repetitiva. (Ludo.guide; One More Game)
  9. Não cobrar medidor por tudo. Em Memory of Heroez, cada troca de forma come um "big chunk" do AP, e a resenha diz que isso limita os combos. Transformado, o básico do Kavo deveria ser grátis, e o raio cobra o tempo e os golpes de K. (One More Game)
  10. Destransformar por vontade, como o Shazam, que diz a palavra de novo para voltar. Assim dá para guardar energia, falar com a atendente sem assustar e esconder a identidade. (Wikipedia, Shazam)
  11. Plano de câmera do henshin = só na 1ª vez. Depois, a câmera de jogo não pode girar, senão quebra o sentido do controle no meio da onda. (Haigh-Hutchinson, "retain control reference frames"; Nesky #18)

Lacunas (não achei ou não consegui abrir)

  • Ian Pilipski / Insomniac: procurei pelo nome e por palestras de câmera da Insomniac e não achei nenhuma palestra de câmera com esse nome. As palestras da Insomniac na GDC que apareceram são de animação, renderização e travessia.
  • Astro Bot, A Hat in Time, Odyssey: não achei fonte de design de câmera, só resenhas genéricas e tópicos de fórum que não abri. Do Odyssey, só a compilação da MarioWiki.
  • Sleeping Dogs: não pesquisei a fundo e não tenho fonte.
  • Yakuza e Like a Dragon, reclamação de câmera em lugar apertado: o tópico da Steam (Ishin) não carregou o conteúdo, então não uso.
  • Cores das setas do GoW pelo guia Prima: o site bloqueou (403). Usei o resumo da palestra, que diz vermelha e branca.
  • TV Tropes: bloqueia acesso direto. Li a cópia de 2024 no Wayback Machine.
  • Sonic Retro e fandom (Yakuza, Prototype): bloqueados. Usei a Wikipedia e outras páginas.
  • Kamen Rider: não confirmei a história de que a pose nasceu do acidente do Fujioka. A Wikipedia só diz que ele se machucou no começo das filmagens. Também não achei se os jogos de Kamen Rider deixam pular o henshin.
  • Jogos do Ultraman com Color Timer como mecânica: não confirmei.
  • Câmera de diálogo do Witcher 3: as páginas estavam bloqueadas.
  • Haigh-Hutchinson: li o capítulo 4 numa cópia online do livro e não linko essa cópia. O link oficial é o excerto da Game Developer.