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|inmunerat|rata gigante|9|1|6|0|-goblin|goblin lancero|16|2|5|1|-skeleton|esqueleto|28|4|4|2|bleed,poisongolem|gólem de mena|46|6|1|4|bleed,poisonEl 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|ánimorat|rata gigante|9|1|6|0|12|-|-|-|wandergoblin|goblin lancero|16|2|5|1|10|-|-|-|wandershaman|goblin chamán|13|1|4|0|9|-|-|-|sleepwolf|lobo sangriento|20|3|8|1|14|-|-|-|sleepwolf_leader|lobo jefe|36|4|9|2|14|-|-|-|sleepskeleton|esqueleto|28|4|4|2|10|slash|blunt|bleed,poison|sleepcultist|cultista|26|3|6|1|11|-|-|-|wandergolem|gólem de mena|46|6|1|4|7|pierce,fire|blunt|bleed,poison|sleepservant|siervo de piedra|14|2|3|1|9|-|blunt|bleed,poison|sleepwarden|Guardián abisal|120|5|4|3|10|-|-|bleed,poison|guardEl 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 #:
record Item(string Id, string Name, string Slot, Dictionary<string, int> Mods);
record Effect(string Id, string Name, Dictionary<string, int> Mods);
record Kind(string Id, string Name, int Hp, int Strength, int Agility, int Defense, string[] Immune);
static class Data{ public static readonly string[] Stats = ["maxHp", "strength", "agility", "defense", "evasion"]; public static readonly string[] Slots = ["armor", "ring"];
public const string ItemsText = """ # 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 """;
public const string EffectsText = """ # id|nombre|modificadores rage|furia|strength:3,defense:-2 shield|escudo|defense:3 weak|debilidad|strength:-4 """;
public const string EnemiesText = """ # 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 """;
public static readonly Dictionary<string, Item> Items = new(); public static readonly Dictionary<string, Effect> Effects = new(); public static readonly Dictionary<string, Kind> Kinds = new();
static Data() { foreach (string[] row in Rows(ItemsText)) { Items[row[0]] = new Item(row[0], row[1], row[2], ParseMods(row[3])); } foreach (string[] row in Rows(EffectsText)) { Effects[row[0]] = new Effect(row[0], row[1], ParseMods(row[2])); } foreach (string[] row in Rows(EnemiesText)) { string[] immune = row[6] == "-" ? Array.Empty<string>() : row[6].Split(','); Kinds[row[0]] = new Kind(row[0], row[1], int.Parse(row[2]), int.Parse(row[3]), int.Parse(row[4]), int.Parse(row[5]), immune); } }
static List<string[]> Rows(string table) { var result = new List<string[]>(); foreach (string line in table.Split('\n')) { string text = line.Trim(); if (text == "" || text.StartsWith('#')) continue; result.Add(text.Split('|')); } return result; }
static Dictionary<string, int> ParseMods(string text) { var mods = new Dictionary<string, int>(); if (text == "-") return mods; foreach (string part in text.Split(',')) { string[] pair = part.Split(':'); mods[pair[0]] = int.Parse(pair[1]); } return mods; }}record es un registro con campos que no cambian. Un Dictionary devuelve los elementos en el orden de inserción mientras no se borre nada, y de las tablas de aquí nunca se borra.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{ public readonly string Name; public readonly Dictionary<string, int> Base = new(); public readonly List<Effect> Effects = new(); public int Hp;
public Creature(string name) { Name = name; foreach (string stat in Data.Stats) Base[stat] = 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 : Creature{ public readonly Kind Kind;
public Enemy(Kind kind) : base(kind.Name) { Kind = kind; Base["maxHp"] = kind.Hp; Base["strength"] = kind.Strength; Base["agility"] = kind.Agility; Base["defense"] = kind.Defense; Hp = kind.Hp; }
// La inmunidad son datos: una lista de la fila de la tabla. public override bool IsImmune(string effectId) => Array.IndexOf(Kind.Immune, effectId) >= 0;}Y el héroe lleva algo más: equipo.
class Hero : Creature{ public readonly Dictionary<string, string> Equipment = new();
public Hero(string name, int maxHp, int strength, int agility, int defense) : base(name) { Base["maxHp"] = maxHp; Base["strength"] = strength; Base["agility"] = agility; Base["defense"] = defense; Hp = maxHp; foreach (string slot in Data.Slots) 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:
public Enemy(int id, EnemyKind kind) : base(id, Nm(kind)){ Kind = kind; Base["maxHp"] = kind.Hp; Base["strength"] = kind.Strength; Base["agility"] = kind.Agility; Base["defense"] = kind.Defense; Base["speed"] = kind.Speed; Hp = kind.Hp; State = kind.Mood; Calm = 0; LostTurns = 0; Phase = 1; Dead = false; Summoned = false; Noticed = false;}public override bool IsImmune(string effectId){ return 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.
var all = new List<Creature> { new Hero("bárbaro", 48, 7, 4, 3) };foreach (Kind kind in Data.Kinds.Values) all.Add(new Enemy(kind));
foreach (Creature c in all){ string 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.WriteLine(line);}
Console.WriteLine("Debilidad para todos:");foreach (Creature c in all){ int before = c.Stat("strength"); c.AddEffect("weak"); Console.WriteLine(c.Name + ": fuerza " + before + " -> " + c.Stat("strength"));}bárbaro: vida 48, fuerza 7, defensa 3, esquiva 8rata gigante: vida 9, fuerza 1, defensa 0, esquiva 12goblin lancero: vida 16, fuerza 2, defensa 1, esquiva 10esqueleto: vida 28, fuerza 4, defensa 2, esquiva 8, inmune a la hemorragia y al venenogólem de mena: vida 46, fuerza 6, defensa 4, esquiva 2, inmune a la hemorragia y al venenoDebilidad para todos:bárbaro: fuerza 7 -> 3rata gigante: fuerza 1 -> 0goblin lancero: fuerza 2 -> 0esqueleto: fuerza 4 -> 0gólem de mena: fuerza 6 -> 2La 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.public int EffectBonus(string stat){ int total = 0; foreach (Effect effect in Effects) total += effect.Mods.GetValueOrDefault(stat, 0); return total;}
// El valor en bruto (puede ser negativo): base + equipo + efectos.public int RawStat(string stat) => Base[stat] + GearBonus(stat) + EffectBonus(stat);
// Un atributo nunca baja de cero.public int Stat(string stat){ int value = 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:
public void AddEffect(string id){ foreach (Effect effect in Effects) { if (effect.Id == id) return; } Effects.Add(Data.Effects[id]);}
// Los efectos se suman: dos efectos sobre un atributo se acumulan.public int EffectBonus(string stat){ int total = 0; foreach (Effect effect in Effects) total += effect.Mods.GetValueOrDefault(stat, 0); return total;}
// El valor en bruto (puede ser negativo): base + equipo + efectos.public int RawStat(string stat) => Base[stat] + GearBonus(stat) + EffectBonus(stat);
// Un atributo nunca baja de cero.public int Stat(string stat){ int value = RawStat(stat); return value < 0 ? 0 : value;}
public override int GearBonus(string stat){ int total = 0; foreach (string slot in Data.Slots) { string id = Equipment[slot]; if (id == "") continue; total += Data.Items[id].Mods.GetValueOrDefault(stat, 0); } return total;}
// La vida nunca supera el nuevo máximo.public void Equip(string id){ Equipment[Data.Items[id].Slot] = id; if (Hp > MaxHp()) Hp = MaxHp();}Paso a paso, en palabras
- Solo se guarda la base: fuerza 7, destreza 4, defensa 3. La esquiva es derivada: destreza × 2 = 8.
- 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.
- El anillo va en otra ranura y no echa la cota: las ranuras se suman. Fuerza 7 + 2 = 9.
- Furia: +3 de fuerza, −2 de defensa. effectBonus suma los efectos: fuerza 12, defensa 5. Al terminar, la capa desaparece.
- El total se calcula de nuevo: stat() = max(0, base + equipo + efectos). No se guarda, así que no puede desincronizarse.
var hero = new Hero("bárbaro", 48, 7, 4, 3);Console.WriteLine("-- Al principio --");Report(hero);hero.Equip("chain");hero.Equip("ring_strength");Console.WriteLine("-- Con la cota de malla y el anillo de fuerza --");Report(hero);hero.AddEffect("rage");Console.WriteLine("-- Y con furia --");Report(hero);-- 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: 3Mira 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.public int Stat(string stat){ int value = RawStat(stat); return value < 0 ? 0 : value;}
public int MaxHp(){ int value = 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).public int Evasion() => Stat("agility") * 2 + 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;Un atributo no baja de cero: si la suma es negativa, stat devuelve 0. El número en bruto se conserva para la esquiva, para que no se pierda que la cota pesa.
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.public int RawStat(string stat){ int value = Base[stat] + GearBonus(stat); foreach (Effect effect in Effects) value += effect.Modifier(stat); return value;}
public int Stat(string stat){ int value = RawStat(stat); return value < 0 ? 0 : value;}
public int MaxHp(){ int value = Stat("maxHp"); return value < 1 ? 1 : value;}
public int MaxMana(){ return Stat("maxMana");}
// Con menos del 25 % de vida la herida es grave: los golpes se vuelven menos certeros.public bool IsHeavilyWounded(){ return Hp * 4 < MaxHp();}
public int Accuracy(){ return 75 + Stat("agility") * 2 + RawStat("accuracy") - (IsHeavilyWounded() ? 15 : 0);}
public int Evasion(){ return Stat("agility") * 2 + RawStat("evasion");}
public int CritChance(){ return 5 + 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:
public override int GearBonus(string stat){ int total = 0; foreach (string slot in Data.Slots) { string id = Equipment[slot]; if (id == "") continue; total += Data.Items[id].Mods.GetValueOrDefault(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.public void Equip(string id){ Equipment[Data.Items[id].Slot] = id; if (Hp > MaxHp()) Hp = MaxHp();}En el canon es igual (gearBonus recorre las ranuras y suma los modificadores):
public override int GearBonus(string stat){ int total = 0; foreach (string slot in Equipment.Keys) { string itemId = Equipment.Get(slot); if (itemId == "") continue; foreach (Mod mod in ITEMS[itemId].Mods) { if (mod.Stat == stat) total += mod.Value; } } return total;}
public Item Weapon(){ string id = Equipment.Get("weapon"); return ITEMS[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?
var hero = new Hero("bárbaro", 48, 7, 4, 3);hero.Equip("ring_life");hero.Hp = hero.MaxHp();hero.AddEffect("weak");hero.Unequip("ring");Console.WriteLine(hero.Hp + "/" + hero.MaxHp());Console.WriteLine(hero.Stat("strength"));El máximo baja de 58 a 48, y unequip devuelve la vida al límite: 48 / 48. La fuerza es 7 − 4 = 3: el efecto es una capa que no depende del anillo. La opción 58 / 48 olvida devolver la vida, 7 olvida el efecto, y 58 / 58 olvida ambas cosas.
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:
var hero = new Hero("bárbaro", 48, 7, 4, 3);hero.AddEffect("rage");hero.AddEffect("shield");Report(hero);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: 8La 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.
En effectBonus la asignación = conserva solo el último efecto: cada uno nuevo borra el anterior. El escudo no tiene bonus de fuerza y la pone a cero, y en la defensa solo queda el +3 del escudo sin el −2 de la furia. Debe ser +=: la capa de «efectos» es la suma de todos los efectos, no el último.
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:
> equip chain48/48 | fuerza 7 | defensa 7 | destreza 4 | esquiva 3> equip amulet_bear48/54 | fuerza 8 | defensa 7 | destreza 3 | esquiva 1> heal54/54 | fuerza 8 | defensa 7 | destreza 3 | esquiva 1> buff rage54/54 | fuerza 11 | defensa 5 | destreza 3 | esquiva 1> remove amulet48/48 | fuerza 10 | defensa 5 | destreza 4 | esquiva 3> remove armor48/48 | fuerza 10 | defensa 1 | destreza 4 | esquiva 8Fí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
ring1yring2. ¿Qué pasa conring_strengthsi 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 hacerstat()cuando termine?
Test
Compruébate
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Polimorfismo · Listas · Dictionary y HashSet · record, switch y patrones
- Ejercicios sobre el tema: Las crónicas de Arcanum