Doug's Game Dev Log
Sobre
EN|PT-BR
2026-08-07

Tudo se Encaixou num Grid e Começou a Brilhar

Os últimos posts foram sobre apagar coisas, waves e tabelas de stats de torre. Esse aqui é o contrário: o td-survivors passou dois dias ganhando uma cara. O fundo deixou de ser um campo verde chapado, o mundo inteiro se encaixou num grid rígido, as torres se encaixam no lugar tipo Lego, os botões inclinam e brilham, e um shader de pós-processamento cobre tudo com uma camada de doce. Nada do gameplay mudou. Tudo em como ele parece mudou.

A grama aprendeu a curvar em volta do caminho

O fundo antigo era um tile de grama repetido com o caminho desenhado por cima. Parecia papel quadriculado. O novo é um auto-tiler de marching-squares, e é a parte que mais me deixa feliz.

O truque é parar de pensar em células e começar a pensar em cantos. Toda célula do grid tem quatro cantos; cada canto é "caminho" ou "grama" dependendo de estar dentro de pathHalfWidth da linha do caminho. Quatro cantos, caminho-ou-não, é um número de 4 bits de 0 a 15, e esse número indexa direto numa tabela de 16 entradas que diz qual tile desenhar e quanto rotacionar:

// bit 0 = canto inferior-esquerdo, bit 1 = inferior-direito, bit 2 = superior-direito, bit 3 = superior-esquerdo
const TABLE = [
  { category: 'ground', rotationSteps: 0 }, // 0000: nenhum canto é caminho, grama pura
  { category: 'corner', rotationSteps: 0 }, // 0001: o caminho espia num canto
  ...
  { category: 'path',   rotationSteps: 0 }, // 1111: os quatro cantos são caminho
]

Cinco categorias de tile cobrem todos os casos: grama cheia, caminho cheio, linear (uma borda reta de grama pra caminho), corner (o caminho contornando um canto externo) e inner (a grama mordendo um canto interno). Rotaciona esses cinco em 0/90/180/270 graus e você consegue renderizar qualquer formato de caminho com transições macias, com cara de desenhado à mão, em vez de uma linha dura e serrilhada. Os dois casos de "sela" (cantos opostos são caminho, os outros dois grama) são ambíguos, então caem de volta pra caminho sólido.

A classificação é pura e tem teste antes (lib/autotile.ts), e a camada de render só resolve categorias pros PNGs de verdade, escolhendo uma variante de arte aleatória por célula com um RNG semeado pra que um campo de grama não fique com cara de tile repetido. Tinha um bug de canto genuinamente cruel escondido na geometria: numa dobra do caminho, o canto diagonal externo do grid fica a exatamente sqrt(0.5) = 0.707 da linha central, então se pathHalfWidth for 0.7 aquele canto lê como grama e todo canto convexo vira um tile inner mordido e feio. Subir a meia-largura pra 0.75, só um tiquinho acima da diagonal, resolve sem alargar o caminho reto nem um pouco. O tipo de bug que você só acha encarando um pixel errado por dez minutos.

O mundo inteiro se encaixou em 1080p

Enquanto reconstruía o fundo eu também travei o grid de vez. O canvas agora renderiza num fixo 1920x1080, toda fase tem exatamente 30x16 unidades de mundo, e a escala da câmera é 1920 / 30 = 64 pixels por unidade. Isso quer dizer que todo tile de fundo desenha em exatamente 64x64 pixels, nunca reamostrado, nunca borrado. O caminho é feito em coordenadas inteiras que correm pelos centros das células, então ele é sempre limpo e de um bloco de largura.

O detalhe divertido: eu não queria fazer UI em números de 1080p. Então toda a UI ainda é escrita num confortável espaço de design de 1280x720, e um único módulo de fronteira (uiScreen.ts) escala as coordenadas de design pra cima no caminho pra tela e escala o input do mouse pra baixo na volta. Todo desenho e hit-test do jogo fala 720p; só o blit final sabe de 1080p. O placement ganhou a mesma rigidez: as torres agora dão snap num grid de 8px e ocupam footprints quadrados de 64x64, então duas torres ou cabem lado a lado ou não cabem, sem matemática nebulosa de sobreposição.

Aí os botões ganharam uma terceira dimensão e tudo começou a brilhar

Apresentação não é nada sem suco, então a UI ganhou duas demãos dele.

Primeiro, um renderer de botão de verdade. Todo botão agora é desenhado como um único quad inclinado numa passada raw de canvas 2D, empilhando uma sombra projetada, um corpo com gradiente, uma luz de borda, um brilho especular e um glow de destaque, todos compartilhando a mesma perspectiva pra ler como uma face física, levemente 3D, em vez de um retângulo chapado. A geometria pura (o quad inclinado, onde o brilho fica, a reação ao hover) mora num arquivo de lib testado; o encanamento do canvas é a fronteira sem teste. Os botões inclinam, pegam luz e dão bloom no hover.

Segundo, um único shader de pós-processamento sobre o frame inteiro composto, uma correção propositalmente exagerada estilo "fliperama açucarado": bloom por threshold pra que orbs, explosões e os botões brilhantes vazem luz, uma correção de cor com contraste-e-saturação quente pra amarrar a paleta, um vinheta suave pra puxar o olho pro centro, um sopro de aberração cromática nos cantos e um pouquinho de grão de filme pra matar o banding dos gradientes. Todo botão de ajuste é uma constante simples no topo do arquivo com um comentário, então calibrar o visual inteiro do jogo é editar um punhado de números e deixar o Vite dar hot-reload.

Dois dias, zero gameplay novo, e o jogo finalmente parece uma coisa em que você ia querer clicar. Próximo passo: as cartas de upgrade ainda precisam do tratamento de scaling dois-números-e-uma-reta que eu prometi, então de volta a apagar coisas.