# Mission-Scripting mit Rhai Diese Anleitung beschreibt die geplante Scriptsprache für Missionen und den Level-/Kampagneneditor. Die Sprache ist **Rhai**, weil sie direkt in Rust läuft, WASM-tauglich ist, eine einfache Schreibweise hat und gut sandboxbar ist. Status: Entwurf plus erste Umsetzung. Die Beispiele zeigen die geplante API. Implementiert ist derzeit der Basis-Hook `on_start(m)` in `src/script.rs`, mit `m.log(text)`, `m.objective(text)` und direktem Spawn über `m.spawn_at(side, kind, x, y, count, level)`. `side` akzeptiert Zahlen (`0` bis `4`) oder Namen wie `"PLAYER"`, `"HEROES"`, `"MOSSGRIM"`, `"NIGHTWELL"`, `"RIVAL3"`. Zonen, Flags, Einheiten-Ereignisse, zeitgesteuerte Hooks und vollständige Missionsfelder sind noch nicht aktiv. Der geplante Name `m.spawn` ist in Rhai reserviert; für jetzt ist `m.spawn_at` die aktive Funktion. ## 1. Wofür Missionsscripting da ist Missionsscripting erlaubt Missionen, die nicht nur aus festen Daten bestehen: - Zeitabläufe: nach 300 Sekunden greifen Verstärkungen an. - Reaktionen: wenn der Spieler einen Tresor plündert, erscheint ein Paladin. - Phasen: erst Wellen, dann Festung, dann Boss. - Zufall mit Kontrolle: eine patrouillierende Gruppe nimmt eine von zwei Wegen. - Dynamische Briefings und objectives. - Rivalen, Höhlen, Splitter, Manaquellen und Tore können auf den Spielfortschritt reagieren. - Kampagnenmissionen können später mit eigener Geschichte, eigenen Zielen und eigener Logik bearbeitet werden. Wichtig: Skripte sollen Missionen verändern, aber nicht das ganze Spielprogramm ersetzen. Sie werden daher in einer abgesicherten Umgebung ausgeführt. ## 2. Sprache: Rhai Die Missionsskripte werden in Rhai geschrieben. Beispiel: ```rhai const HERO = 1; const MOSSGRIM = 2; const NIGHTWELL = 3; fn on_start(m) { m.log("Krummfinger hat einen Plan."); m.objective("Finde den Splitter, bevor die Wachen Alarm schlagen."); } ``` Rhai ist absichtlich einfach gehalten: - Variablen mit `let`. - Konstanten mit `const`. - Funktionen mit `fn`. - `if`, `for`, `while`, Booleans, Zahlen und Strings. - Arrays mit `[...]`. - Maps mit `#{...}`. - Keine Dateien, keine Netzwerkzugriffe, kein direkter Zugriff auf Rust-Codespeicher. ## 3. Wo ein Missionsskript liegt Eine Mission besteht künftig aus Daten und optional einem Skript. Beispiel als Missionsdefinition: ```json { "key": "custom01", "title": "Die erste Falle", "story": "Die Wachen glauben, der alte Schacht sei sicher. Sie irren sich.", "size": [96, 80], "seed": 123456, "gold": 2500, "goals": ["Shards", "Survive(3)"], "script": "missions/custom01.rhai" } ``` Im Editor könnte das Skript direkt in einer Textbox oder in einer Datei `missions/custom01.rhai` bearbeitet werden. Der Kampagneneditor kann später diese Struktur vollständig speichern: ```json { "campaign": "Eigene Kampagne", "missions": [ { "key": "custom01", "title": "Die erste Falle", "story": "Kurztext für das Briefing.", "goals": ["Shards"], "script": "missions/custom01.rhai" }, { "key": "custom02", "title": "Die zweite Welle", "story": "Nach dem Splitter kommt der Orden zurück.", "goals": ["Survive(4)"], "script": "missions/custom02.rhai" } ] } ``` ## 4. Das Missionsobjekt `m` Alle Ereignisse bekommen ein Missionsobjekt `m` übergeben. Es stellt die Mission, den Spielstand und die erlaubten Aktionen bereit. ### Lesbare Eigenschaften | Feld | Bedeutung | |---|---| | `m.time` | verstrichene Missionszeit in Sekunden | | `m.map_w` | Kartenbreite in Tiles | | `m.map_h` | Kartenhöhe in Tiles | | `m.gold` | Gold des gerade aktiven Meisters | | `m.mana` | Mana des gerade aktiven Meisters | | `m.loot` | bereits erbeutetes Tresorgold | | `m.shards_taken` | genommene Seelenkernsplitter | | `m.springs` | eingenommene Manaquellen | | `m.freed` | befreite angekettete Kreaturen | | `m.wave` | aktuelle bzw. letzte Nummer der Heldwellen | | `m.time_limit` | Zeitlimit in Sekunden, `0` bedeutet kein Limit | | `m.wave_limit` | Wellenlimit, `0` bedeutet endlos | | `m.gate_hp` | Lebenspunkte des Ordens-Tores | | `m.state` | `0` läuft, `1` Sieg, `2` Niederlage | ### Seiten | Konstante | Bedeutung | |---|---| | `PLAYER` oder `0` | roter Meister, normalerweise der Spieler | | `HEROES` oder `1` | der Orden der Ritter | | `MOSSGRIM` oder `2` | grüner Rivale Mossgrim | | `NIGHTWELL` oder `3` | blauer Rivale Lady Nightwell | | `RIVAL3` oder `4` | dritter Rivale, gelb | Empfohlen, benannte Konstanten zu benutzen: ```rhai const HEROES = 1; const MOSSGRIM = 2; const NIGHTWELL = 3; ``` ### Kreaturen Kreaturennamen als Zeichenketten, passend zu den internen Namen: ```rhai "Worker" "Kobold" "Spider" "Mage" "Ogre" "Knight" "Archer" "Priest" "Dwarf" "Paladin" "Jailer" "Priestess" "Chainbreaker" "Bat" "Gargoyle" "Owl" "Wyrm" "Cogling" "Revenant" "Giant" "Fairy" "BattleMage" ``` Beispiel: ```rhai m.spawn("Knight", 2, m.at(0.72, 0.48), HEROES); ``` ### Ziele Die Missionsziele können als Zielobjekte behandelt werden: ```rhai goal.name goal.count goal.done ``` Namen: ```rhai Shards Heart KillAll Loot Towers Survive Rivals Springs Freed ``` Beispiel: ```rhai if goal.name == "Shards" && goal.done { m.objective("Zweiter Abschnitt: Die Festung."); } ``` ## 5. Erlaubte Aktionen Die erlaubten Funktionen sind bewusst klein und sicher gehalten. ### Text und UI ```rhai m.log("Nachricht im Log."); m.hint("Hinweis für den Spieler."); m.warn("Warnung mit Alarm-Ton."); m.objective("Aktuelles Ziel im Interface."); ``` ### Effekte ```rhai m.play("alarm"); m.flash("danger"); m.flash("treasure"); ``` Geplante Effekt-Namen: ```rhai alarm danger treasure claim level_up heal ``` ### Variablen, Flags und Zähler ```rhai m.set("phase", 1); m.get("phase"); m.has("phase"); m.set_flag("guards_alerted"); m.has_flag("guards_alerted"); m.clear_flag("guards_alerted"); m.bump("vaults_opened"); m.counter("vaults_opened"); ``` Regel: Jede Aktion, die nur einmal passieren soll, wird mit einem Flag oder Zähler geschützt. ### Positionen Koordinaten im Editor werden am besten als Bruchteile der Karte angegeben: ```rhai m.at(0.70, 0.50) ``` Das bedeutet: 70 Prozent der Kartenbreite, 50 Prozent der Kartenhöhe. Der Editor rechnet das automatisch in Tile-Koordinaten um. Weitere geplante Positionshilfen: ```rhai m.map_center() m.fort_pos(0) m.gate_pos() m.core_pos(0) m.cave_pos(0) ``` ### Triggerzonen, markierte Kacheln und Linien Location-Trigger sind benannte Orte, Flächen oder Wege in der Mission. Das Skript kann reagieren, wenn eine Kreatur diese Stelle erreicht, eine Linie überquert, in einer Zone spawnen soll oder Zerstörungen in der Zone auslöst. Zonen können direkt im Skript definiert oder grafisch im Level-/Kampagneneditor festgelegt werden. Wenn sie im Editor gezeichnet werden, übernimmt der Editor sie automatisch in die Mission und stellt sie dem Skript bereit. #### Zonen im Skript definieren ```rhai fn on_start(m) { m.add_zone("north_gate", m.at(0.70, 0.45), 2.0); m.add_zone("south_gate", m.at(0.70, 0.55), 2.0); m.add_zone("vault_approach", m.at(0.82, 0.50), 3.0); } ``` Flächen aus markierten Kacheln: ```rhai fn on_start(m) { m.add_tiles_zone("vault_path", [ [34, 40], [34, 41], [35, 40] ]); } ``` Rechtecke und Linien: ```rhai fn on_start(m) { m.add_rect_zone("guard_room", m.at(0.70, 0.40), m.at(0.80, 0.60)); m.add_line("bridge_path", [m.at(0.60, 0.50), m.at(0.72, 0.50)], 1.0); } ``` #### Zonen grafisch im Editor festlegen Der Level-/Kampagneneditor kann Zonen grafisch setzen: - Kreis: Zentrum und Radius ziehen. - Rechteck: zwei Ecken ziehen. - Polygon: Punkte anklicken. - Markierte Kacheln: einzelne Tiles bemalen oder eine Fläche malen. - Linie: Brücke, Weg, Patrouillenpfad oder Grenzlinie ziehen. - Optionaler Filter: Seite, Kreaturentyp, einmalig, nur Spielerkreaturen, nur Gegnerkreaturen. Die grafisch gesetzten Zonen werden in den Missionsdaten gespeichert und automatisch für das Skript verfügbar gemacht. Das Skript muss sie nicht von Hand definieren. Es kann aber jederzeit zusätzliche Zonen ergänzen. Beispielhafter Skriptaufruf: ```rhai fn on_start(m) { m.load_editor_zones(); } ``` Nach `m.load_editor_zones()` stehen alle im Editor gezeichneten Zonen im Skript zur Verfügung. Wer zusätzliche Trigger braucht, kann sie danach ergänzen: ```rhai fn on_start(m) { m.load_editor_zones(); m.add_zone("backup_path", m.at(0.58, 0.50), 2.0); } ``` Der Editor kann den generierten Anteil des Skripts auch sichtbar machen: ```rhai // --- editor-generated zones: normally not edited manually --- fn editor_zones(m) { m.add_zone("north_gate", m.at(0.70, 0.50), 3.0); m.add_tiles_zone("vault_path", [[34, 40], [34, 41], [35, 40]]); } ``` Dann ruft das Missionsskript einfach `editor_zones(m)` auf. Zonen können auch direkt in der Missionsdefinition stehen: ```json "zones": [ { "name": "north_gate", "shape": "circle", "at": [0.70, 0.45], "radius": 2.0 }, { "name": "vault_approach", "shape": "circle", "at": [0.82, 0.50], "radius": 3.0 }, { "name": "vault_path", "shape": "tiles", "tiles": [[34, 40], [34, 41], [35, 40]] }, { "name": "bridge_path", "shape": "line", "from": [0.60, 0.50], "to": [0.72, 0.50], "width": 1.0 } ] ``` #### Zonenaktionen im Skript Sobald eine Zone definiert ist, kann das Skript in dieser Zone Aktionen auslösen. Direkt umgesetzt sind bereits `m.spawn_at` und `m.spawn_count_in_zone`; `m.spawn_in_zone` folgt später: ```rhai m.spawn_in_zone("north_gate", "Knight", 3, HEROES); m.spawn_count_in_zone("north_gate", "Knight", 3, HEROES, 2); m.spawn_count_in_zone("north_gate", "Archer", 3, HEROES, 1); m.destroy_walls_in_zone("north_gate"); m.destroy_walls_at(m.at(0.72, 0.50), 2.0); m.destroy_doors_in_zone("vault_path"); m.destroy_towers_in_zone("north_gate"); m.log("Die Zone ist offen."); m.warn("Die Ritter kommen durch die Öffnung!"); ``` Beispiel: Eine Kreatur erreicht eine grafisch gesetzte Zone, die Wand fällt, und dahinter spawnen Gegner. ```rhai const HEROES = 1; const PLAYER = 0; fn on_start(m) { m.load_editor_zones(); m.log("Eine markierte Zone ist im Editor gesetzt worden."); } fn on_unit_reaches(m, unit, zone) { if zone != "north_gate" { return; } if unit.side != PLAYER { return; } if m.has_flag("north_gate_opened") { return; } m.set_flag("north_gate_opened"); m.log("Eine Kreatur hat das Nordtor erreicht."); m.warn("Hinter der Wand warten Ritter!"); m.destroy_walls_in_zone("north_gate"); m.spawn_count_in_zone("north_gate", "Knight", 3, HEROES, 2); m.spawn("Archer", 3, m.fort_pos(0), HEROES); } ``` Diese Funktionen dürfen nicht willkürlich beliebige Tiles bearbeiten. Sie arbeiten nur über die Missions-API und sind deterministisch. ### Kreaturen und Wellen erscheinen lassen Geplant: ```rhai m.spawn(kind, level, at, side); ``` Umgesetzt ist für jetzt, weil `spawn` in Rhai reserviert ist: ```rhai m.spawn_at(side, kind, x, y, count, level); m.spawn_count_in_zone(zone, kind, count, side, level); ``` Beispiel: ```rhai m.spawn_at("HEROES", "Knight", 21.5, 21.5, 3, 2); m.spawn_count_in_zone("north_gate", "Knight", 3, "HEROES", 2); ``` ```rhai m.spawn("Knight", 2, m.at(0.72, 0.48), HEROES); ``` Gruppen über mehrere Aufrufe: ```rhai m.spawn("Knight", 2, m.at(0.72, 0.45), HEROES); m.spawn("Archer", 2, m.at(0.74, 0.55), HEROES); ``` Wellen: Umgesetzt ist für jetzt nur `m.spawn_wave(count)`. Es setzt die Missionswellen auf `true`, erhöht das Wellenlimit um die gewünschte Anzahl und startet die nächste Welle sofort: ```rhai m.spawn_wave(6); ``` Geplant, aber noch nicht umgesetzt: ```rhai m.set_wave_limit(8); m.set_first_wave(180); ``` Ziele und Limits ändern: ```rhai m.set_time_limit(1200); m.add_time_limit(300); m.set_wave_limit(10); ``` ### Zufall Nur erlaubte Zufallsfunktionen benutzen: ```rhai m.rand_int(1, 10) m.rand_choice(["north", "south"]) ``` Beispiel: ```rhai let path = m.rand_choice(["north", "south"]); m.set("patrol_path", path); ``` ## 6. Ereignis-Hooks Ein Missionsskript kann verschiedene Ereignisse definieren. Nicht jedes Ereignis muss vorkommen. | Hook | Wann | |---|---| | `on_start(m)` | Missionsstart | | `on_time(m)` | regelmäßig während der Mission, empfohlen: 1x pro Sekunde | | `on_turn(m, turn)` | in Matches, wenn eine Server-Runde ankommt | | `on_wave_start(m)` | vor einer neuen Welle | | `on_wave(m, wave)` | nach dem Start einer Welle | | `on_vault_claimed(m, vault_index, value)` | wenn ein Rittertresor erobert wurde | | `on_shard_taken(m, count)` | wenn ein Splitter genommen wurde | | `on_tower_destroyed(m, kind)` | wenn ein Turm zerstört wurde | | `on_fort_alarm(m, fort_index)` | wenn eine Festung Alarm schlägt | | `on_rival_attacked(m, side)` | wenn ein Rivale direkt angegriffen wird | | `on_rival_dead(m, side)` | wenn ein Rivalenkern zerstört ist | | `on_cave_claimed(m, cave_index, chained)` | wenn eine Höhle eingenommen wird | | `on_freed(m, kind, cave_index)` | wenn eine angekettete Kreatur befreit wird | | `on_gate_damage(m, damage)` | wenn das Ordenstor getroffen wird | | `on_unit_reaches(m, unit, zone)` | wenn eine Kreatur eine Triggerzone oder ein Ziel erreicht | | `on_unit_crosses(m, unit, line)` | wenn eine Kreatur eine Triggerlinie überquert | | `on_branch(m, branch)` | wenn ein Verzweigungsziel erreicht wurde und die nächste Mission feststeht | | `on_goal(m, goal)` | wenn sich ein Missionsziel deutlich ändert oder erreicht wird | | `on_end(m, won)` | Missionsende | ### Das `unit`-Objekt in Location-Triggern In `on_unit_reaches` und `on_unit_crosses` bekommt das Skript ein kleines `unit`-Objekt: ```rhai unit.id unit.kind unit.level unit.side unit.alive unit.x unit.y ``` Beispiel: ```rhai if unit.kind == "Ogre" && unit.side == PLAYER { m.log("Ein Oger hat die Zone erreicht."); } ``` Die Zone `zone` ist der Name der grafisch gesetzten oder im Skript definierten Zone: ```rhai if zone == "north_gate" { // nur auf diese Zone reagieren } ``` Triggerzonen sollten immer mit Flags oder Zählern abgesichert werden, damit dieselbe Aktion nicht mehrfach ausgelöst wird. ### Missionsverzweigungen im Skript Wenn eine Mission Verzweigungsziele hat, kann das Skript auf eine erreichte Branch reagieren: ```rhai fn on_branch(m, branch) { m.log("Zwei Wege: " + branch.id); m.objective("Die Story führt weiter auf dem Weg " + branch.id); } ``` Ein Branch-Objekt kann später diese Felder haben: ```rhai branch.id branch.title branch.goal branch.next ``` Beispiel für zwei alternative Wege: ```rhai fn on_branch(m, branch) { if branch.goal == "KillAll" { m.log("Die Wachstube ist zerstört."); m.objective("Der Weg führt zu den Patrouillenpfaden."); } if branch.goal == "Loot" { m.log("Die Tresore sind geplündert."); m.objective("Der Weg führt zu den Goldkammern."); } } ``` Weitere Informationen dazu stehen in `docs/CAMPAIGN_EDITOR.md`. ## 7. Grundregel: Alles muss wiederholbar sein Ereignisse können mehrfach ausgelöst werden. Ein Skript soll deshalb idiomatisch Flags oder Zähler benutzen. Beispiel: Ein Alarm soll genau einmal ausgelöst werden. ```rhai fn on_fort_alarm(m, fort_index) { let key = "alarm_fort_" + fort_index.to_string(); if !m.has_flag(key) { m.set_flag(key); m.log("Die Wache hat Alarm geschlagen!"); m.play("alarm"); } } ``` Hilfsmuster für einmalige Aktionen: ```rhai fn once(m, key, action) { if !m.has_flag(key) { m.set_flag(key); action(); } } ``` Verwendung: ```rhai fn on_start(m) { once(m, "intro", || { m.log("Krummfinger: Wir haben Gäste."); m.objective("Erkunde den Schacht."); }); } ``` ## 8. Beispiel 1: Minimales Startskript ```rhai const HEROES = 1; fn on_start(m) { m.log("Krummfinger: Ein kleiner Posten, aber vielleicht ein Splitter."); m.objective("Finde den Splitter."); } fn on_goal(m, goal) { if goal.name == "Shards" && goal.done { m.log("Splitter gesichert."); m.objective("Der Weg zur Zitadelle ist offen."); } } ``` ## 9. Beispiel 2: Zeitgesteuerte Verstärkung Diese Mission lässt nach fünf Minuten Ritter aus dem Osten erscheinen. Nach zehn Minuten kommt eine zweite Gruppe. ```rhai const HEROES = 1; fn on_time(m) { if m.time >= 300 && !m.has_flag("wave_5min") { m.set_flag("wave_5min"); m.log("Verstärkung aus dem Osten."); m.spawn("Knight", 2, m.at(0.70, 0.48), HEROES); m.spawn("Archer", 2, m.at(0.70, 0.55), HEROES); } if m.time >= 600 && !m.has_flag("wave_10min") { m.set_flag("wave_10min"); m.log("Jetzt kommen sie ernsthaft."); m.spawn("Knight", 3, m.at(0.74, 0.45), HEROES); m.spawn("Knight", 3, m.at(0.74, 0.60), HEROES); m.warn("Zweite Welle!"); } } ``` ## 10. Beispiel 3: Drei Phasen einer Mission Diese Mission hat drei Abschnitte. Jeder Abschnitt wird genau einmal ausgelöst. ```rhai const HEROES = 1; fn phase_name(m, phase) { if phase == 1 { return "Erste Phase"; } if phase == 2 { return "Zweite Phase"; } return "Dritte Phase"; } fn on_time(m) { let phase = 0; if m.time >= 900 { phase = 3; } else if m.time >= 600 { phase = 2; } else if m.time >= 300 { phase = 1; } let key = "phase_" + phase.to_string(); if phase > 0 && !m.has_flag(key) { m.set_flag(key); m.log(phase_name(m, phase)); if phase == 1 { m.spawn_wave(3); m.objective("Halte die erste kleine Welle."); } if phase == 2 { m.spawn_wave(5); m.spawn("Paladin", 2, m.at(0.80, 0.50), HEROES); m.objective("Schlage die Verstärkung zurück."); } if phase == 3 { m.spawn_wave(8); m.warn("Der Orden kommt mit allem, was er hat."); m.objective("Überlebe den letzten Ansturm."); } } } ``` ## 11. Beispiel 4: Tresor löst Boss aus Wenn der Spieler genug Tresore plündert, erscheint ein starker Gegner. ```rhai const HEROES = 1; fn on_vault_claimed(m, vault_index, value) { m.log("Tresor " + vault_index.to_string() + " ist offen: " + value.to_string()); m.bump("opened_vaults"); if m.counter("opened_vaults") == 2 { m.set_flag("second_vault_alarm"); m.log("Der Wächter wird gerufen."); m.spawn("Paladin", 4, m.at(0.82, 0.50), HEROES); m.warn("Ein Paladin nähert sich."); } if m.counter("opened_vaults") >= 4 { m.log("Der Orden schickt alles, was er übrig hat."); m.spawn_wave(6); } } ``` ## 12. Beispiel 5: Bosskampf mit mehreren Phasen Ein Paladin wird nach und nach von weiteren Rittern unterstützt. ```rhai const HEROES = 1; fn boss_phase(m, phase) { if phase == 1 { m.spawn("Knight", 3, m.at(0.76, 0.42), HEROES); m.spawn("Knight", 3, m.at(0.76, 0.58), HEROES); m.log("Der Paladin wird von Rittern begleitet."); } if phase == 2 { m.spawn("BattleMage", 3, m.at(0.82, 0.50), HEROES); m.warn("Ein Schlachtenmagier betritt das Feld."); } if phase == 3 { m.spawn("Giant", 3, m.at(0.78, 0.50), HEROES); m.log("Ein Riese zertrümmert die ersten Fallen."); } } fn on_tower_destroyed(m, kind) { let key = "tower_" + kind.to_string(); if !m.has_flag(key) { m.set_flag(key); m.bump("towers_destroyed"); } let destroyed = m.counter("towers_destroyed"); if destroyed >= 1 && !m.has_flag("boss_1") { m.set_flag("boss_1"); boss_phase(m, 1); } if destroyed >= 3 && !m.has_flag("boss_2") { m.set_flag("boss_2"); boss_phase(m, 2); } if destroyed >= 5 && !m.has_flag("boss_3") { m.set_flag("boss_3"); boss_phase(m, 3); } } ``` ## 13. Beispiel 6: Rivalen reagieren lassen Ein Rivale greift zurück, sobald der Spieler zu aggressiv wird. ```rhai const MOSSGRIM = 2; const NIGHTWELL = 3; fn on_rival_attacked(m, side) { m.bump("rival_pressure"); if m.counter("rival_pressure") >= 3 && !m.has_flag("mossgrim_retaliation") { m.set_flag("mossgrim_retaliation"); m.log("Mossgrim spürt deinen Angriff."); m.spawn_wave(6); } if m.counter("rival_pressure") >= 6 && !m.has_flag("nightwell_retaliation") { m.set_flag("nightwell_retaliation"); m.log("Lady Nightwell mischt sich ein."); m.spawn("Owl", 3, m.at(0.58, 0.40), NIGHTWELL); m.spawn("Owl", 3, m.at(0.58, 0.60), NIGHTWELL); } } fn on_rival_dead(m, side) { if side == MOSSGRIM { m.log("Mossgrims Kern ist zerbrochen."); } if side == NIGHTWELL { m.log("Lady Nightwells Kern ist zerbrochen."); } } ``` ## 14. Beispiel 7: Befreite Kreaturen lösen Wächter aus Wenn eine angekettete Kreatur befreit wird, kommen Wachen. ```rhai const HEROES = 1; fn on_freed(m, kind, cave_index) { let key = "cave_freed_" + cave_index.to_string(); if !m.has_flag(key) { m.set_flag(key); m.log("Die Ketten sind gebrochen."); if kind == "Ogre" { m.spawn("Knight", 2, m.cave_pos(cave_index), HEROES); m.spawn("Knight", 2, m.fort_pos(0), HEROES); m.warn("Die Ritter haben die Befreiung bemerkt."); } if kind == "Wyrm" { m.spawn("Knight", 3, m.fort_pos(0), HEROES); m.spawn("Priest", 3, m.fort_pos(0), HEROES); m.log("Ein Lindwurm ist frei - und die Wachen kommen trotzdem."); } } } ``` ## 15. Beispiel 8: Zufall mit sicherer Wiederholung Ein Patrouillenweg wird zufällig gewählt, aber die Mission bleibt reproduzierbar, weil `m.rand_choice` benutzt wird. ```rhai const HEROES = 1; fn on_start(m) { let path = m.rand_choice(["north", "south"]); m.set("patrol_path", path); m.log("Die Patrouille wählt den Weg: " + path.to_string()); } fn on_time(m) { if m.time < 300 || m.has_flag("patrol_arrived") { return; } let path = m.get("patrol_path"); if path == "north" { m.spawn("Knight", 2, m.at(0.70, 0.30), HEROES); m.spawn("Archer", 2, m.at(0.72, 0.36), HEROES); } if path == "south" { m.spawn("Knight", 2, m.at(0.70, 0.70), HEROES); m.spawn("Archer", 2, m.at(0.72, 0.64), HEROES); } m.set_flag("patrol_arrived"); } ``` ## 16. Beispiel 9: Zeitlimitwarnungen Eine Mission mit Zeitlimit warnt kurz vor Ablauf. ```rhai fn on_time(m) { if m.time_limit <= 0 { return; } let left = m.time_limit - m.time; if left <= 300 && !m.has_flag("warn_5min") { m.set_flag("warn_5min"); m.warn("Noch fünf Minuten."); } if left <= 60 && !m.has_flag("warn_last_minute") { m.set_flag("warn_last_minute"); m.warn("Die letzte Minute läuft."); } } fn on_end(m, won) { if !won { m.log("Die Zeit ist abgelaufen."); } } ``` ## 17. Beispiel 10: Dynamische Briefing-Texte Ein Skript kann objectives ändern, ohne die Missionsdatei neu zu starten. ```rhai fn on_start(m) { m.objective("Erkunde den Schacht."); } fn on_goal(m, goal) { if goal.name == "Shards" && !goal.done { m.objective("Splitter gefunden, aber noch nicht gesichert."); } if goal.name == "Shards" && goal.done { m.objective("Sichere den Weg zurück zum Kern."); } if goal.name == "Survive" && goal.done { m.objective("Die Wellen sind geschlagen."); } } ``` ### Beispiel 11: Grafisch gezeichnete Zone: Wand fällt, Gegner erscheinen Im Leveleditor wird ein Kreis namens `north_gate` gezeichnet: ```json "zones": [ { "name": "north_gate", "shape": "circle", "at": [0.70, 0.50], "radius": 3.0 } ] ``` Das Missionsskript lädt die Editorzonen automatisch: ```rhai const HEROES = 1; const PLAYER = 0; fn on_start(m) { m.load_editor_zones(); m.objective("Such den Weg in die Festung."); } fn on_unit_reaches(m, unit, zone) { if zone != "north_gate" { return; } if unit.side != PLAYER { return; } if m.has_flag("north_gate_opened") { return; } m.set_flag("north_gate_opened"); m.log("Eine Kreatur hat das Nordtor erreicht."); m.warn("Die Wand bricht!"); m.destroy_walls_in_zone("north_gate"); m.spawn_count_in_zone("north_gate", "Knight", 3, HEROES, 2); m.spawn("Archer", 3, m.fort_pos(0), HEROES); } ``` Wenn der Editor die Zonen automatisch in das Skript schreibt, kann `on_start(m)` auch einfach so aussehen: ```rhai fn on_start(m) { editor_zones(m); m.objective("Such den Weg in die Festung."); } ``` Der Editor-generierte Abschnitt wäre dann: ```rhai // --- editor-generated zones: normally not edited manually --- fn editor_zones(m) { m.add_zone("north_gate", m.at(0.70, 0.50), 3.0); } ``` ### Beispiel 12: Markierte Kacheln als Zone Im Leveleditor werden mehrere Kacheln markiert, zum Beispiel ein Tresorweg: ```json "zones": [ { "name": "vault_path", "shape": "tiles", "tiles": [[34, 40], [34, 41], [35, 40], [35, 41]] } ] ``` Das Skript reagiert, sobald eine Spielerkreatur diese markierten Kacheln betritt: ```rhai const HEROES = 1; const PLAYER = 0; fn on_start(m) { m.load_editor_zones(); m.log("Krummfinger hat einen Weg markiert."); } fn on_unit_reaches(m, unit, zone) { if zone != "vault_path" { return; } if unit.side != PLAYER { return; } if m.has_flag("vault_alarm") { return; } m.set_flag("vault_alarm"); m.log("Auf dem markierten Weg ist jemand eingedrungen."); m.warn("Tresoralarm!"); m.destroy_doors_in_zone("vault_path"); m.spawn_count_in_zone("vault_path", "Knight", 2, HEROES, 3); } ``` Wenn die Zone nicht im Editor gesetzt wird, kann dieselbe Zone auch im Skript definiert werden: ```rhai fn on_start(m) { m.add_tiles_zone("vault_path", [ [34, 40], [34, 41], [35, 40], [35, 41] ]); } ``` ### Beispiel 13: Triggerlinie - Kreatur überquert eine Brücke Im Editor wird eine Linie über eine Brücke gezeichnet: ```json "zones": [ { "name": "bridge_path", "shape": "line", "from": [0.60, 0.50], "to": [0.72, 0.50], "width": 1.0 } ] ``` Das Skript lässt Hinterhalte erscheinen, wenn eine Kreatur die Brücke überquert: ```rhai const HEROES = 1; const PLAYER = 0; fn on_start(m) { m.load_editor_zones(); m.objective("Die Brücke ist nicht sicher."); } fn on_unit_crosses(m, unit, line) { if line != "bridge_path" { return; } if unit.side != PLAYER { return; } if m.counter("bridge_ambushes") >= 2 { return; } m.bump("bridge_ambushes"); m.log("Die Brücke ist beobachtet."); m.warn("Hinterhalt!"); m.spawn("Knight", 2, m.at(0.72, 0.45), HEROES); m.spawn("Archer", 2, m.at(0.74, 0.55), HEROES); } ``` ## 18. Missionsdefinition mit Skript und Briefing Ein vollständiges Beispiel: ```json { "key": "custom01", "title": "Die erste Falle", "story": "Die Wachen glauben, der alte Schacht sei sicher. Sie irren sich. Krummfinger hat eine Falle gebaut, aber sie braucht einen Köder.", "hints": [ "Türme laufen nicht von allein weg.", "Ritterboden lässt sich nur erobern, wenn keine Ritter daneben stehen." ], "size": [96, 80], "seed": 123456, "gold": 2500, "goals": ["Shards", "Survive(3)"], "zones": [ { "name": "north_gate", "shape": "circle", "at": [0.70, 0.50], "radius": 3.0 }, { "name": "vault_path", "shape": "tiles", "tiles": [[34, 40], [34, 41], [35, 40], [35, 41]] } ], "script": "missions/custom01.rhai" } ``` Dazu `missions/custom01.rhai`: ```rhai const HEROES = 1; fn on_start(m) { m.log("Krummfinger: Wir haben Gäste."); m.objective("Finde den Splitter, bevor sie merken, dass die Falle offen steht."); } fn on_goal(m, goal) { if goal.name == "Shards" && goal.done { m.log("Splitter gefunden."); m.objective("Halte drei kurze Wellen aus."); m.spawn_wave(1); } } fn on_wave(m, wave) { if wave == 2 { m.spawn("Knight", 2, m.at(0.70, 0.40), HEROES); } if wave == 3 { m.spawn("Knight", 2, m.at(0.70, 0.48), HEROES); m.spawn("Knight", 2, m.at(0.70, 0.56), HEROES); m.warn("Letzte Welle!"); } } ``` ## 19. Regeln für Multiplayer und Determinismus Die Simulation des Spiels ist in Matches deterministisch. Skripte dürfen diese Deterministik nicht zerstören. Daher gilt: 1. Keine lokalen Zufallsentscheidungen, die nur auf einem Client passieren. 2. Nur `m.rand_int`, `m.rand_choice` oder andere klar bestimmte Zufallsfunktionen der Missions-API benutzen. 3. Keine Zeitereignisse, die von FPS, Kamera, Maus, Tastatur oder Grafikabhängigkeit leben. 4. `on_time` soll nur Missionslogik ausführen, keine grafischen Effekte direkt auf den Zustand schreiben. 5. Änderungen an Kreaturen, Wellen und Zielen nur über die freigegebene Missions-API. 6. Für Matches können Skripte später so laufen, dass alle Clients dieselbe Mission und denselben Seed haben. ## 20. Empfohlene Schreibweise ### Kurze Skripte bevorzugen Gut: ```rhai fn on_time(m) { if m.time >= 300 && !m.has_flag("second_wave") { m.set_flag("second_wave"); m.spawn_wave(4); } } ``` Schlecht: ```rhai fn on_time(m) { for i in range(1000000) { // zu langsam und in der Simulation gefährlich } } ``` ### Flags nach Missionsphase benennen Gut: ```rhai m.set_flag("phase2_started"); m.has_flag("phase2_started"); ``` Weniger gut: ```rhai m.set_flag("flag"); ``` ### Zähler für wiederholte Bedingungen Gut: ```rhai m.bump("vaults_opened"); if m.counter("vaults_opened") >= 3 { m.spawn_wave(4); } ``` ### Klare Namen für Kreaturen Gut: ```rhai m.spawn("Knight", 2, m.at(0.70, 0.50), HEROES); ``` Weniger gut: ```rhai m.spawn(13, 2, m.at(0.70, 0.50), 1); ``` Zahlen sind kurz, aber schlechter lesbar. ## 21. Komplettes Muster-Skript für eine Mission ```rhai const HEROES = 1; const MOSSGRIM = 2; fn start_intro(m) { m.log("Krummfinger: Diese Höhle ist nicht leer."); m.objective("Finde den Splitter und überlebe die Wellen."); } fn alarm(m, key, text) { if !m.has_flag(key) { m.set_flag(key); m.log(text); m.warn(text); } } fn reinforce_from_north(m) { m.spawn("Knight", 2, m.at(0.70, 0.35), HEROES); m.spawn("Archer", 2, m.at(0.72, 0.32), HEROES); } fn reinforce_from_south(m) { m.spawn("Knight", 2, m.at(0.70, 0.65), HEROES); m.spawn("Archer", 2, m.at(0.72, 0.68), HEROES); } fn on_start(m) { start_intro(m); } fn on_time(m) { if m.time >= 300 { alarm(m, "alarm_5min", "Nach fünf Minuten hören die Wachen etwas."); let roll = m.rand_choice(["north", "south"]); if roll == "north" { reinforce_from_north(m); } else { reinforce_from_south(m); } } if m.time >= 900 { alarm(m, "alarm_15min", "Die zweite Gruppe kommt."); m.spawn_wave(2); } } fn on_goal(m, goal) { if goal.name == "Shards" && goal.done { m.log("Der Splitter gehört uns."); m.objective("Halte durch, bis die letzten Wellen kommen."); m.spawn_wave(1); } if goal.name == "Survive" && goal.done { m.log("Der Ansturm ist geschlagen."); } } fn on_rival_attacked(m, side) { if side == MOSSGRIM && !m.has_flag("mossgrim_help") { m.set_flag("mossgrim_help"); m.log("Mossgrim hört den Lärm."); m.spawn_wave(3); } } fn on_end(m, won) { if won { m.log("Krummfinger: Gut gemacht. Der Kern wird vollständig."); } else { m.log("Krummfinger: Wir haben zu früh angefangen."); } } ``` ## 22. Was der Editor später können muss Der Leveleditor sollte mindestens diese Dinge bearbeiten: 1. Missionsdaten: - `key` - `title` - `story` - `hints` - `size` - `seed` - `gold` - `goals` - `portals` - `treasures` - `waves` - `wave_limit` - `first_wave` - `time_limit` 2. Kampagnenstruktur: - Missionen hinzufügen, entfernen, umsortieren. - Missionstitel, Story und Hinweise bearbeiten. - Kampagnennamen vergeben. - Missionen als JSON speichern und laden. 3. Kartenbearbeitung: - Größe setzen. - Gelände anpassen. - Flüsse und Seen bearbeiten. - Höhlen setzen. - Manaquellen setzen. - Tore setzen. - Festungen setzen und bearbeiten. - Startpunkte des Spielers setzen. - Triggerzonen grafisch setzen: Kreis, Rechteck, Polygon, markierte Kacheln, Linie/Brücke. - Zonen benennen, Radius oder Kachelmenge festlegen und automatisch ins Skript übernehmen. - Zonen optional filtern: nur Spielerseite, nur Ritterseite, bestimmte Kreaturen, einmalig auslösbar. 4. Skriptbearbeitung: - Skriptdatei auswählen oder im Editor schreiben. - Editor-Triggerzonen automatisch im Skript erzeugen oder aktualisieren. - Grafisch gesetzte Zonen im Editor verschieben, löschen oder umbenennen und ins Skript übernehmen. - Syntaxprüfung, wenn möglich mit Rhai. - Testmission direkt starten. - Fehler im Skript anzeigen: - unbekannte Funktion - unbekannte Kreatur - ungültige Koordinate - ungültiges Ziel - Endlosschleife 5. Briefing: - Titel - Storytext - Hinweise - objectives - optionale Sprache/Locale-Keys ## 23. Fehlerbeispiele Diese Fehler sollte der Editor anzeigen: ```rhai fn on_start(m) { m.spawn("UnknownCreature", 2, m.at(0.5, 0.5), 1); } ``` Fehler: unbekannte Kreatur. ```rhai fn on_time(m) { m.spawn("Knight", 2, m.at(2.0, 0.5), 1); } ``` Fehler: Koordinate außerhalb der Karte. ```rhai fn on_start(m) { while true { m.spawn_wave(1); } } ``` Fehler: Endlosschleife oder zu viele Wiederholungen. ```rhai fn on_start(m) { m.unknown_function(); } ``` Fehler: unbekannte Missionsfunktion. ## 24. Kurzfassung für Levelautoren Ein Missionsskript besteht normalerweise aus vier Teilen: ```rhai const HEROES = 1; fn on_start(m) { m.objective("Kurzes Ziel."); } fn on_time(m) { // zeitgesteuerte Ereignisse } fn on_goal(m, goal) { // auf Missionsfortschritt reagieren } fn on_unit_reaches(m, unit, zone) { // auf grafisch gesetzte Zonen reagieren } ``` Die wichtigsten Werkzeuge sind: ```rhai m.log("Text"); m.objective("Ziel"); m.spawn("Knight", 2, m.at(0.70, 0.50), HEROES); m.spawn_wave(3); m.has_flag("phase1_done"); m.set_flag("phase1_done"); m.counter("vaults_opened"); m.bump("vaults_opened"); m.load_editor_zones(); m.add_zone("north_gate", m.at(0.70, 0.50), 3.0); m.spawn_count_in_zone("north_gate", "Knight", 3, HEROES, 2); m.destroy_walls_in_zone("north_gate"); ``` Damit lassen sich bereits viele Missionen bauen: Wellen, Bossphasen, Reaktionen, Tresorfälle, Rivalen, Zeitlimits, Location-Trigger und dynamische Briefings.