As Waves Entraram de Dieta
Os arquivos de wave do td-survivors tinham engordado. Não "precisa de um refactor algum dia" gordo. Doze mil linhas de gordo. Hoje eles entraram de dieta, e a dieta funcionou tão bem que eu tô levemente preocupado que isso volte pra me assombrar.
Como as waves funcionavam antes
Cada fase tinha um *.waves.json que listava cada spawn na mão. Tipo do inimigo, timestamp exato, um objeto por criatura. Só o stage1.waves.json tinha 1.270 linhas disso:
{ "type": "normie", "time": 1 },
{ "type": "normie", "time": 5 },
{ "type": "speedy", "time": 5 },
{ "type": "normie", "time": 9 },
Multiplica isso por onze fases, cada uma com dezenas de waves, e você tem uma pasta onde os inimigos são mais numerosos que o código de verdade. Quer que a wave 6 solte mais um goblin? Acha o array certo, conta os timestamps, torce pra não ter errado um time e empilhado dois inimigos no mesmo frame. Era menos "game design" e mais "digitação com passos extras".
Como as waves funcionam agora
A curva de dificuldade de uma fase inteira agora cabe num cartão postal:
{
"initialQuantity": 10,
"initialDuration": 10,
"ramp": { "xp": 1.1, "hp": 1.1, "speed": 1.02, "quantity": 1.1, "duration": 1.09 },
"bosses": { "10": "juggernaut", "25": "necrolord", "50": "wyrm" },
"waves": [
["normie"],
["speedy"],
["normie", "speedy"],
["splitter"]
]
}
É isso. Cada wave agora é só um pool de tipos de inimigo. A wave não lista mais os spawns dela; ela declara quais inimigos são permitidos, e a engine deriva o resto.
O formato de pool também fez uma coisa legal cair no colo pra fase 1: ela funciona como um tutorial. Olha a sequência, ["normie"], ["speedy"], ["normie", "speedy"], ["splitter"], ["normie", "speedy", "splitter"]. Toda vez que um inimigo novo vai entrar na festa, ele ganha uma wave sozinho primeiro, só ele, antes de ser misturado com os outros. Então o player conhece o splitter sozinho e consegue ver ele se dividir sem uma dúzia de outras coisas brigando pela atenção, aí a wave seguinte joga ele no meio do caos. Sem tooltip, sem pop-up, só "esse é o cara novo, se vira pra entender" seguido de "beleza, agora lida com ele pra valer". O onboarding inteiro está codificado no formato da lista de pools. A wave n solta initialQuantity * quantity^n inimigos, espalhados igualmente ao longo de initialDuration * duration^n segundos, andando pelo pool em round-robin, então uma wave ["normie", "speedy"] alterna os dois sozinha. HP, XP e velocidade sobem cada um na sua própria ramp composta. Os bosses viraram um único mapa compartilhado numeroDaWave -> tipo em vez de um campo boss copiado e colado em cada wave. E como os pools simplesmente dão a volta quando chegam no fim da lista, o modo endless sai de graça: sem caso especial, as ramps só continuam compondo (com um teto, pra um pulo de debug pra wave 9999 não dar HP infinito pros inimigos e transformar o renderer em NaN, lição que eu aprendi do jeito divertido).
Os números, porque apagar código é a melhor sensação em software
O refactor foi 615 linhas adicionadas, 12.457 apagadas. Saldo: quase doze mil linhas de spawn de inimigo feito na mão, sumiram, trocadas por cinco números e umas ramps. A matemática pura das waves (waveSpawns, waveQuantity, waveDuration, waveMultipliers) é minúscula e tem teste antes, que é o jeito certo de tratar a única parte do código que decide se a wave 40 é vencível.
Calibrar uma fase saiu de "abre um arquivo de 1.300 linhas e reza" pra "empurra o ramp.hp de 1.1 pra 1.12 e se sinta um mago da dificuldade". Eu consigo rebalancear a curva de poder de uma fase inteira editando um único float. Isso ou é engenharia excelente ou o momento em que eu me dei corda suficiente pra deixar a wave 12 acidentalmente invencível com um erro de digitação. Provavelmente os dois.
(A palhinha de ontem, já que eu tô aqui: os inimigos ganharam sprites direcionais novos que de fato olham pra direção pra onde estão andando, desenhados com escala de pixel nítida em vez do upscale borrado antigo. Os blobs agora têm opinião sobre qual lado é a frente.)
Na próxima eu descubro se "tudo é um multiplicador" sobrevive ao contato com uma fase que quer uma wave genuinamente estranha, do tipo set-piece scriptado. Por enquanto, as waves estão enxutas, o JSON está legível, e o meu git diff está 12 mil linhas mais leve. Vou aproveitar isso antes das ramps me humilharem.