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

Um Raio Que Ilumina o Tabuleiro Inteiro

A Nightfall é a fase que tira a sua visão: o tabuleiro fica quase todo no escuro e uma torre só atira no que consegue enxergar. Essa semana eu remexi em como esse escuro se comporta. Os raios agora iluminam o campo de batalha inteiro por um terço de segundo antes de voltar pra noite, as torres pararam de iluminar um raio que crescia junto com o nível delas, o cursor de posicionamento carrega a própria lanterna e os props decorativos aprenderam a sair da frente. Tudo coisa pequena, mas a Nightfall saiu de "mapa chato" pra fase que eu fico rejogando.

Trovão em três fases

O flash antigo do raio era uma batida só: segura branco puro por 50ms, some com o overlay branco em 300ms, volta pro escuro. Lia como flash de câmera, não como clima, e pior: não te dizia nada. Você ficava cego e depois o tabuleiro estava exatamente tão escuro quanto antes.

Agora o flash roda uma linha do tempo de três fases:

  1. Hold, 0.05s de branco puro cobrindo tudo.
  2. Reveal, 0.35s em que o overlay branco vai a zero enquanto o ambient da cena é empurrado pra luz do dia, então por um instante você vê o tabuleiro inteiro de verdade: cada inimigo, cada prop, cada buraco na sua cobertura.
  3. Dim, 0.9s trazendo esse ambient de volta pra cor de noite da fase, então o escuro volta aos poucos em vez de fechar de uma vez.

O truque é que as fases dois e três não são overlay nenhum, elas escrevem direto no lightSystem.ambientColor, misturando o ambient noturno declarado no JSON da fase com um LIT_AMBIENT de mais ou menos (0.85, 0.85, 0.95). O light system multiplica a cena por ambient mais luzes, então subir o ambient é literalmente "liga o sol por um segundo".

Os raios voltam num intervalo aleatório de 45 a 90 segundos e agora tem três samples de trovão (thunder1.ogg até thunder3.ogg, creditados no CREDITS.md). Tudo isso é cosmético: roda em tempo de relógio e no rand do próprio LittleJS, nunca no RNG com seed do gameplay, então o co-op continua determinístico independente de quem viu qual tempestade. Mesma regra do trabalho de determinismo. O flash também avança a própria linha do tempo dentro do passo de desenho e não no de update, então abrir um menu no meio do raio não trava a tela no branco.

As torres enxergam uma distância fixa agora

Toda torre na Nightfall emite uma luz, e essa luz usava o alcance de tiro da torre como raio. Parecia elegante quando escrevi ("você vê o que consegue atirar") e desandou na primeira vez que apareceu um morteiro. O alcance de um morteiro é grande, o de um morteiro upado é gigante, então um sozinho inundava o mapa inteiro de luz e apagava a premissa da fase.

A visão agora é uma constante, TOWER_VISION_RADIUS, definida como uma fração do raio da lanterna do cursor. Começou na metade e o playtest subiu pra três quartos (2.25 unidades de mundo contra as 3.0 da lanterna). Visão e alcance viraram duas coisas diferentes, o que significa que dá pra ter uma torre que atira bem além do que enxerga e precisa de uma vizinha pra abrir caminho. Isso é um problema de posicionamento bem mais interessante que "põe o morteiro em qualquer lugar".

As torres lanterna ganharam um buff na mesma passada, já que elas são a resposta pro problema que o morteiro estava resolvendo de graça sem querer.

O cursor carrega uma lanterna

Enquanto você posiciona uma torre, agora tem uma luz seguindo o cursor. Antes você arrastava um fantasma de torre por cima de um retângulo preto e rezava. Agora a lanterna de posicionamento mostra o chão em que você está prestes a se comprometer, o que pesa muito num mapa onde o terreno embaixo dos seus pés decide se a torre presta.

A decoração parou de nascer embaixo das torres

A Nightfall divide o sistema de espalhamento com todas as outras fases: depois que o terreno se encaixou num grid, cada célula de chão rola uma chance de receber um prop decorativo, com jitter, flip e escala saídos do mesmo fluxo com seed. Dois problemas apareceram no playtest: os props se amontoavam em pilhas e uma torre colocada em cima de um deixava a arte espiando por baixo da base.

Os dois viraram problemas de geometria com soluções puras em lib/scenery.ts:

  • propRect dá a um prop um footprint alinhado aos eixos, a partir do tamanho nativo da arte vezes a escala sorteada.
  • edgeDistance devolve a menor distância borda a borda entre dois desses rects (zero quando se sobrepõem), e um prop candidato é rejeitado se não estiver a pelo menos MIN_PROP_EDGE_GAP (0.5 unidades de mundo, 32px) de todo prop já aceito.
  • pointToRect dá a distância de um ponto até a borda mais próxima de um rect, que é como uma torre (modelada como um disco com o raio do footprint dela) descobre em tempo de jogo que está pisando num prop e o remove.

A passada de espalhamento agora recebe os tamanhos de arte por variante em vez de só a contagem de variantes, porque não dá pra checar espaçamento sem saber o tamanho das coisas. Tudo testado, tudo determinístico, sem nenhum objeto de engine no meio.

Botão direito volta uma marcha

Bobagem, mas me incomodava fazia semanas: o botão de velocidade só ciclava pra frente, então passar do 3x significava dar a volta inteira. Clicar com o botão direito agora volta um nível. Como velocidade de jogo é uma ação replicada, isso entrou como um intent de verdade (o applyIntent trata o passo pra trás igual ao passo pra frente) em vez de gambiarra local de UI, então se comporta igualzinho pro host de co-op.

Na próxima: inimigos que se desfazem direito quando morrem.