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

O Dano Tem Tipo Agora, e Alguns Inimigos se Importam

Na semana passada o jogo aprendeu a concordar com o mesmo frame entre máquinas. Essa semana ele aprendeu que nem todo dano é igual. Até agora um golpe era só um número: subtrai do HP, pronto. Agora todo golpe carrega um tipo de dano, os inimigos têm opiniões sobre esses tipos, e um pedacinho de sintaxe no spawn deixa eu conjurar um inimigo imune ao que eu quiser sem escrever uma classe nova ou um sprite novo.

Nove sabores de dor

Tem um damageTypes.json novo listando o vocabulário: balístico, elétrico, fogo, impacto, explosão, energia, veneno, frost, confusão. É dado, embrulhado num helper tipado, e toda torre é marcada com o tipo que ela causa. A Gun é balística, o Cannon é explosão, a Tesla é elétrica, o Flame é fogo, a torre Frost é frost, o dardo de veneno é veneno, e assim por diante.

O detalhe importante é que a marca viaja até o fim pelo pipeline de dano compartilhado. Todo golpe é carimbado: projéteis, feixes, os raios em estrela da Tesla, o cone do Flame, a execução da guilhotina, as armadilhas de espinho e até os ticks de dano ao longo do tempo do fogo e do veneno. Existe exatamente um caminho dealDamage no jogo, então carimbar uma vez faz toda fonte ser tipada automaticamente, sem tratamento especial por torre.

Inimigos com opinião

Do outro lado, os inimigos ganharam um mapa damageResist. Cada entrada é um multiplicador: 0 significa imune, menos que 1 significa resistente, 1 é normal (o padrão quando ausente), e mais que 1 significa frágil. Quando um golpe resolve, o tipo dele é consultado no mapa de resistência do alvo e o dano é escalado por esse multiplicador antes de encostar no HP.

Duas coisas caem disso de forma limpa. A imunidade não zera só o número de dano, ela também cancela o debuff de status anexado àquele golpe: um inimigo imune a frost não toma dano de frost e ignora a lentidão; imune a elétrico significa nada de stun; imune a veneno ou fogo significa que o DoT nunca pega. Esse controle mora num lugar só, o effectBlocked, que todo caminho de aplicação já consultava, então o mapa de resistência encaixou direto. E em vez de um número de dano enganoso "0", um golpe imune faz flutuar um rótulo MISS, movido por um campo missCount replicado pra ler honesto no co-op também.

Isso também me deixou apagar um caso especial improvisado. O elemental de frost carregava a vulnerabilidade a fogo como uma entrada específica no mapa de status; agora é só fire: 1.5 no mapa de damageResist, e como o mapa é consultado tanto pros golpes diretos quanto pros ticks de DoT, o burn dele escala 1,5 do mesmo jeito com zero código extra. Um sistema, uma regra, menos ramificações.

Já que eu estava no código de movimento, também liguei direito as regras de movimento dos efeitos de status, incluindo uma picante: inimigos em chamas agora aceleram. Uma criatura pegando fogo entra em pânico e corre pra saída, que é um riscozinho divertido pra torre Flame. Deixa lento ou pega fogo, mas não os dois sem cuidado.

O spawner com notação de ponto

Aqui está a parte de que eu me orgulho. As definições de wave spawnam inimigos por uma string de id. Eu estendi essa string pra uma notação de ponto: base.imunidade1.imunidade2.... Então normie.fire é um normie comum que por acaso é imune a fogo; speedy.frost.electric é um veloz que ri tanto da sua torre Frost quanto da sua Tesla. Qualquer inimigo base, qualquer combinação de imunidades, escrito inteiramente no JSON de waves, sem precisar de tipo novo de inimigo.

Por baixo dos panos o spawner faz o parse da spec (um parseEnemyVariant puro e feito com TDD) e assa as imunidades num bitmask sobre os índices dos tipos de dano, guardado no inimigo e replicado nos snapshots de co-op. dealDamage e effectBlocked consultam esse mask junto com o mapa de resistência estático, então uma variante imune toma zero daquele tipo e o status dela é cancelado, exatamente como uma imunidade nativa.

Os visuais são totalmente movidos por código, que é o truque inteiro pra manter isso barato. Cada tipo de dano carrega uma cor [r,g,b] no JSON. Um inimigo imune desenha um halo colorido empilhado, uma silhueta aumentada por imunidade, e um tint misturado (fogo mais frost puxa pro roxo). Adicionar um sabor novinho de imunidade depois precisa de uma entrada de cor e nada mais: sem trabalho de sprite, sem arte nova, sem código. Esse é o retorno do design orientado a dados aparecendo bem na hora que você quer.

Por que se dar o trabalho

O ponto de tudo isso é tornar real a pressão de "não existe uma torre melhor que todas". Antes, você achava a torre favorita e ia no embalo. Agora eu posso soltar uma wave imune a fogo pra punir um build puro de Flame, ou uma corrida imune a frost que ignora seu campo de lentidão, e você tem que diversificar de verdade. O combate ganhou uma dimensão, o elenco de inimigos ganhou um botão que eu posso girar sem adicionar entidades, e a coisa toda anda no único pipeline de dano que eu já tinha. De volta pro quadro.