Dois Números e uma Reta
As torres do td-survivors sobem de nível 1 a 100, e cada nível tem seu próprio dano, alcance, cadência de tiro e um monte de stats específicos de comportamento. Isso dá cem blocos de stats por torre, vezes uma dúzia de torres. Hoje eu parei de fingir que queria escrever tudo isso na mão, e fechei com um modelo de scaling do qual eu tô um pouco convencido: descrever só as duas pontas, level1 e level100, e deixar uma reta desenhar os outros noventa e oito. É o mesmo instinto de "deixa o dado minúsculo" que colocou as waves de dieta semana passada, agora apontado pros stats de torre.
O JSON da torre agora é só o começo e o fim dela
Um tipo de torre declara dois blocos de stats e nada no meio:
{
"id": "gun",
"level1": { "damage": 30, "range": 4, "interval": 0.55 },
"level100": { "damage": 200, "range": 10, "interval": 0.2 }
}
O nível 50 não está escrito em lugar nenhum. Ele é calculado: interpola cada campo pelo quanto você já andou na trilha de 1 a 100. O modelo inteiro é uma função pura:
const t = (level - 1) / (maxLevel - 1) // 0 no lvl 1, 1 no lvl 100
out[key] = a + (b - a) * t // a = level1, b = level100
É isso. É esse o sistema de scaling. Quer que o dano de uma torre suba forte? Empurra o damage do level100 pra cima. Quer que o alcance quase não mexa? Escreve quase igual nas duas pontas. Pra deixar um stat totalmente fixo nos 100 níveis, escreve o mesmo número nos dois blocos e a reta fica com inclinação zero. O comportamento cai direto dos dois números que você de fato enxerga, em vez de ficar escondido dentro de uma fórmula de crescimento que você tem que simular de cabeça.
Tem exatamente uma pegadinha. Alguns stats são contagens, não magnitudes: os saltos da corrente do tesla, os quiques do ricochet, o máximo de armadilhas do spike. Uma torre com "3,7 alvos na corrente" não faz sentido, então esses campos (listados no intFields do scaling.json) são arredondados pra número inteiro depois da interpolação. O resto continua float.
O sistema que apaguei era mais esperto, e esse era o problema
O modelo antigo era um multiplicador único. Cada nível multiplicava os stats base por 1 + 0.05 * (level - 1), então cada torre precisava só de um bloco de stats. Elegante no papel. Miserável na prática, porque nem todo stat quer subir. O intervalo de tiro deveria encolher conforme a torre sobe, não crescer, então precisava de uma lista inverseFields pra ser dividido em vez de multiplicado. Razões, ângulos e contagens não deveriam mexer, então precisavam de uma lista fixedFields pra serem pulados. E qualquer torre que não coubesse no multiplicador tamanho-único ganhava uma saída de emergência scaleMode: "linear" com... um segundo bloco de stats. Então eu tinha dois caminhos de código de scaling, três listas de config classificando cada campo, e torres divididas entre dois sistemas dependendo de o multiplicador uniforme servir ou não.
O modelo linear faz tudo isso evaporar. Intervalo encolhendo? Só escreve o interval do level100 menor que o do level1; a mesma interpolação corre ladeira abaixo. Um stat que não deve mexer? Mesmo valor nas duas pontas. Não existe mais taxonomia de "sobe vs desce vs fixo" porque a direção é só qual ponta é maior. O scaling.json saiu de um perLevel mais inverseFields mais fixedFields pra duas linhas: maxLevel e a listinha intFields. O resolveTowerLevel, o scaleMode e os dois arrays de classificação foram embora, e toda torre passa pelo mesmo caminho de código.
A lição, de novo: a representação comprimida esperta (um número, crescido por uma fórmula) custou mais que a burra e explícita (dois números, ligados por uma reta), porque a burra me deixa ver e setar direto a coisa com que eu de fato me importo, que é onde a torre termina no nível máximo. A mesma armadilha em que as waves caíram. Menos peças móveis, e as peças que sobraram são justo as que um humano calibrando o jogo realmente vai pegar.
Os upgrades são os próximos
O próximo passo é óbvio: as cartas de upgrade do run (dano, cadência, slow, raio de coleta) ainda sobem de nível do jeito antigo, na lógica particular de cada uma. Elas estão implorando pelo mesmo tratamento, um par level1/levelMax e uma reta entre eles, pra que o jogo inteiro fale uma língua de scaling só. Esse é o plano pra próxima, presumindo que a reta não desenvolva vontade de virar curva.