Ir al contenido
wannacode

Héroes, enemigos, atributos

En resumen: Una criatura para todos: el héroe y los enemigos se distinguen por datos (filas de una tabla) y por dos métodos, y cada atributo es una suma de capas, base + equipo + efectos, que se calcula cada vez que se pregunta.

Problema

Diez enemigos, tres clases, una pregunta

El juego tiene diez tipos de enemigos y tres clases de héroe. Todos tienen vida, fuerza, destreza y defensa, y un golpe pregunta los mismos números a todos. ¿Cómo se escribe eso?

La primera idea de quien ha aprendido herencia es una clase por cada tipo: Enemy es el padre, Goblin y Skeleton son los hijos. Pronto se ve el árbol:

Enemy
├── Rat
├── Goblin
│ ├── GoblinSpearman
│ └── GoblinShaman
├── Undead
│ ├── Skeleton inmune a la hemorragia y al veneno
│ └── StoneServant inmune a la hemorragia y al veneno (¡otra vez!)
├── Golem inmune a la hemorragia y al veneno (¡otra vez!!)
└── ...

El esqueleto, el siervo de piedra y el gólem no sangran ni se envenenan, pero no son de una misma familia: uno es muerto viviente, los otros son de piedra. O bien la inmunidad se copia en tres clases, o hace falta una clase «Sin sangre» entre ellos que cruce la familia «No muerto». Y una segunda característica (débil al fuego) sería otra familia cruzada. Un árbol profundo está bien mientras cada caso quepa en una rama.

Hay otro problema: un enemigo nuevo significa una clase nueva, código compilado nuevo y un programador. Un diseñador que quiere un goblin con 30 de vida no puede hacerlo solo.

En RPG 1 las cinco razas heredan de un Character. Allí la herencia está en su sitio, porque cada raza hace algo distinto (su propia habilidad). Aquí los enemigos se distinguen por números. El comportamiento distinto va en un método. Los números distintos son datos.

La idea

Un enemigo es una fila de una tabla

La primera regla del juego: si dos cosas se distinguen solo por números, son una clase y dos filas de datos. Los enemigos de esta lección son cuatro líneas de texto, con las celdas separadas por |:

# id|nombre|vida|fuerza|destreza|defensa|inmune
rat|rata gigante|9|1|6|0|-
goblin|goblin lancero|16|2|5|1|-
skeleton|esqueleto|28|4|4|2|bleed,poison
golem|gólem de mena|46|6|1|4|bleed,poison

El juego funciona exactamente así, solo que con diez enemigos. Esta es la tabla real del canon. Las columnas que esta lección no usa (signo, color, daño, experiencia, oro, botín, habilidad, profundidad) se han omitido, y las filas son las mismas:

# id|nombre|vida|fuerza|destreza|defensa|velocidad|resiste|débil|inmune|ánimo
rat|rata gigante|9|1|6|0|12|-|-|-|wander
goblin|goblin lancero|16|2|5|1|10|-|-|-|wander
shaman|goblin chamán|13|1|4|0|9|-|-|-|sleep
wolf|lobo sangriento|20|3|8|1|14|-|-|-|sleep
wolf_leader|lobo jefe|36|4|9|2|14|-|-|-|sleep
skeleton|esqueleto|28|4|4|2|10|slash|blunt|bleed,poison|sleep
cultist|cultista|26|3|6|1|11|-|-|-|wander
golem|gólem de mena|46|6|1|4|7|pierce,fire|blunt|bleed,poison|sleep
servant|siervo de piedra|14|2|3|1|9|-|blunt|bleed,poison|sleep
warden|Guardián abisal|120|5|4|3|10|-|-|bleed,poison|guard

El código lee la tabla al arrancar y crea un registro por cada fila. Un enemigo nuevo es una fila nueva: no cambia ni una línea de código. Esto se llama dirigido por datos (data-driven): el juego es un motor y el contenido está en tablas que se editan sin programador.

Más precisamente, para curiosos

Así se leen las tablas de la lección (los enemigos, los objetos y los efectos). El texto es una constante, y rows lo divide en líneas y celdas, saltando las líneas vacías y las notas que empiezan por #:

const STATS = ["maxHp", "strength", "agility", "defense", "evasion"];
const SLOTS = ["armor", "ring"];
const ITEMS_TEXT = `
# id|nombre|ranura|modificadores
leather|armadura de cuero|armor|defense:2
chain|cota de malla|armor|defense:4,evasion:-5
ring_strength|anillo de fuerza|ring|strength:2
ring_life|anillo de vida|ring|maxHp:10
`;
const EFFECTS_TEXT = `
# id|nombre|modificadores
rage|furia|strength:3,defense:-2
shield|escudo|defense:3
weak|debilidad|strength:-4
`;
const ENEMIES_TEXT = `
# id|nombre|vida|fuerza|destreza|defensa|inmune
rat|rata gigante|9|1|6|0|-
goblin|goblin lancero|16|2|5|1|-
skeleton|esqueleto|28|4|4|2|bleed,poison
golem|gólem de mena|46|6|1|4|bleed,poison
`;
function rows(table) {
const result = [];
for (const line of table.split("\n")) {
const text = line.trim();
if (text === "" || text.startsWith("#")) continue;
result.push(text.split("|"));
}
return result;
}
function parseMods(text) {
const mods = {};
if (text === "-") return mods;
for (const part of text.split(",")) {
const [stat, value] = part.split(":");
mods[stat] = Number(value);
}
return mods;
}
const ITEMS = {};
for (const [id, name, slot, mods] of rows(ITEMS_TEXT)) {
ITEMS[id] = { id, name, slot, mods: parseMods(mods) };
}
const EFFECTS = {};
for (const [id, name, mods] of rows(EFFECTS_TEXT)) {
EFFECTS[id] = { id, name, mods: parseMods(mods) };
}
const KINDS = {};
for (const [id, name, hp, strength, agility, defense, immune] of rows(ENEMIES_TEXT)) {
KINDS[id] = { id, name, hp: Number(hp), strength: Number(strength), agility: Number(agility), defense: Number(defense), immune: immune === "-" ? [] : immune.split(",") };
}
Los registros son objetos simples, y los modificadores son un objeto «atributo → número». Object.values(KINDS) devuelve los enemigos en el orden en que se añadieron.

La idea

Una criatura para todos

Las diferencias entre el héroe y los enemigos son pocas. Por eso hay una clase Creature con todo lo común: un nombre, vida, una base (un número para cada atributo) y una lista de efectos. Este es su comienzo:

class Creature {
constructor(name) {
this.name = name;
this.base = {};
for (const stat of STATS) this.base[stat] = 0;
this.effects = [];
this.hp = 0;
}

Creature tiene un mapa de atributos y una lista de efectos; no es un goblin ni un esqueleto. Esto es composición: montas una criatura con piezas en lugar de heredarla de una criatura más general. Un enemigo de la tabla simplemente rellena la base con su fila:

class Enemy extends Creature {
constructor(kind) {
super(kind.name);
this.kind = kind;
this.base.maxHp = kind.hp;
this.base.strength = kind.strength;
this.base.agility = kind.agility;
this.base.defense = kind.defense;
this.hp = kind.hp;
}
// La inmunidad son datos: una lista de la fila de la tabla.
isImmune(effectId) {
return this.kind.immune.includes(effectId);
}
}

Y el héroe lleva algo más: equipo.

class Hero extends Creature {
constructor(name, maxHp, strength, agility, defense) {
super(name);
this.base.maxHp = maxHp;
this.base.strength = strength;
this.base.agility = agility;
this.base.defense = defense;
this.hp = maxHp;
this.equipment = {};
for (const slot of SLOTS) this.equipment[slot] = "";
}

Queda un nivel de herencia: Creature → Hero y Enemy. Las dos subclases sustituyen un método cada una, porque solo ahí el comportamiento es distinto: el héroe tiene gearBonus (equipo), el enemigo tiene isImmune (inmunidad de su fila). Todo lo demás, incluido todo el sistema de combate, pregunta a una Creature y no sabe a quién pregunta.

En el canon es igual. Aquí están el constructor de Enemy y su isImmune:

constructor(id, kind) {
super(id, nm(kind));
this.kind = kind;
this.base.set('maxHp', kind.hp);
this.base.set('strength', kind.strength);
this.base.set('agility', kind.agility);
this.base.set('defense', kind.defense);
this.base.set('speed', kind.speed);
this.hp = kind.hp;
this.state = kind.mood;
this.calm = 0;
this.lostTurns = 0;
this.phase = 1;
this.dead = false;
this.summoned = false;
this.noticed = false;
}
isImmune(effectId) {
return this.kind.immune.indexOf(effectId) >= 0;
}

Un enemigo de la tabla copia sus números en base y guarda una referencia a su fila (kind). La inmunidad no es una clase, sino una lista de esa fila.

Más precisamente, para curiosos

La regla «composición antes que herencia» no dice «no heredes nunca». Dice: hereda para el comportamiento (un método es distinto), monta para la composición (las piezas son distintas). Aquí Creature guarda el esqueleto común y las subclases son dos y delgadas. Si los héroes fueran cinco razas con cinco habilidades, como en RPG 1, la herencia sería la herramienta correcta.

En el canon los efectos también son clases pequeñas (PoisonEffect, RageEffect), porque el veneno y el fuego hacen algo cada turno. Un efecto que solo cambia números puede ser también una fila de tabla, como en la lección.

Míralo

Todos con un solo código

El héroe y los cuatro enemigos entran en una lista de Creature. El bucle no pregunta quién es quién: llama a maxHp, stat y evasion, y a un enemigo inmune a la hemorragia le añade una nota. Después todos reciben el mismo efecto.

const everyone = [new Hero("bárbaro", 48, 7, 4, 3)];
for (const kind of Object.values(KINDS)) everyone.push(new Enemy(kind));
for (const c of everyone) {
let line = c.name + ": vida " + c.maxHp() + ", fuerza " + c.stat("strength") + ", defensa " + c.stat("defense") + ", esquiva " + c.evasion();
if (c.isImmune("bleed")) line += ", inmune a la hemorragia y al veneno";
console.log(line);
}
console.log("Debilidad para todos:");
for (const c of everyone) {
const before = c.stat("strength");
c.addEffect("weak");
console.log(c.name + ": fuerza " + before + " -> " + c.stat("strength"));
}
Salida
bárbaro: vida 48, fuerza 7, defensa 3, esquiva 8
rata gigante: vida 9, fuerza 1, defensa 0, esquiva 12
goblin lancero: vida 16, fuerza 2, defensa 1, esquiva 10
esqueleto: vida 28, fuerza 4, defensa 2, esquiva 8, inmune a la hemorragia y al veneno
gólem de mena: vida 46, fuerza 6, defensa 4, esquiva 2, inmune a la hemorragia y al veneno
Debilidad para todos:
bárbaro: fuerza 7 -> 3
rata gigante: fuerza 1 -> 0
goblin lancero: fuerza 2 -> 0
esqueleto: fuerza 4 -> 0
gólem de mena: fuerza 6 -> 2

La debilidad resta 4 de fuerza a todos. La rata, el goblin y el esqueleto bajan de cero y se quedan en 0: no pueden golpear con fuerza negativa. El gólem tiene 6, así que conserva 2. La siguiente tarjeta explica por qué.

La idea

Un atributo es una suma de capas

La fuerza del héroe no es un número. Es una suma:

  • base: con lo que nace el héroe (las 7 de fuerza del bárbaro). Se guarda en la criatura;
  • equipo: los bonus de los objetos que lleva ahora (+2 del anillo). No se guarda, se reúne de las ranuras;
  • efectos: los bonus y las cargas de los efectos activos ahora (+3 de la furia). No se guarda, se reúne de la lista de efectos.
// Los efectos se suman: dos efectos sobre un atributo se acumulan.
effectBonus(stat) {
let total = 0;
for (const effect of this.effects) total += effect.mods[stat] ?? 0;
return total;
}
// El valor en bruto (puede ser negativo): base + equipo + efectos.
rawStat(stat) {
return this.base[stat] + this.gearBonus(stat) + this.effectBonus(stat);
}
// Un atributo nunca baja de cero.
stat(stat) {
const value = this.rawStat(stat);
return value < 0 ? 0 : value;
}

El total se calcula en cada pregunta y no se guarda en ningún sitio. Así no puede desincronizarse. Te quitas la cota y la defensa baja, porque gearBonus no encuentra el objeto. Termina un efecto y la fuerza vuelve, porque effectBonus no encuentra el efecto. Nada «devuelve el número». Si la criatura guardara la defensa total, tendrías que acordarte de cambiarla en cada suceso: equipar, quitar, empieza un efecto, termina un efecto, el héroe sube de nivel. Un sitio olvidado y la defensa «se vuelve loca».

El héroe del programa es un bárbaro (48 de vida, 7 de fuerza, 4 de destreza, 3 de defensa) con cota de malla, anillo de fuerza y furia. Cada línea es base +equipo +efectos = total:

addEffect(id) {
for (const effect of this.effects) {
if (effect.id === id) return;
}
this.effects.push(EFFECTS[id]);
}
// Los efectos se suman: dos efectos sobre un atributo se acumulan.
effectBonus(stat) {
let total = 0;
for (const effect of this.effects) total += effect.mods[stat] ?? 0;
return total;
}
// El valor en bruto (puede ser negativo): base + equipo + efectos.
rawStat(stat) {
return this.base[stat] + this.gearBonus(stat) + this.effectBonus(stat);
}
// Un atributo nunca baja de cero.
stat(stat) {
const value = this.rawStat(stat);
return value < 0 ? 0 : value;
}
gearBonus(stat) {
let total = 0;
for (const slot of SLOTS) {
const id = this.equipment[slot];
if (id === "") continue;
total += ITEMS[id].mods[stat] ?? 0;
}
return total;
}
// La vida nunca supera el nuevo máximo.
equip(id) {
this.equipment[ITEMS[id].slot] = id;
if (this.hp > this.maxHp()) this.hp = this.maxHp();
}
Paso a paso, en palabras
  1. Solo se guarda la base: fuerza 7, destreza 4, defensa 3. La esquiva es derivada: destreza × 2 = 8.
  2. Cota de malla: defensa 3 + 4 = 7. El bonus de esquiva es -5, pero stat() es 0; el número en bruto queda y la esquiva baja a 3.
  3. El anillo va en otra ranura y no echa la cota: las ranuras se suman. Fuerza 7 + 2 = 9.
  4. Furia: +3 de fuerza, −2 de defensa. effectBonus suma los efectos: fuerza 12, defensa 5. Al terminar, la capa desaparece.
  5. El total se calcula de nuevo: stat() = max(0, base + equipo + efectos). No se guarda, así que no puede desincronizarse.
const hero = new Hero("bárbaro", 48, 7, 4, 3);
console.log("-- Al principio --");
report(hero);
hero.equip("chain");
hero.equip("ring_strength");
console.log("-- Con la cota de malla y el anillo de fuerza --");
report(hero);
hero.addEffect("rage");
console.log("-- Y con furia --");
report(hero);
Salida
-- Al principio --
bárbaro: vida 48/48
maxHp: 48 +0 +0 = 48
strength: 7 +0 +0 = 7
agility: 4 +0 +0 = 4
defense: 3 +0 +0 = 3
evasion: 0 +0 +0 = 0
esquiva: 8
-- Con la cota de malla y el anillo de fuerza --
bárbaro: vida 48/48
maxHp: 48 +0 +0 = 48
strength: 7 +2 +0 = 9
agility: 4 +0 +0 = 4
defense: 3 +4 +0 = 7
evasion: 0 -5 +0 = 0
esquiva: 3
-- Y con furia --
bárbaro: vida 48/48
maxHp: 48 +0 +0 = 48
strength: 7 +2 +3 = 12
agility: 4 +0 +0 = 4
defense: 3 +4 -2 = 5
evasion: 0 -5 +0 = 0
esquiva: 3

Mira la última línea: evasion: 0 -5 +0 = 0, pero la esquiva es 3. Por qué, lo dice la siguiente tarjeta.

La idea

El número en bruto y el atributo

La cota pesa: resta 5 a la esquiva. Pero un atributo con signo menos no tiene sentido: una fuerza de −3 convertiría un golpe en curación. Por eso stat no baja de cero:

// Un atributo nunca baja de cero.
stat(stat) {
const value = this.rawStat(stat);
return value < 0 ? 0 : value;
}
maxHp() {
const value = this.stat("maxHp");
return value < 1 ? 1 : value;
}

La esquiva, en cambio, tiene que saber que la cota pesa. No usa stat, sino el número en bruto (rawStat), que puede ser negativo:

// La esquiva sale de la destreza y del bonus en bruto (que puede ser negativo).
evasion() {
return this.stat("agility") * 2 + this.rawStat("evasion");
}

Con destreza 4 la esquiva es 4 × 2 = 8. La cota resta 5, pero stat("evasion") es 0, mientras que la esquiva es 8 − 5 = 3. Si la esquiva usara stat("evasion"), la cota no costaría nada.

maxHp tiene su propia regla: no baja de 1. Una criatura con 0 de vida máxima está muerta en cuanto nace, y dividir por la vida máxima (por ejemplo, una barra en la interfaz) fallaría.

return value < 0 ?  : value;

El juego hace lo mismo, con algunos números derivados más: puntería, probabilidad de golpe crítico y «herido de gravedad» (por debajo del 25 % de vida los golpes son menos precisos):

// El valor en bruto (puede ser negativo): base + equipo + efectos.
rawStat(stat) {
let value = this.base.get(stat) + this.gearBonus(stat);
for (const effect of this.effects) value += effect.modifier(stat);
return value;
}
stat(stat) {
const value = this.rawStat(stat);
return value < 0 ? 0 : value;
}
maxHp() {
const value = this.stat('maxHp');
return value < 1 ? 1 : value;
}
maxMana() {
return this.stat('maxMana');
}
// Con menos del 25 % de vida la herida es grave: los golpes se vuelven menos certeros.
isHeavilyWounded() {
return this.hp * 4 < this.maxHp();
}
accuracy() {
return 75 + this.stat('agility') * 2 + this.rawStat('accuracy') - (this.isHeavilyWounded() ? 15 : 0);
}
evasion() {
return this.stat('agility') * 2 + this.rawStat('evasion');
}
critChance() {
return 5 + Math.floor(this.stat('agility') / 10);
}

De ahí viene rawStat en el canon: suma la base, gearBonus y cada efecto con effect.modifier(stat). La lección mantiene effectBonus aparte para que veas las tres capas una a una.

La idea

El héroe lleva, el enemigo no

El Creature.gearBonus base devuelve 0, porque un enemigo no tiene equipo. El héroe lo sustituye:

gearBonus(stat) {
let total = 0;
for (const slot of SLOTS) {
const id = this.equipment[slot];
if (id === "") continue;
total += ITEMS[id].mods[stat] ?? 0;
}
return total;
}

El héroe guarda solo qué objeto está en qué ranura (nombre → id). Los números están en la tabla de objetos. Así, una cota es una fila de datos, no una copia en cada héroe, y cambiar el equilibrio es cambiar un número en un solo sitio.

Cuando un objeto entra o sale, la vida máxima puede cambiar. El anillo de vida da +10. Si lo quitas cuando el héroe está en 58 de 58, la vida actual sigue en 58 con un máximo de 48: algo que no debe ocurrir. Por eso equip y unequip devuelven la vida al límite:

// La vida nunca supera el nuevo máximo.
equip(id) {
this.equipment[ITEMS[id].slot] = id;
if (this.hp > this.maxHp()) this.hp = this.maxHp();
}

En el canon es igual (gearBonus recorre las ranuras y suma los modificadores):

gearBonus(stat) {
let total = 0;
for (const slot of this.equipment.keys()) {
const itemId = this.equipment.get(slot);
if (itemId === '') continue;
for (const mod of ITEMS.get(itemId).mods) {
if (mod.stat === stat) total += mod.value;
}
}
return total;
}
weapon() {
const id = this.equipment.get('weapon');
return ITEMS.get(id === '' ? 'fists' : id);
}
Más precisamente, para curiosos

En el canon el héroe tiene también una tercera ranura, el arma, y weapon() devuelve los puños cuando la ranura está vacía, para que no haya un caso especial «sin arma». Así el héroe siempre tiene con qué golpear. En la lección las ranuras son dos, para que los números se vean.

Los objetos se guardan por id (una cadena), no por referencia. Por eso la partida guardada es sencilla (se guarda «ring_life», no un objeto) y el mismo objeto puede estar en muchos sitios.

Pruébalo

Predice la salida

El héroe se pone el anillo de vida (+10 de vida máxima) y se cura del todo. Después le cae la debilidad (−4 de fuerza) y se quita el anillo. ¿Qué imprime el programa?

const hero = new Hero("bárbaro", 48, 7, 4, 3);
hero.equip("ring_life");
hero.hp = hero.maxHp();
hero.addEffect("weak");
hero.unequip("ring");
console.log(hero.hp + "/" + hero.maxHp());
console.log(hero.stat("strength"));
Respuesta

Pruébalo

Encuentra el error

Un compañero «simplificó» una línea, y ahora un héroe con furia y escudo tiene números raros. Los dos efectos se ponen uno tras otro, y el programa imprime esto:

const hero = new Hero("bárbaro", 48, 7, 4, 3);
hero.addEffect("rage");
hero.addEffect("shield");
report(hero);
Salida
bárbaro: vida 48/48
maxHp: 48 +0 +0 = 48
strength: 7 +0 +0 = 7
agility: 4 +0 +0 = 4
defense: 3 +0 +3 = 6
evasion: 0 +0 +0 = 0
esquiva: 8

La furia no ayuda a la fuerza (no hay +3), y la defensa es 3 + 3, no 3 − 2 + 3. Aquí están los tres métodos que calculan las capas. ¿Qué línea tiene la culpa?

Elige la línea con el error.

Más precisamente

Cuándo una clase, cuándo una fila

Cuando aparece algo nuevo en el juego, pregunta: ¿se distingue por números o por comportamiento?

  • Un enemigo nuevo con otra vida, fuerza y defensa, una inmunidad al fuego, otro ánimo: una fila de la tabla;
  • un objeto nuevo con modificadores: una fila de la tabla, y un tipo nuevo de ranura es un nombre nuevo en la lista de ranuras;
  • un efecto que solo cambia números (furia, escudo, debilidad): una fila de la tabla;
  • un efecto que hace algo cada turno (veneno, fuego, curación): un método, por eso en el canon es una clase pequeña;
  • un enemigo que decide cómo moverse (el chamán invoca, el lobo huye): comportamiento, es decir, código. En el canon son habilidades y máquinas de estados, no subclases de Creature.

Una clase (o un método sustituido) hace falta cuando el comportamiento distinto no cabe en un número. Todo lo demás crece con filas de una tabla.

Ejercicio

El amuleto

Resuelve la tarea en tu editor.

Amuleto. Añade una tercera ranura amulet y un objeto amulet_bear («amuleto del oso»: +6 de vida máxima, +1 de fuerza, −1 de destreza) solo como una fila nueva en la tabla de objetos y un nombre nuevo en la lista de ranuras. Sin clase nueva. El programa lee comandos de la entrada (uno por línea): equip <objeto>, remove <ranura>, buff <efecto> y heal (la vida pasa a ser la máxima). Tras cada comando escribe > comando y una línea: vida/máximo | fuerza | defensa | destreza | esquiva. El héroe es el mismo bárbaro (48, 7, 4, 3).

Con la entrada equip chain / equip amulet_bear / heal / buff rage / remove amulet / remove armor (una por línea) la salida es:

Salida
> equip chain
48/48 | fuerza 7 | defensa 7 | destreza 4 | esquiva 3
> equip amulet_bear
48/54 | fuerza 8 | defensa 7 | destreza 3 | esquiva 1
> heal
54/54 | fuerza 8 | defensa 7 | destreza 3 | esquiva 1
> buff rage
54/54 | fuerza 11 | defensa 5 | destreza 3 | esquiva 1
> remove amulet
48/48 | fuerza 10 | defensa 5 | destreza 4 | esquiva 3
> remove armor
48/48 | fuerza 10 | defensa 1 | destreza 4 | esquiva 8

Fíjate en heal y en remove amulet: con el amuleto el máximo es 54 y heal rellena hasta 54, mientras que remove amulet devuelve la vida al límite de 48.

Más tareas

  • Un enemigo nuevo sin código. Añade el lobo sangriento del canon (20 de vida, 3 de fuerza, 8 de destreza, 1 de defensa, sin inmunidad) solo como una fila de la tabla de enemigos. ¿Cuánto es su esquiva?
  • Un segundo anillo. Haz las ranuras ring1 y ring2. ¿Qué pasa con ring_strength si lo pones en las dos? ¿Es correcto?
  • Un efecto con duración. Dale al efecto un número de turnos y un método tick() que lo reduzca y quite el efecto al llegar a cero. ¿Qué debe hacer stat() cuando termine?

Test

Compruébate

1. El juego tiene un enemigo nuevo: igual que el goblin, pero con 30 de vida en lugar de 16. ¿Qué haces?
2. Un héroe con 3 de defensa se pone una cota de malla (+4 de defensa) y luego recibe furia (−2 de defensa). ¿Cuánto es stat("defense")?
3. El héroe tiene destreza 4 y una cota de malla con un bonus de esquiva de −5. ¿Cuánto es evasion()?
4. ¿Por qué Creature no tiene un campo «defensa con equipo» que se actualice al equiparse?
5. ¿Por qué Hero.gearBonus reescribe el método de Creature en lugar de tener simplemente un campo propio?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·