Capítulo 22 - Economia: Preços, Drops e Balanceamento
O Que Vamos Aprender
Neste capítulo você vai:
- Entender por que a economia importa em roguelikes (como o sistema de Gil em Final Fantasy): tudo tem preço, e o balanceamento decide se o jogo é justo ou quebrado
- Modelar loot tables com pesos aleatórios (como os drops em Diablo): cada criatura tem uma tabela de probabilidades
- Criar a classe
EntradaSaqueeEconomiapara governar preços e recompensas - Implementar cascatas de dificuldade: inimigos mais fortes em andares mais profundos
- Usar constantes de balanceamento para ajustes rápidos de design
- Simular corridas de teste para validar a curva de dificuldade
- Integrar drops no combate da Masmorra
Ao final, você terá um sistema econômico coerente que propicia progressão justa e recompensadora.
O Ciclo de Incentivos
Antes de código, pense no jogo como um jogador. Por que continuo descendo, tentando sobreviver?
- Ganho ouro dos inimigos que derroto
- Compro armas melhores com esse ouro na loja
- Com armas melhores, consigo derrotar inimigos mais fortes
- Inimigos mais fortes soltam mais ouro e itens raros
- Volto à loja, compro armadura, deço mais fundo
- No andar final, enfrento o chefe com tudo que consegui acumular
Este é o ciclo de progressão. É o coração psicológico do jogo. Se o primeiro inimigo soltar tanto ouro quanto o chefe, o jogo é entediante (sem senso de progressão). Se o primeiro inimigo soltar nada, é frustrante (sem recompensa). A economia bem balanceada é o pulso que mantém o jogo vivo.
Constantes de Balanceamento
Vamos definir constantes que governam toda a progressão. Isso é crucial porque ajustar um número muda tudo. É como ajustar a economia de Final Fantasy: um número muda, e o jogo inteiro se desbalanceia ou se equilibra perfeitamente.
Estas constantes vivem num único lugar (EconomiaConstants). Se você quer deixar o jogo mais fácil, muda um número e pronto. Sem procurar pelo código inteiro. Isto é design limpo.
// lib/economia_constants.dart
/// Constantes de balanceamento da economia
class EconomiaConstants {
/// Dificuldade escalonada por andar
static const int kBaseHPPorInimigo = 10;
static const double kAumentoHPPorAndar = 0.2; // +20% HP por andar
/// Recompensas em ouro
static const int kOuroBasePorInimigo = 5;
static const double kAumentoOuroPorAndar = 0.3; // +30% ouro por andar
/// Preços base da loja
static const int kPrecoEspadaFerro = 50;
static const int kPrecoArmaduraCouro = 75;
static const int kPrecoPocaoVida = 20;
/// Margem do comerciante
static const double kMargemVenda = 0.5; // Comerciante oferece 50%
}
Estas constantes são parâmetros de design. Ajuste um deles e observe como o jogo responde. A progressão fica lenta demais? Aumente kOuroBasePorInimigo. O primeiro inimigo é muito fácil e entediante? Aumente kAumentoHPPorAndar. Isto é iteração de design: números controlam a sensação inteira do jogo. Este é o coração invisível do balanceamento.
EntradaSaque: A Tabela de Drops
Cada tipo de inimigo tem uma tabela de drops, uma lista de itens que pode soltar com probabilidades. Por exemplo:
- Zumbi: 80% moeda de ouro, 15% adaga velha, 5% nada
- Lobo: 60% moeda de ouro, 30% espada de ferro, 10% poção de vida
- Orc: 50% moeda de ouro, 40% poção de vida, 10% nada
Loot tables são como os drops em Diablo: cada monstro tem uma probabilidade de soltar cada item. Modelamos isto com a classe EntradaSaque, que encapsula item, chance e quantidade mínima/máxima.
// lib/entrada_saque.dart
import 'dart:math';
/// Uma entrada na tabela de drops de um inimigo
/// Define qual item pode cair, com que
/// probabilidade, e em que quantidade
class EntradaSaque {
final String itemId;
final double chance;
final int quantidadeMin;
final int quantidadeMax;
final String nomeItem;
EntradaSaque({
required this.itemId,
required this.chance,
required this.quantidadeMin,
required this.quantidadeMax,
required this.nomeItem,
}) : assert(chance >= 0.0 && chance <= 1.0),
assert(quantidadeMin >= 0 && quantidadeMax >= quantidadeMin);
/// Calcula a quantidade a cair (entre min e max)
int resolverQuantidade(Random random) {
if (quantidadeMin == quantidadeMax) {
return quantidadeMin;
}
return quantidadeMin +
random.nextInt(quantidadeMax - quantidadeMin + 1);
}
@override
String toString() =>
'$nomeItem (${(chance * 100).toStringAsFixed(1)}%): '
'$quantidadeMin—$quantidadeMax';
}
Classe Economia: O Governador
A classe Economia centraliza toda a lógica de economia. Tem dois serviços principais:
- Determinar drops após combate (usa um Rolador para decisões probabilísticas)
- Calcular preços de compra e venda
// lib/economia.dart
import 'dart:math';
import 'rolador.dart';
import 'entrada_saque.dart';
import 'economia_constants.dart';
/// Sistema de economia: drops, preços, balanceamento
class Economia {
final Map<String, List<EntradaSaque>> tabelasDrops;
final Rolador roller;
Economia({
required this.tabelasDrops,
Rolador? roller,
}) : roller = roller ?? Rolador();
/// Resolve os drops de um inimigo derrotado
List<String> resolverDrop(String nomeInimigo) {
final drops = tabelasDrops[nomeInimigo];
if (drops == null) {
return ['ouro:${EconomiaConstants.kOuroBasePorInimigo}'];
}
final resultado = <String>[];
for (final entry in drops) {
if (roller.teste(entry.chance)) {
final qtd = entry.resolverQuantidade(roller.random);
resultado.add('${entry.itemId}:$qtd');
}
}
if (resultado.isEmpty) {
resultado.add('ouro:${EconomiaConstants.kOuroBasePorInimigo}');
}
return resultado;
}
/// Calcula o preço de compra (preço que você paga à loja)
int precoCompra(String itemId) {
return switch (itemId) {
'espada_ferro' => EconomiaConstants.kPrecoEspadaFerro,
'espada_aco' =>
(EconomiaConstants.kPrecoEspadaFerro * 1.5)
.toInt(),
'espada_mithril' =>
(EconomiaConstants.kPrecoEspadaFerro * 3.0)
.toInt(),
'armadura_couro' => EconomiaConstants.kPrecoArmaduraCouro,
'armadura_ferro' =>
(EconomiaConstants.kPrecoArmaduraCouro * 1.5).toInt(),
'pocao_vida' => EconomiaConstants.kPrecoPocaoVida,
'pocao_restauracao' =>
(EconomiaConstants.kPrecoPocaoVida * 2).toInt(),
_ => 10,
};
}
/// Calcula o preço de venda (preço que o comerciante oferece)
int precoVenda(String itemId) {
final compra = precoCompra(itemId);
return (compra * EconomiaConstants.kMargemVenda).toInt();
}
/// Retorna dificuldade escalonada para um andar
double getDificuldadeAndar(int numero) {
return 1.0 + (numero * EconomiaConstants.kAumentoHPPorAndar);
}
/// Retorna recompensa escalonada para um andar
int getOuroEscalonado(int numero) {
final base = EconomiaConstants.kOuroBasePorInimigo.toDouble();
final aum = EconomiaConstants.kAumentoOuroPorAndar;
final multiplicador = 1.0 + (numero * aum);
return (base * multiplicador).toInt();
}
}
Criando as Tabelas de Drops
Agora populamos as tabelas com dados concretos para cada tipo de inimigo:
// lib/tabelas_drops.dart
import 'entrada_saque.dart';
/// Tabelas de drops padrão para todos os tipos de inimigo
class TabelasDrops {
static Map<String, List<EntradaSaque>> criar() {
return {
'Zumbi': [
EntradaSaque(
itemId: 'ouro',
chance: 1.0,
quantidadeMin: 3,
quantidadeMax: 8,
nomeItem: 'Moedas de ouro',
),
EntradaSaque(
itemId: 'adaga_velha',
chance: 0.15,
quantidadeMin: 1,
quantidadeMax: 1,
nomeItem: 'Adaga velha',
),
],
'Lobo': [
EntradaSaque(
itemId: 'ouro',
chance: 0.9,
quantidadeMin: 5,
quantidadeMax: 15,
nomeItem: 'Moedas de ouro',
),
EntradaSaque(
itemId: 'espada_ferro',
chance: 0.25,
quantidadeMin: 1,
quantidadeMax: 1,
nomeItem: 'Espada de ferro',
),
EntradaSaque(
itemId: 'pocao_vida',
chance: 0.1,
quantidadeMin: 1,
quantidadeMax: 2,
nomeItem: 'Poção de vida',
),
],
'Orc': [
EntradaSaque(
itemId: 'ouro',
chance: 0.95,
quantidadeMin: 15,
quantidadeMax: 30,
nomeItem: 'Moedas de ouro',
),
EntradaSaque(
itemId: 'espada_aco',
chance: 0.35,
quantidadeMin: 1,
quantidadeMax: 1,
nomeItem: 'Espada de aço',
),
EntradaSaque(
itemId: 'armadura_ferro',
chance: 0.2,
quantidadeMin: 1,
quantidadeMax: 1,
nomeItem: 'Armadura de ferro',
),
],
};
}
}
Integrando Drops no Combate
Quando você derrota um inimigo, resolvemos o drop. Isto acontece no sistema de combate:
// Exemplo: em combate.dart, quando inimigo morre
void executarCombate(
Jogador jogador,
Inimigo inimigo,
Economia economia,
) {
// ... combate normal ...
if (!inimigo.estaVivo) {
print('${inimigo.nome} foi derrotado!');
final drops = economia.resolverDrop(inimigo.nome);
for (final drop in drops) {
final partes = drop.split(':');
final tipo = partes[0];
final quantidade = int.parse(partes[1]);
if (tipo == 'ouro') {
jogador.ouro += quantidade;
print('Você ganhou $quantidade ouro!');
} else {
jogador.adicionarItem(tipo, quantidade);
print('Você encontrou: $tipo x$quantidade');
}
}
}
}
Testando a Curva de Dificuldade
Uma boa economia só se revela após testes. Simule 100 corridas e veja se você sai ganhando ou quebrado. A classe SimuladorEconomia roda múltiplas corridas hipotéticas, contando ouro ganho, e mostra estatísticas: média, mínimo, máximo. Se a média é muito alta ou muito baixa, você ajusta as constantes e testa de novo.
// lib/simulador_economia.dart
import 'dart:math';
import 'economia.dart';
import 'tabelas_drops.dart';
/// Simula corridas de teste para validar balanceamento
class SimuladorEconomia {
final Economia economia;
SimuladorEconomia(this.economia);
/// Simula N corridas e retorna estatísticas médias
Map<String, dynamic> simularCorridas(int numCorridas) {
final stats = <int>[];
for (int i = 0; i < numCorridas; i++) {
int ouroTotal = 0;
for (final nomeInimigo in ['Zumbi', 'Lobo', 'Orc']) {
final drops = economia.resolverDrop(nomeInimigo);
for (final drop in drops) {
final partes = drop.split(':');
if (partes[0] == 'ouro') {
ouroTotal += int.parse(partes[1]);
}
}
}
stats.add(ouroTotal);
}
final media = stats.reduce((a, b) => a + b) / stats.length;
final minimo = stats.reduce((a, b) => min(a, b));
final maximo = stats.reduce((a, b) => max(a, b));
return {
'corridas': numCorridas,
'ouro_medio': media.toStringAsFixed(2),
'ouro_minimo': minimo,
'ouro_maximo': maximo,
};
}
}
Uso (exemplo de como rodar a simulação):
void main() {
final economia = Economia(tabelasDrops: TabelasDrops.criar());
final simulador = SimuladorEconomia(economia);
final resultado = simulador.simularCorridas(100);
print('Simulação de 100 corridas:');
print(resultado);
}
Saída esperada:
Simulação de 100 corridas:
{corridas: 100, ouro_medio: 85.45, ouro_minimo: 52, ouro_maximo: 148}
Se a média é muito baixa, aumente kOuroBasePorInimigo. Se é muito alta, reduza. Isto é iteração de design.
Dificuldade por Andar
A dificuldade aumenta gradualmente. Quanto mais fundo, mais perigoso. O sistema usa getDificuldadeAndar() para calcular um multiplicador: no andar 0, é 1.0x (normal). No andar 3, é 1.6x (60% mais forte). No andar 10, é 3.0x (3 vezes mais forte).
// Exemplo de escalação de dificuldade por andar
void aplicarDificuldadeAndar(
Inimigo inimigo,
int andarNumero,
Economia economia,
) {
final multiplicador = economia.getDificuldadeAndar(andarNumero);
inimigo.hpMax = (inimigo.hpMax * multiplicador).toInt();
inimigo.hp = inimigo.hpMax;
inimigo.ataque = (inimigo.ataque * multiplicador).toInt();
print('Inimigo escalonado para andar $andarNumero: '
'HP=${inimigo.hpMax}, ATK=${inimigo.ataque}');
}
Isto significa:
- Andar 0: Zumbi tem 10 HP
- Andar 3: Zumbi tem 10 * 1.6 = 16 HP
- Andar 10: Zumbi tem 10 * 3.0 = 30 HP
A mesma criatura fica progressivamente mais desafiadora.
Desafios da Masmorra
Desafios Básicos
Desafio 22.1. O Tesouro do Dragão Antigo. A lenda diz que um dragão guardava uma Chave Dourada nos tempos antigos. Crie uma nova tabela de drops onde o Dragão tem 5% de chance de deixar cair essa chave rara. Implemente em EntradaSaque com id 'chave_dourada', chance 0.05, quantidade 1, descrição épica. Teste: derrote o dragão 20 vezes, conte quantas vezes recebe a chave. A probabilidade bate com 5%? Dica: use EntradaSaque para encapsular cada possível drop.
Desafio 22.2. Ganância do Comerciante. O comerciante da masmorra cobrava margem de 50%. Você descobriu que ele é ganancioso demais. Reduza a margem de venda para 30% mudando kMargemVenda de 0.5 para 0.3. Agora uma Espada de Ferro que custa 50 ouro vale quanto em venda? Calcule manualmente e depois valide em código. Os preços mais justos faz você comprar mais itens? Dica: novo preço = 50 × 0.3.
Desafios Avançados
Desafio 22.3. A Maldição dos Cinco Andares. Você desce 5 andares, cada um com 3 Lobos hostis. Implemente uma simulação: (1) Faça loop dos andares 0-4, (2) em cada andar, gere 3 Lobos com HP escalonado por getDificuldadeAndar(), (3) resolva drops de cada lobo, (4) some o ouro total. Execute e veja: quantos ouro ganharam? O HP dos lobos aumenta conforme desce? A economia se ajusta naturalmente? Dica: imprima resumo: “Andar X: 3 Lobos, Y ouro, HP variou de Z a W”.
Desafio 22.4. Modo Fácil para Aprendizes. Criar um jogo que escala dificuldade é difícil. Você quer testar em modo fácil onde tudo é menos letal. Crie EconomiaFacil extends Economia: dificuldade em 50% (inimigos mais fracos), drops em 150% (mais ouro). Simule 5 andares em modo fácil e modo normal, compare. Em fácil, sobrevive melhor? Ganha mais ouro? Dica: use super.getDificuldadeAndar() para chamar o pai e depois multiplicar.
Desafio 22.5. (Desafio): Raríssimo. Nem todo item é igual. Itens raros são mais caros. Crie um enum Raridade { comum, raro, mitico } e adicione esse campo em EntradaSaque. Depois, multiplique preço de compra: comum (1x), raro (3x), mítico (10x). Crie 3 drops de um inimigo: ouro comum (50 ouro), espada rara (200 ouro), artefato mítico (5000 ouro). Teste o balanceamento: qual é mais comum? Qual mais valioso? Dica: use switch/case no getter precoCompra().
Boss Final 22.6. A Profundeza Recompensa. Conforme desce, as recompensas aumentam. Implemente um bônus de +10% de ouro a cada 2 andares (andar 2→+10%, andar 4→+20%, andar 6→+30%). Integre em getOuroEscalonado(). Teste descendo 10 andares: o ouro cresce suavemente ou tem saltos? Sinta-se recompensado pela sua coragem. Dica: use (andar ~/ 2) * 0.10 para calcular bônus.
Pergaminho do Capítulo
Neste capítulo, você aprendeu:
- Loot tables modelam o que cada inimigo deixa cair quando morre
EntradaSaqueencapsula item, chance e quantidade;Roladorresolve aleatoriedadeEconomiaé o governador central: preços, drops, dificuldade escalonada- Constantes de balanceamento permitem ajustar o jogo rapidamente
- Simulação valida a curva: 100 corridas revelam se o jogo é justo ou quebrado
- Dificuldade por andar escala inimigos naturalmente, sem queda abrupta
A economia é o pulso invisível. Inimigos derrotados alimentam o ciclo: ouro para armas melhores para derrotar inimigos mais fortes. Sem isto, o jogo é apenas um labirinto.
::: vocab Vocabulário do Dia
- Curva de dificuldade - crescimento dos stats dos inimigos por andar; tem que ser desafiador sem ser impossível.
EntradaSaque- par (item, peso) que compõe uma tabela de drops; usado pelo combate para sortear recompensas.- Modificador econômico - coeficiente que multiplica preços ou drops; ajuste fino sem mudar constantes individuais.
- Balanceamento - afinar números (HP, dano, preços) para o jogo ser justo e divertido; trabalho de iteração.
- Recompensa proporcional - drops escalam com a dificuldade do andar; ouro e XP de inimigo profundo vale mais. :::
Dica Profissional
Próximo Capítulo
No Capítulo 23, a economia ganha uma interface tangível. Vamos construir a loja do mercador — com ItemVenda, Mercador, LojaRenderer e ModoLoja — onde o jogador pode comprar, vender e planejar estraticamente seus próximos movimentos.