Notas de desenvolvimento

Uma visual novel é um conjunto de imagens. Por que o Builder ficou tão complexo?

Como um pequeno protótipo chamado Fibber cresceu e se tornou uma plataforma distribuída com colaboração, publicação versionada e um runtime compartilhado para histórias.

Vista de fora, uma visual novel parece quase trivial: um fundo, um personagem com transparência, uma caixa de texto, algumas escolhas que levam à próxima cena e um pouco de música. Era exatamente assim que eu a via quando comecei a criar a primeira versão.

Ainda acho que o núcleo é simples. Hoje, principalmente com a ajuda da IA, quase qualquer desenvolvedor consegue criar um player de visual novels. A dificuldade começa quando o objetivo deixa de ser fazer uma história e passa a ser construir uma ferramenta na qual outra pessoa, talvez até uma criança, possa criar, testar, publicar e continuar desenvolvendo sua própria história.

A primeira versão tinha uma única tarefa

Antes de Romergo se chamar Romergo, ele era Fibber. Montava uma missão a partir de cenas conectadas, permitia que eu preenchesse essas cenas com textos e imagens e aceitava escolhas ramificadas. A stack era TypeScript, React, Strapi e Ant Design.

Aquela versão cumpria sua função. O grafo tornava a história visível, as cenas podiam ser editadas e o resultado podia ser testado em uma prévia. Para um protótipo, isso bastava. Também bastou para revelar o problema real.

O primeiro gargalo real não era o código

Quando começamos a preencher uma missão real com textos e imagens, montar uma cena de dez minutos podia levar quase um dia inteiro. Criar uma cena. Enviar um fundo. Enviar um personagem. Posicioná-los. Adicionar diálogos. Conectar a próxima cena. Repetir.

Parte disso era um problema de interface. Melhorei a UX, acelerei os uploads e tornei a publicação mais confiável. Mas o maior custo continuava intacto: uma pessoa ainda precisava executar manualmente cada pequena ação.

E se duas pessoas pudessem criar ao mesmo tempo?

A primeira resposta era simples: se uma pessoa é o gargalo, vamos permitir que várias trabalhem na mesma história. Comecei com Liveblocks, aprendi com esse modelo e depois avancei para minha própria camada de colaboração, com Yjs no núcleo.

O experimento abaixo parece mostrar duas janelas do editor se movendo juntas. Por baixo existe uma mudança muito maior na maneira como o produto pensa: as edições se tornam atualizações de um documento compartilhado, em vez de envios isolados de formulários. Daí surgem presença, reconexões, tratamento de conflitos, permissões e a expectativa de que um projeto esteja sempre vivo.

Todo atalho útil acabou se tornando um limite

Strapi foi uma ótima forma de obter um backend rapidamente, mas cada nova função específica de histórias exigia que o CMS se comportasse um pouco menos como um CMS. Ant Design deu velocidade e consistência à primeira interface, mas aos poucos fez o produto parecer limitado pelo sistema visual de outra pessoa.

Uma iteração serverless posterior, baseada em Remix, foi um passo importante. Mesmo assim, a forma como eu havia combinado aplicação, mídia e deploy não oferecia a liberdade horizontal que o produto começava a exigir. Nenhuma dessas escolhas foi um erro: cada uma comprou tempo suficiente para eu descobrir a próxima limitação.

Imagens não são anexos. São infraestrutura.

A mídia virou uma lição própria. Uma imagem da história precisa ser enviada uma vez, transformada quando necessário, armazenada com segurança e entregue rapidamente a qualquer jogador no mundo. Mantê-la junto de um servidor de aplicação que já crescia só deixava o backend mais pesado, então a primeira solução separada foi o Cloudinary.

Certa noite, abri o painel e vi que um único usuário havia consumido cerca de 500 MB enviando a mesma imagem centenas de vezes. Minha correção de emergência comparava hashes e rejeitava duplicatas. Funcionou, mas limites de frequência e cotas teriam tratado esse comportamento de forma mais direta. A maior surpresa foi a largura de banda: uma noite de testes podia consumir mais de dez por cento da franquia gratuita.

Esse episódio mudou o modelo. A mídia não podia mais ser apenas um campo da cena. Ela precisava de um pipeline próprio para armazenamento, deduplicação, entrega, cache e acesso.

Como aquela ideia simples se parece hoje

O Romergo atual separa o Builder do player, mas ambos usam o mesmo runtime de histórias. A prévia dentro do editor e uma experiência publicada seguem as mesmas regras, o que torna muito mais difícil introduzir divergências no contrato.

Yjs sincroniza o projeto editável entre colaboradores. A publicação cria um snapshot verificado dos dados e da mídia da história, para que os autores continuem alterando o rascunho sem mudar silenciosamente a versão que os jogadores já estão executando. A API é feita com Hono, a interface usa primitivas shadcn e Radix, e a camada de nuvem oferece coordenação distribuída, armazenamento de objetos e entrega por CDN.

Esse runtime compartilhado agora leva a história ao navegador e ao PWA, ao Telegram e a uma experiência sincronizada no Discord. Compatibilidade entre versões, funcionamento offline e estado multijogador deixaram de ser efeitos colaterais em torno de uma página. Eles são sistemas próprios do produto.

Arquitetura atual

Uma história editável. Um contrato publicado.

Uma visão geral da arquitetura atual do Romergo.

1. Projeto editável

  • Builder UI: Cenas · fluxo · mídia
  • AI / MCP: Operações de edição autenticadas
  • Realtime / Yjs: Durable Objects · documento de trabalho compartilhado
  • Editor API: Gravações CAS · materialização do rascunho
  • Snapshot runtime do rascunho: PublishedQuestLocalizedContentV2 · D1 / R2
  • Mídia do rascunho: Objetos R2 · metadados D1

2. Publicação versionada

  • Verificações de publicação: Schema runtime · declaração do autor · existência da mídia
  • Publicação imutável vN: Snapshot runtime · mídia copiada e remapeada
  • Contrato de visibilidade: Público · não listado · privado

3. Um contrato de execução

  • Snapshot do rascunho: Estado editável mais recente
  • Publicação vN: Estado estável do player
  • Player-runtime compartilhado: Cenas · transições · condições · variáveis · saves
  • Prévia do Builder: Executa o rascunho no mesmo motor

4. Canais de reprodução

  • Web / PWA: Player no navegador
  • Telegram Mini App: Player incorporado
  • Discord Activity: Jogo em grupo sincronizado

5. Infraestrutura da plataforma

  • Clerk + OAuth: Identidade e acesso MCP
  • Hono Workers: Serviços API e MCP
  • Cloudflare D1: Projetos · versões · metadados
  • Cloudflare R2: Mídia · snapshots runtime grandes
  • Durable Objects: Salas Yjs · sessões do Discord
  • Progresso do jogador: Sincronização D1 · fila local offline

A visual novel ainda é apenas um conjunto de imagens

O curioso é que a ideia original sobreviveu. Uma visual novel ainda é formada por fundos, personagens, texto, escolhas e som. O Romergo ficou complexo porque tudo ao redor desse núcleo simples precisava se tornar confiável: colaboração, mídia, publicação, versões, jogo offline e várias formas de viver a mesma história.

Se essa evolução me ensinou algo, foi a medir todo o caminho humano, não apenas o código que renderiza a tela final. O player pode precisar de cinco elementos básicos. Um produto que ajuda alguém a transformar uma ideia em uma história concluída e jogável precisa de um sistema.

Voltar ao blog técnico