Helden, Feinde, Werte
Kurz gesagt: Ein Wesen für alle: Held und Feinde unterscheiden sich durch Daten (Zeilen einer Tabelle) und durch zwei Methoden, und jeder Wert ist eine Summe aus Schichten, Basis + Ausrüstung + Effekte, die bei jeder Anfrage neu berechnet wird.
Problem
Zehn Feinde, drei Klassen, eine Frage
Im Spiel gibt es zehn Arten von Feinden und drei Heldenklassen. Alle haben Leben, Stärke, Geschick und Verteidigung, und ein Treffer fragt bei allen dieselben Zahlen ab. Wie schreibt man so etwas?
Der erste Einfall von jedem, der Vererbung gelernt hat, ist eine Klasse für jede Art: Enemy ist der Elternteil, Goblin und Skeleton sind die Kinder. Bald sieht man den Baum:
Enemy├── Rat├── Goblin│ ├── GoblinSpearman│ └── GoblinShaman├── Undead│ ├── Skeleton immun gegen Blutung und Gift│ └── StoneServant immun gegen Blutung und Gift (schon wieder!)├── Golem immun gegen Blutung und Gift (schon wieder!!)└── ...Das Skelett, der Steindiener und der Golem bluten nicht und lassen sich nicht vergiften, gehören aber nicht zu einer Familie: Eines ist untot, die anderen sind aus Stein. Entweder wird die Immunität in drei Klassen kopiert, oder du brauchst dazwischen eine Klasse „Blutlos“, die die Familie „Untot“ kreuzt. Und eine zweite Eigenschaft (schwach gegen Feuer) wäre eine weitere kreuzende Familie. Ein tiefer Baum ist gut, solange jeder Fall in einen Zweig passt.
Es gibt noch ein Problem: Ein neuer Feind bedeutet eine neue Klasse, neuen kompilierten Code und einen Programmierer. Ein Designer, der einen Goblin mit 30 Leben will, kann ihn nicht allein machen.
In RPG 1 erben die fünf Völker von einem Character. Dort ist die Vererbung am richtigen Platz, weil jedes Volk etwas anderes tut (seine eigene Fähigkeit). Hier unterscheiden sich die Feinde in Zahlen. Anderes Verhalten gehört in eine Methode. Andere Zahlen sind Daten.
Die Idee
Ein Feind ist eine Zeile in einer Tabelle
Die erste Regel des Spiels: Wenn sich zwei Dinge nur in Zahlen unterscheiden, sind sie eine Klasse und zwei Zeilen Daten. Die Feinde dieser Lektion sind vier Textzeilen, die Zellen sind durch | getrennt:
# id|Name|Leben|Stärke|Geschick|Verteidigung|immunrat|Riesenratte|9|1|6|0|-goblin|Speergoblin|16|2|5|1|-skeleton|Skelett|28|4|4|2|bleed,poisongolem|Erzgolem|46|6|1|4|bleed,poisonDas Spiel arbeitet genau so, nur mit zehn Feinden. Hier ist die echte Tabelle aus dem Canon. Die Spalten, die diese Lektion nicht nutzt (Zeichen, Farbe, Schaden, Erfahrung, Gold, Beute, Fähigkeit, Tiefe), sind weggelassen, die Zeilen sind dieselben:
# id|Name|Leben|Stärke|Geschick|Verteidigung|Tempo|resistent|schwach|immun|Stimmungrat|Riesenratte|9|1|6|0|12|-|-|-|wandergoblin|Speergoblin|16|2|5|1|10|-|-|-|wandershaman|Goblin-Schamane|13|1|4|0|9|-|-|-|sleepwolf|Blutwolf|20|3|8|1|14|-|-|-|sleepwolf_leader|Wolfsführer|36|4|9|2|14|-|-|-|sleepskeleton|Skelett|28|4|4|2|10|slash|blunt|bleed,poison|sleepcultist|Kultist|26|3|6|1|11|-|-|-|wandergolem|Erzgolem|46|6|1|4|7|pierce,fire|blunt|bleed,poison|sleepservant|Steindiener|14|2|3|1|9|-|blunt|bleed,poison|sleepwarden|Wächter der Tiefe|120|5|4|3|10|-|-|bleed,poison|guardDer Code liest die Tabelle beim Start und macht für jede Zeile einen Datensatz. Ein neuer Feind ist eine neue Zeile: Keine einzige Codezeile ändert sich. Das nennt man datengetrieben (data-driven): Das Spiel ist eine Maschine, und der Inhalt steht in Tabellen, die ohne Programmierer bearbeitet werden können.
Genauer, für Neugierige
So werden die Tabellen der Lektion (Feinde, Gegenstände und Effekte) gelesen. Der Text ist eine Konstante, und rows zerlegt ihn in Zeilen und Zellen und überspringt leere Zeilen und Notizen, die mit # beginnen:
const STATS = ["maxHp", "strength", "agility", "defense", "evasion"];const SLOTS = ["armor", "ring"];
const ITEMS_TEXT = `# id|Name|Slot|Modifikatorenleather|Lederrüstung|armor|defense:2chain|Kettenhemd|armor|defense:4,evasion:-5ring_strength|Ring der Stärke|ring|strength:2ring_life|Ring des Lebens|ring|maxHp:10`;
const EFFECTS_TEXT = `# id|Name|Modifikatorenrage|Raserei|strength:3,defense:-2shield|Schild|defense:3weak|Schwäche|strength:-4`;
const ENEMIES_TEXT = `# id|Name|Leben|Stärke|Geschick|Verteidigung|immunrat|Riesenratte|9|1|6|0|-goblin|Speergoblin|16|2|5|1|-skeleton|Skelett|28|4|4|2|bleed,poisongolem|Erzgolem|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(",") };}Object.values(KINDS) liefert die Feinde in der Reihenfolge, in der sie hinzugefügt wurden.Die Idee
Ein Wesen für alle
Die Unterschiede zwischen Held und Feinden sind klein. Deshalb gibt es eine Klasse Creature mit allem Gemeinsamen: einem Namen, Leben, einer Basis (eine Zahl für jeden Wert) und einer Liste von Effekten. Hier ist ihr Anfang:
class Creature { constructor(name) { this.name = name; this.base = {}; for (const stat of STATS) this.base[stat] = 0; this.effects = []; this.hp = 0; }Creature hat eine Tabelle von Werten und eine Liste von Effekten; es ist kein Goblin und kein Skelett. Das ist Komposition: Du setzt ein Wesen aus Teilen zusammen, statt es von einem allgemeineren Wesen zu erben. Ein Feind aus der Tabelle füllt die Basis einfach aus seiner Zeile:
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; }
// Immunität sind Daten: eine Liste aus der Tabellenzeile. isImmune(effectId) { return this.kind.immune.includes(effectId); }}Und der Held trägt noch etwas mehr: Ausrüstung.
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] = ""; }Es bleibt eine Ebene Vererbung: Creature → Hero und Enemy. Die beiden Unterklassen ersetzen je eine Methode, weil nur dort das Verhalten verschieden ist: Der Held hat gearBonus (Ausrüstung), der Feind hat isImmune (Immunität aus seiner Zeile). Alles andere, auch alles im Kampfsystem, fragt ein Creature und weiß nicht, wen es fragt.
Im Canon ist es dasselbe. Hier sind der Konstruktor von Enemy und sein 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;}Ein Feind aus der Tabelle kopiert seine Zahlen in base und behält eine Referenz auf seine Zeile (kind). Immunität ist keine Klasse, sondern eine Liste in dieser Zeile.
Genauer, für Neugierige
Die Regel „Komposition statt Vererbung“ sagt nicht „erbe nie“. Sie sagt: Erbe für Verhalten (eine Methode ist anders), setze für den Aufbau zusammen (die Teile sind anders). Hier hält Creature das gemeinsame Gerüst, und die Unterklassen sind zwei dünne. Wären die Helden fünf Völker mit fünf Fähigkeiten wie in RPG 1, wäre Vererbung das richtige Werkzeug.
Im Canon sind auch die Effekte kleine Klassen (PoisonEffect, RageEffect), weil Gift und Brennen jeden Zug etwas tun. Ein Effekt, der nur Zahlen ändert, kann auch eine Tabellenzeile sein, wie in der Lektion.
Sieh es dir an
Alle durch einen Code
Der Held und die vier Feinde kommen in eine Liste von Creature. Die Schleife fragt nicht, wer wer ist: Sie ruft maxHp, stat und evasion auf und hängt bei einem Feind, der gegen Blutung immun ist, eine Notiz an. Danach bekommen alle denselben Effekt.
const everyone = [new Hero("Barbar", 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 + ": Leben " + c.maxHp() + ", Stärke " + c.stat("strength") + ", Verteidigung " + c.stat("defense") + ", Ausweichen " + c.evasion(); if (c.isImmune("bleed")) line += ", immun gegen Blutung und Gift"; console.log(line);}
console.log("Schwäche für alle:");for (const c of everyone) { const before = c.stat("strength"); c.addEffect("weak"); console.log(c.name + ": Stärke " + before + " -> " + c.stat("strength"));}Barbar: Leben 48, Stärke 7, Verteidigung 3, Ausweichen 8Riesenratte: Leben 9, Stärke 1, Verteidigung 0, Ausweichen 12Speergoblin: Leben 16, Stärke 2, Verteidigung 1, Ausweichen 10Skelett: Leben 28, Stärke 4, Verteidigung 2, Ausweichen 8, immun gegen Blutung und GiftErzgolem: Leben 46, Stärke 6, Verteidigung 4, Ausweichen 2, immun gegen Blutung und GiftSchwäche für alle:Barbar: Stärke 7 -> 3Riesenratte: Stärke 1 -> 0Speergoblin: Stärke 2 -> 0Skelett: Stärke 4 -> 0Erzgolem: Stärke 6 -> 2Die Schwäche nimmt jedem 4 von der Stärke. Die Ratte, der Goblin und das Skelett fallen unter null und bleiben bei 0: Sie können mit negativer Stärke nicht zuschlagen. Der Golem hat 6, also behält er 2. Die nächste Karte sagt, warum.
Die Idee
Ein Wert ist eine Summe aus Schichten
Die Stärke des Helden ist nicht eine Zahl. Sie ist eine Summe:
- Basis: womit der Held geboren wird (die 7 Stärke des Barbaren). Sie wird im Wesen gespeichert;
- Ausrüstung: die Boni der Gegenstände, die gerade getragen werden (+2 vom Ring). Sie wird nicht gespeichert, sondern aus den Slots gesammelt;
- Effekte: die Boni und Lasten der Effekte, die gerade wirken (+3 von der Raserei). Sie wird nicht gespeichert, sondern aus der Effektliste gesammelt.
// Effekte addieren sich: Zwei Effekte auf einen Wert werden zusammengezählt.effectBonus(stat) { let total = 0; for (const effect of this.effects) total += effect.mods[stat] ?? 0; return total;}
// Der Rohwert (kann negativ sein): Basis + Ausrüstung + Effekte.rawStat(stat) { return this.base[stat] + this.gearBonus(stat) + this.effectBonus(stat);}
// Ein Wert fällt nie unter null.stat(stat) { const value = this.rawStat(stat); return value < 0 ? 0 : value;}Die Summe wird bei jeder Anfrage berechnet und nirgends gespeichert. So kann sie nicht aus dem Takt geraten. Du nimmst das Hemd ab, und die Verteidigung sinkt, weil gearBonus den Gegenstand nicht findet. Ein Effekt läuft aus, und die Stärke kommt zurück, weil effectBonus den Effekt nicht findet. Nichts „setzt die Zahl zurück“. Würde das Wesen die gesamte Verteidigung speichern, müsstest du daran denken, sie bei jedem Ereignis zu ändern: anlegen, ablegen, ein Effekt beginnt, ein Effekt endet, der Held steigt eine Stufe auf. Eine vergessene Stelle, und die Verteidigung „dreht durch“.
Der Held im Programm ist ein Barbar (48 Leben, 7 Stärke, 4 Geschick, 3 Verteidigung) mit Kettenhemd, Ring der Stärke und Raserei. Jede Zeile ist Basis +Ausrüstung +Effekte = Summe:
addEffect(id) { for (const effect of this.effects) { if (effect.id === id) return; } this.effects.push(EFFECTS[id]);}
// Effekte addieren sich: Zwei Effekte auf einen Wert werden zusammengezählt.effectBonus(stat) { let total = 0; for (const effect of this.effects) total += effect.mods[stat] ?? 0; return total;}
// Der Rohwert (kann negativ sein): Basis + Ausrüstung + Effekte.rawStat(stat) { return this.base[stat] + this.gearBonus(stat) + this.effectBonus(stat);}
// Ein Wert fällt nie unter null.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;}
// Das Leben übersteigt nie das neue Maximum.equip(id) { this.equipment[ITEMS[id].slot] = id; if (this.hp > this.maxHp()) this.hp = this.maxHp();}Schritt für Schritt, in Worten
- Gespeichert ist nur die Basis: Stärke 7, Geschick 4, Verteidigung 3. Ausweichen ist abgeleitet: Geschick × 2 = 8.
- Kettenhemd: Verteidigung 3 + 4 = 7. Der Ausweich-Bonus ist -5, stat() aber 0; die Rohzahl bleibt, das Ausweichen sinkt auf 3.
- Der Ring sitzt in einem anderen Slot und verdrängt das Hemd nicht: Slots werden summiert. Stärke 7 + 2 = 9.
- Raserei: +3 Stärke, −2 Verteidigung. effectBonus zählt die Effekte zusammen: Stärke 12, Verteidigung 5. Endet sie, verschwindet die Schicht.
- Die Summe wird neu berechnet: stat() = max(0, Basis + Ausrüstung + Effekte). Sie wird nicht gespeichert und kann nicht aus dem Takt geraten.
const hero = new Hero("Barbar", 48, 7, 4, 3);console.log("-- Zu Beginn --");report(hero);hero.equip("chain");hero.equip("ring_strength");console.log("-- Mit Kettenhemd und dem Ring der Stärke --");report(hero);hero.addEffect("rage");console.log("-- Und mit Raserei --");report(hero);-- Zu Beginn --Barbar: Leben 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 Ausweichen: 8-- Mit Kettenhemd und dem Ring der Stärke --Barbar: Leben 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 Ausweichen: 3-- Und mit Raserei --Barbar: Leben 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 Ausweichen: 3Sieh dir die letzte Zeile an: evasion: 0 -5 +0 = 0, aber das Ausweichen ist 3. Warum, sagt die nächste Karte.
Die Idee
Die Rohzahl und der Wert
Das Hemd ist schwer: Es nimmt 5 vom Ausweichen. Aber ein Wert mit Minuszeichen ergibt keinen Sinn: Stärke −3 würde einen Treffer in Heilung verwandeln. Deshalb fällt stat nicht unter null:
// Ein Wert fällt nie unter null.stat(stat) { const value = this.rawStat(stat); return value < 0 ? 0 : value;}
maxHp() { const value = this.stat("maxHp"); return value < 1 ? 1 : value;}Das Ausweichen muss aber wissen, dass das Hemd etwas wiegt. Es nutzt nicht stat, sondern die Rohzahl (rawStat), die negativ sein darf:
// Ausweichen kommt aus dem Geschick und dem rohen Bonus (der negativ sein darf).evasion() { return this.stat("agility") * 2 + this.rawStat("evasion");}Mit Geschick 4 ist das Ausweichen 4 × 2 = 8. Das Hemd nimmt 5, aber stat("evasion") ist 0, während das Ausweichen 8 − 5 = 3 ist. Würde das Ausweichen stat("evasion") nutzen, kostete das Hemd nichts.
maxHp hat eine eigene Regel: Es fällt nicht unter 1. Ein Wesen mit 0 maximalem Leben ist tot, sobald es geboren wird, und eine Division durch das maximale Leben (zum Beispiel für einen Balken in der Oberfläche) ginge schief.
return value < 0 ? : value;Ein Wert fällt nicht unter null: Ist die Summe negativ, gibt stat 0 zurück. Die Rohzahl bleibt für das Ausweichen erhalten, damit nicht verloren geht, dass das Hemd schwer ist.
Das Spiel macht dasselbe, mit ein paar weiteren abgeleiteten Zahlen: Treffsicherheit, Chance auf einen kritischen Treffer und „schwer verwundet“ (unter 25 % Leben sind die Treffer ungenauer):
// Der Rohwert (er kann negativ sein): Basis + Ausrüstung + Effekte.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');}
// Unter 25 % Leben ist die Wunde schwer: Treffer werden ungenauer.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);}Daher kommt rawStat im Canon: Es addiert die Basis, gearBonus und jeden Effekt mit effect.modifier(stat). Die Lektion hält effectBonus getrennt, damit du die drei Schichten einzeln siehst.
Die Idee
Der Held trägt, der Feind nicht
Das Basis-Creature.gearBonus gibt 0 zurück, weil ein Feind keine Ausrüstung hat. Der Held ersetzt es:
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;}Der Held speichert nur, welcher Gegenstand in welchem Slot ist (Name → id). Die Zahlen selbst stehen in der Tabelle der Gegenstände. Ein Kettenhemd ist also eine Zeile Daten, keine Kopie in jedem Helden, und das Balancing zu ändern heißt, an einer Stelle eine Zahl zu ändern.
Wenn ein Gegenstand an- oder abgelegt wird, kann sich das maximale Leben ändern. Der Ring des Lebens gibt +10. Legst du ihn ab, während der Held bei 58 von 58 ist, bleibt das aktuelle Leben 58 bei einem Maximum von 48: etwas, das nicht passieren darf. Deshalb holen equip und unequip das Leben in die Grenze zurück:
// Das Leben übersteigt nie das neue Maximum.equip(id) { this.equipment[ITEMS[id].slot] = id; if (this.hp > this.maxHp()) this.hp = this.maxHp();}Im Canon ist es dasselbe (gearBonus geht die Slots durch und summiert die Modifikatoren):
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);}Genauer, für Neugierige
Im Canon hat der Held auch einen dritten Slot, die Waffe, und weapon() gibt Fäuste zurück, wenn der Slot leer ist, damit es keinen Sonderfall „keine Waffe“ gibt. So hat der Held immer etwas zum Zuschlagen. In der Lektion gibt es zwei Slots, damit die Zahlen sichtbar bleiben.
Gegenstände werden über die id (einen String) gehalten, nicht über eine Referenz. Deshalb ist der Spielstand einfach (er speichert „ring_life“, kein Objekt), und derselbe Gegenstand kann an vielen Stellen vorkommen.
Probier es aus
Sage die Ausgabe voraus
Der Held legt den Ring des Lebens an (+10 maximales Leben) und heilt sich vollständig. Dann trifft ihn Schwäche (−4 Stärke), und er legt den Ring ab. Was gibt das Programm aus?
const hero = new Hero("Barbar", 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"));Das Maximum fällt von 58 auf 48, und unequip holt das Leben in die Grenze zurück: 48 / 48. Die Stärke ist 7 − 4 = 3: Der Effekt ist eine Schicht, die nicht vom Ring abhängt. Die Variante 58 / 48 vergisst, das Leben zurückzuholen, 7 vergisst den Effekt, und 58 / 58 vergisst beides.
Probier es aus
Finde den Fehler
Ein Kollege hat eine Zeile „vereinfacht“, und jetzt hat ein Held mit Raserei und Schild seltsame Zahlen. Die beiden Effekte werden nacheinander gesetzt, und das Programm gibt dies aus:
const hero = new Hero("Barbar", 48, 7, 4, 3);hero.addEffect("rage");hero.addEffect("shield");report(hero);Barbar: Leben 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 Ausweichen: 8Die Raserei hilft der Stärke nicht (es gibt kein +3), und die Verteidigung ist 3 + 3, nicht 3 − 2 + 3. Hier sind die drei Methoden, die die Schichten berechnen. Welche Zeile ist schuld?
Wähle die Zeile mit dem Fehler.
In effectBonus behält die Zuweisung = nur den letzten Effekt: Jeder neue löscht den vorigen. Der Schild hat keinen Bonus auf die Stärke und setzt sie zurück, und bei der Verteidigung bleibt nur das +3 des Schilds ohne das −2 der Raserei. Es muss += heißen: Die Schicht „Effekte“ ist die Summe aller Effekte, nicht der letzte.
Genauer
Wann eine Klasse, wann eine Zeile
Wenn im Spiel etwas Neues auftaucht, frage: Unterscheidet es sich in Zahlen oder im Verhalten?
- Ein neuer Feind mit anderem Leben, anderer Stärke und Verteidigung, einer Immunität gegen Feuer, einer anderen Stimmung: eine Zeile in der Tabelle;
- ein neuer Gegenstand mit Modifikatoren: eine Zeile in der Tabelle, und eine neue Art von Slot ist ein neuer Name in der Liste der Slots;
- ein Effekt, der nur Zahlen ändert (Raserei, Schild, Schwäche): eine Zeile in der Tabelle;
- ein Effekt, der jeden Zug etwas tut (Gift, Brennen, Heilung): eine Methode, deshalb ist er im Canon eine kleine Klasse;
- ein Feind, der entscheidet, wie er sich bewegt (der Schamane beschwört, der Wolf flieht): Verhalten, also Code. Im Canon sind das Fähigkeiten und Zustandsautomaten, keine Unterklassen von
Creature.
Eine Klasse (oder eine ersetzte Methode) braucht man, wenn das andere Verhalten nicht in eine Zahl passt. Alles andere wächst mit Zeilen in einer Tabelle.
Übung
Das Amulett
Löse die Aufgabe in deinem Editor.
Amulett. Füge einen dritten Slot amulet und einen Gegenstand amulet_bear („Bärenamulett“: +6 maximales Leben, +1 Stärke, −1 Geschick) nur als neue Zeile in der Gegenstandstabelle und neuen Namen in der Liste der Slots hinzu. Keine neue Klasse. Das Programm liest Befehle aus der Eingabe (einen pro Zeile): equip <Gegenstand>, remove <Slot>, buff <Effekt> und heal (das Leben wird zum Maximum). Nach jedem Befehl schreibt es > Befehl und eine Zeile: Leben/Maximum | Stärke | Verteidigung | Geschick | Ausweichen. Der Held ist derselbe Barbar (48, 7, 4, 3).
Mit der Eingabe equip chain / equip amulet_bear / heal / buff rage / remove amulet / remove armor (je eine pro Zeile) ist die Ausgabe:
> equip chain48/48 | Stärke 7 | Verteidigung 7 | Geschick 4 | Ausweichen 3> equip amulet_bear48/54 | Stärke 8 | Verteidigung 7 | Geschick 3 | Ausweichen 1> heal54/54 | Stärke 8 | Verteidigung 7 | Geschick 3 | Ausweichen 1> buff rage54/54 | Stärke 11 | Verteidigung 5 | Geschick 3 | Ausweichen 1> remove amulet48/48 | Stärke 10 | Verteidigung 5 | Geschick 4 | Ausweichen 3> remove armor48/48 | Stärke 10 | Verteidigung 1 | Geschick 4 | Ausweichen 8Achte auf heal und auf remove amulet: Mit dem Amulett ist das Maximum 54 und heal füllt bis 54 auf, während remove amulet das Leben in die Grenze von 48 zurückholt.
Weitere Aufgaben
- Ein neuer Feind ohne Code. Füge den Blutwolf aus dem Canon (20 Leben, 3 Stärke, 8 Geschick, 1 Verteidigung, keine Immunität) nur als Zeile in die Feind-Tabelle ein. Wie hoch ist sein Ausweichen?
- Ein zweiter Ring. Mache die Slots
ring1undring2. Was passiert mitring_strength, wenn du ihn in beide legst? Ist das richtig? - Ein Effekt mit Dauer. Gib dem Effekt eine Zahl von Zügen und eine Methode
tick(), die sie verringert und den Effekt bei null entfernt. Was mussstat()nach dem Auslaufen tun?
Test
Prüfe dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Polymorphie · Arrays: push, splice und sort · Map und Set · Daten, switch und Destrukturierung
- Aufgaben zum Thema: Die Chroniken von Arkanum