21 KiB
Kampagneneditor - Ziel, Dateiformat und Verzweigungen
Dieses Dokument beschreibt den geplanten umfassenden Kampagneneditor für DungeonCraft. Der Editor soll vom Hauptmenü startbar sein und es ermöglichen, ganze Kampagnen zu erstellen, zu speichern, zu öffnen und an andere Spieler weiterzugeben.
Status: Entwurf plus erste Umsetzung. Die Kernstruktur (CampaignDoc, MissionDoc, BranchDoc, .dccamp, Menüpunkt, Editorseite, Start einzelner Missionen und Branch-Auswahl) ist implementiert; Karten-/Zonen-Editing folgt. Der Script-Basisruntime (on_start, m.log, m.objective, m.spawn_at, m.spawn_count_in_zone, m.spawn_wave) ist bereits in Rust integriert.
1. Ziel
Der Kampagneneditor soll später Folgendes können:
- Kampagnen aus dem Hauptmenü starten.
- Neue Kampagnen erstellen.
- Bestehende Kampagnen öffnen.
- Kampagnen als Datei speichern und teilen.
- Missionen einer Kampagne bearbeiten.
- Missionsziele definieren.
- Kampagnenverzweigungen erstellen.
- Briefing, Titel, Story, Hinweise und objectives bearbeiten.
- Kartenparameter bearbeiten.
- Festungen, Tore, Flüsse, Höhlen, Manaquellen und Rivalen setzen.
- Triggerzonen grafisch festlegen.
- Missionsskripte mit Rhai bearbeiten.
- Skripteditor mit Syntax-Highlighting und Syntaxprüfung.
- Missionen testweise starten.
- Kampagnenfortschritt speichern.
2. Start im Hauptmenü
Der Editor soll im Hauptmenü erreichbar sein, zum Beispiel:
DungeonCraft
- Kampagne
- Multiplayer
- Neues Spiel
- Spiel laden
- Einstellungen
- Konto
- Standalone herunterladen
- Bestiarium
- Kampagneneditor
- Hilfe
Geplante Menüaktion:
Btn::CampaignEditor
MenuAction::OpenCampaignEditor
Page::CampaignEditor
Der Editor soll auch aus dem Pausemenü erreichbar sein, wenn eine Kampagne gespielt wird, aber im Spielbetrieb nicht die aktuelle Mission zerstören, ohne eine Sicherung anzulegen.
3. Kampagnendatei speichern und öffnen
Kampagnendateien sollen weitergegeben werden können. Geplante Endung:
.dccamp
Eine .dccamp-Datei ist JSON. Sie enthält die vollständige Kampagne:
- Kampagnenname
- Missionen
- Missionsziele
- Verzweigungen
- Kartenparameter
- Scripte
- Triggerzonen
- optionale Sprachtexte
- Version und Formatangaben
Beispielhafter Export:
{
"format": "dungeoncraft.campaign",
"version": 1,
"name": "Meine eigene Kampagne",
"author": "Spieler",
"missions": [
{
"key": "my01",
"title": "Der erste Schacht",
"story": "Ein kleiner Posten, ein Splitter, zwei Wege.",
"size": [96, 80],
"seed": 101010,
"gold": 2500,
"goals": [
"Shards"
],
"next": "my02a",
"branches": [
{
"id": "killall_path",
"goal": "KillAll",
"title": "Die Wachstube zerstören",
"next": "my02a"
},
{
"id": "loot_path",
"goal": "Loot(3000)",
"title": "Die Tresore plündern",
"next": "my02b"
}
],
"zones": [
{
"name": "north_gate",
"shape": "circle",
"at": [0.70, 0.50],
"radius": 3.0
}
],
"script": {
"language": "rhai",
"source": "fn on_start(m) { m.log('Krummfinger: Es gibt zwei Wege.'); }"
}
}
]
}
Geplante Menüfunktionen:
Kampagne speichern ...
Kampagne öffnen ...
Kampagne herunterladen
Kampagne testweise spielen
Im Browser kann das mit den vorhandenen Datei-APIs realisiert werden:
- Speichern: Datei als
.dccampherunterladen. - Öffnen: Datei mit Dateiauswahl öffnen und JSON lesen.
- Herunterladen: aktuelle Kampagne als
.dccampexportieren.
4. Kampagnenstruktur
Eine Kampagne ist nicht mehr nur eine lineare Liste von Missionen. Sie ist ein Missionsgraph.
Vereinfacht:
my01
|
|--> Ziel A erreicht: my02a
|--> Ziel B erreicht: my02b
Komplexer:
my01
|
|--> KillAll: my02a
|--> Loot(3000): my02b
my02a
|
|--> Shards: my03a
|--> Rivals: my03b
my02b
|
|--> Freed(1): my03c
|--> Towers: my03b
Die Kampagne kann dadurch mehrere Abschnitte, alternative Wege und Wiedervereinigungen haben.
5. Mission mit Verzweigung
Eine Missionsdefinition muss später zwischen normalen Zielen und Verzweigungszielen unterscheiden.
Geplante Bedeutung:
goals: optionale oder normale Ziele.branches: alternative Missionsenden.- Eine Mission gilt als erfolgreich, wenn ein Verzweigungsziel erreicht ist.
- Das erreichte Verzweigungsziel bestimmt die nächste Mission.
Beispiel:
{
"key": "my01",
"title": "Die two Wege",
"story": "Die Ritter haben entweder eine Wachstube oder ein Schatzhaus. Du kannst dich entscheiden, was du zuerst schlägst.",
"goals": [
"Shards"
],
"branches": [
{
"id": "kill_path",
"goal": "KillAll",
"title": "Die Wachstube zerstören",
"next": "my02a",
"priority": 1
},
{
"id": "loot_path",
"goal": "Loot(3000)",
"title": "Die Tresore plündern",
"next": "my02b",
"priority": 2
}
]
}
Regel:
KillAllwird zuerst erreicht: Mission endet, nächste Mission istmy02a.Loot(3000)wird zuerst erreicht: Mission endet, nächste Mission istmy02b.- Wenn beide gleichzeitig erreicht werden, gewinnt die Branch mit niedrigerer
priority. Ist keine Priorität gesetzt, gewinnt die zuerst definierte Branch. - Wenn eine Mission mehrere
brancheshat, darf sie nicht als vollständig gelten, solange keine Branch erreicht ist. - Eine Mission ohne
branchesnutzt weiterhinnextoder die lineare Reihenfolge.
6. Mission mit alternativem Ziel a und b
Beispiel mit zwei alternativen Hauptzielen:
{
"key": "my03",
"title": "Der Nachbarsstreit",
"story": "Mossgrim oder der Orden - du kannst nur einen von beiden zuerst ausschalten.",
"goal_mode": "any",
"branches": [
{
"id": "mossgrim_first",
"goal": "Rivals",
"title": "Mossgrim zuerst ausschalten",
"next": "my04a"
},
{
"id": "order_first",
"goal": "Heart",
"title": "Das Tor des Ordens zuerst brechen",
"next": "my04b"
}
]
}
Spielerlebnis:
- Der Spieler sieht im Briefing beide Wege.
- Das Interface zeigt zwei objectives:
- Mossgrims Kern zerstören.
- Das Ordenstor brechen.
- Sobald eines davon erreicht ist, wird die Mission beendet.
- Der Fortschritt speichert, welcher Weg gewählt wurde.
- Die nächste Mission hängt von diesem Weg ab.
7. Missionsfortschritt bei Verzweigungen
Der Kampagnenfortschritt muss später Verzweigungen speichern.
Geplanter Fortschritt:
{
"format": "dungeoncraft.progress",
"campaign": "Meine eigene Kampagne",
"done": {
"my01": true,
"my02b": false
},
"completed_choice": {
"my01": "loot_path"
},
"unlocked": [
"my01",
"my02b"
],
"carry": {
"my01": {
"creatures": [["Ogre", 3, 120]],
"gold": 500,
"rune_free": 0,
"rune_final": 0
}
}
}
Regeln:
- Eine Mission ist nur dann abgeschlossen, wenn eine ihrer Branches erreicht wurde.
- Die erreichte Branch speichert
completed_choice. - Die nächste Mission wird durch
nextder erreichten Branch bestimmt. - Andere Branches einer bereits entschiedenen Mission gelten als übersprungen.
- Carry wird von der entschiedenen Mission zur nächsten Mission mitgenommen.
- Eine wiedervereinigte Mission kann von mehreren Wegpunkten betreten werden. In diesem Fall muss der Editor entscheiden, welcher Carry-Status verwendet wird, oder jeder Pfad speichert seinen eigenen Carry.
Geplante Variante mit getrenntem Carry pro Branch:
"completed_choice": {
"my01": "loot_path"
},
"branch_done": {
"my01.kill_path": false,
"my01.loot_path": true
},
"branch_carry": {
"my01.loot_path": {
"gold": 900,
"creatures": [["Ogre", 3, 0]]
}
}
8. Editor-Ansicht für Kampagnenverzweigungen
Im Editor soll es eine Kampagnenübersicht geben.
Geplante Darstellung:
My01 Die two Wege
|- KillAll -> My02a Die Wachstube
| |- Shards -> My03a
| `- Rivals -> My03b
`- Loot(3000) -> My02b Das Schatzhaus
|- Freed(1) -> My03c
`- Towers -> My03b
Editor-Funktionen:
- Mission hinzufügen.
- Mission entfernen.
- Mission umbenennen.
- Mission verschieben.
- Verzweigungsziel hinzufügen.
goalauswählen.nextMission auswählen.- Branch-Titel und Beschreibung bearbeiten.
- Priorität setzen.
- Testpfad starten.
- Zyklusprüfung: Kampagnen dürfen keine Endlosschleifen ohne Spielfortschritt enthalten.
- Erreichbarkeitsprüfung: alle Missionen sollen vom Start aus erreichbar sein, außer sie sind explizit als optionale Enden oder Extra-Missionen markiert.
9. Kampagnendatei mit Verzweigungen speichern
Eine gespeicherte Kampagne muss die Verzweigungen enthalten.
Beispielmission mit zwei Folgemissionen:
{
"key": "my01",
"title": "Die zwei Wege",
"size": [96, 80],
"seed": 101010,
"gold": 2500,
"branches": [
{
"id": "kill_path",
"goal": "KillAll",
"title": "Die Wacht zuerst ausschalten",
"next": "my02a",
"priority": 1
},
{
"id": "loot_path",
"goal": "Loot(3000)",
"title": "Die Tresore zuerst plündern",
"next": "my02b",
"priority": 2
}
],
"script": {
"language": "rhai",
"source": "fn on_start(m) { m.objective('Wähle einen Weg: die Wachstube oder die Tresore.'); }"
}
}
Wenn die Kampagne gespeichert wird, muss der Editor prüfen:
my02aexistiert.my02bexistiert.goal KillAllist für diese Mission sinnvoll.goal Loot(3000)ist für diese Mission sinnvoll.- Es gibt mindestens einen Startpfad.
- Die Mission endet wirklich, wenn eines der Branch-Ziele erreicht wird.
10. Geplante Rust-Datenstruktur
Für die Implementierung könnte der Editor mit datengetriebenen Missionen arbeiten.
Geplante Rust-Strukturen:
pub struct CampaignDoc {
pub format: String,
pub version: u32,
pub name: String,
pub author: String,
pub missions: Vec<MissionDoc>,
}
pub struct MissionDoc {
pub key: String,
pub title: String,
pub story: String,
pub size: (usize, usize),
pub seed: u32,
pub gold: f32,
pub portals: usize,
pub treasures: usize,
pub goals: Vec<String>,
pub waves: bool,
pub wave_limit: u32,
pub first_wave: f32,
pub time_limit: f32,
pub next: Option<String>,
pub branches: Vec<BranchDoc>,
pub script: Option<ScriptDoc>,
pub zones: Vec<ZoneDoc>,
}
pub struct BranchDoc {
pub id: String,
pub goal: String,
pub title: String,
pub next: String,
pub priority: Option<u32>,
}
pub struct ScriptDoc {
pub language: String,
pub source: String,
}
pub struct ZoneDoc {
pub name: String,
pub shape: String,
pub at: Option<(f32, f32)>,
pub radius: Option<f32>,
pub tiles: Option<Vec<(i32, i32)>>,
pub from: Option<(f32, f32)>,
pub to: Option<(f32, f32)>,
pub width: Option<f32>,
}
Die String-Ziele müssen beim Spielen in echte Goal-Werte übersetzt werden.
Beispiel:
"Shards" -> Goal::Shards
"Heart" -> Goal::Heart
"KillAll" -> Goal::KillAll
"Loot(3000)" -> Goal::Loot(3000)
"Survive(4)" -> Goal::Survive(4)
"Springs(2)" -> Goal::Springs(2)
"Freed(1)" -> Goal::Freed(1)
"Rivals" -> Goal::Rivals
"Towers" -> Goal::Towers
11. Verzweigungslogik im Spiel
Die Spiellogik braucht einen Missionsabschluss mit Branch.
Geplanter Ablauf:
Mission läuft.
Wenn ein normales Ziel erreicht wird: Fortschritt anzeigen.
Wenn ein Branch-Ziel erreicht wird:
Mission sofort beenden.
completed_choice speichern.
Carry aus aktuellem Spielstand erzeugen.
unlocked nächste Mission setzen.
Briefing oder Erfolgsmeldung anzeigen.
Pseudo-Logik:
if mission_has_branches {
for branch in mission.branches {
if goal_is_done(goal) && mission.state != Victory {
mission.state = Victory;
mission.completed_choice = branch.id;
progress.set_done(mission.key, branch.id, carry_from_game());
progress.unlock_mission(branch.next);
break;
}
}
} else if all_mission_goals_done() {
mission.state = Victory;
}
Bei zeitgleicher Erfüllung mehrerer Branches:
- Sortiere Branches nach
priority. - Wenn keine
prioritygesetzt ist, nutze die Reihenfolge in der JSON. - Wenn der Spieler eine Auswahl haben soll, kann der Editor
ask_on_conflict: trueaktivieren.
12. Beispielmission im Kampagneneditor
Beispiel für eine Mission mit grafischen Zonen, Script und Verzweigung:
{
"key": "my05",
"title": "Die Nordwand oder der Tresor",
"story": "Im Norden ist eine dünne Wand. Dahinter liegt eine Ritterstaffel. Im Osten liegt ein Tresor. Du musst dich entscheiden.",
"size": [104, 96],
"seed": 123456,
"gold": 3000,
"branches": [
{
"id": "north_wand",
"goal": "KillAll",
"title": "Die Nordwand sprengen",
"next": "my06a",
"priority": 1
},
{
"id": "east_vault",
"goal": "Loot(4000)",
"title": "Den Tresor räumen",
"next": "my06b",
"priority": 2
}
],
"zones": [
{
"name": "north_gate",
"shape": "circle",
"at": [0.70, 0.50],
"radius": 3.0
},
{
"name": "east_vault",
"shape": "tiles",
"tiles": [[74, 44], [74, 45], [75, 44], [75, 45]]
}
],
"script": {
"language": "rhai",
"source": "fn on_start(m) { m.load_editor_zones(); m.objective('Wähle: die Nordwand oder den Tresor.'); }\n\nfn on_unit_reaches(m, unit, zone) {\n if zone == 'north_gate' && unit.side == 0 && !m.has_flag('north_opened') {\n m.set_flag('north_opened');\n m.warn('Die Nordwand fällt!');\n m.destroy_walls_in_zone('north_gate');\n m.spawn_count_in_zone('north_gate', 'Knight', 3, 1, 2);\n }\n\n if zone == 'east_vault' && unit.side == 0 && !m.has_flag('vault_alarm') {\n m.set_flag('vault_alarm');\n m.warn('Tresoralarm!');\n m.destroy_doors_in_zone('east_vault');\n m.spawn_count_in_zone('east_vault', 'Knight', 2, 1, 3);\n }\n}"
}
}
13. Editor-Fehlerprüfungen
Der Editor soll Fehler anzeigen, bevor eine Kampagne gespeichert oder gespielt wird:
- Mission
keyist leer oder doppelt. titleoderstoryfehlen.sizeist zu klein oder zu groß.seedfehlt.goldist unzulässig.- Zielname ist unbekannt.
nextzeigt auf eine Mission, die nicht existiert.- Branch zeigt auf eine Mission, die nicht existiert.
- Eine Mission hat keine erreichbare Startmission.
- Eine Mission ist nach keiner anderen Mission erreichbar.
- Branches erzeugen einen Zyklus ohne Abschluss.
KillAllwird gewählt, aber es gibt keine Ritter.Heartwird gewählt, aber die Mission hat kein Tor.Loot(3000)wird gewählt, aber es gibt nicht genug Tresorgold.Springs(2)wird gewählt, aber es gibt weniger als zwei Manaquellen.Freed(1)wird gewählt, aber es gibt keine angekettete Kreatur.- Skript enthält unbekannte Funktionen.
- Zone hat eine ungültige Form oder ungültige Koordinaten.
- Scriptquelle kann nicht als Rhai gelesen werden.
14. Scripteditor mit Syntax-Highlighting
Der Editor soll ein Textfeld für Rhai enthalten.
Geplante Editorfeatures:
- Textarea im Spiel-Editor.
- Zeilennummerierung.
- Syntax-Highlighting.
- Tastatureingabe am Desktop.
- Touch: großes Textfeld oder Editor-Hilfsdialog.
- Speichern als
script.sourcein der.dccamp-Datei. - Testmission direkt starten.
- Fehleranzeige für ungültige Rhai-Syntax oder unbekannte Missionsfunktionen.
Geplante Farben:
Keyword: hellgelb
Comment: grau
String: grün
Number: orange
Mission method: hellblau
Creature kind: cyan
Zone name: violett
Error: rot
Beispiel:
const HEROES = 1;
const PLAYER = 0;
fn on_start(m) {
m.load_editor_zones();
m.objective("Zwei Wege: Nordwand oder Tresor.");
}
fn on_unit_reaches(m, unit, zone) {
if zone != "north_gate" {
return;
}
if unit.side != PLAYER {
return;
}
if m.has_flag("north_opened") {
return;
}
m.set_flag("north_opened");
m.warn("Die Nordwand fällt!");
m.destroy_walls_in_zone("north_gate");
m.spawn_count_in_zone("north_gate", "Knight", 3, HEROES, 2);
}
15. Kampagnenweitergabe
Ein Spieler kann eine Kampagne als Datei weitergeben:
meine_kampagne.dccamp
Ein anderer Spieler öffnet:
Hauptmenü -> Kampagneneditor -> Kampagne öffnen -> meine_kampagne.dccamp
Danach kann die Kampagne gespielt werden.
Die Datei enthält keine Spielstände. Sie enthält nur Missionsdefinitionen, Skripte und optionale Startfortschrittsdaten. Spielstände bleiben getrennt.
Geplante Dateitypen:
.dccamp - Kampagnendefinition
.dcsave - Spielstand
Ein Spielstand kann später auch eine aktive Kampagne referenzieren, aber die Kampagnendatei bleibt veränderbar und teilbar.
16. Empfohlene Implementierungsreihenfolge
Die Umsetzung sollte in Etappen erfolgen.
Etappe A: Datenmodell
CampaignDocMissionDocBranchDocZoneDocScriptDoc- JSON-Laden und Speichern
- Tests für Beispielkampagnen
Etappe B: Kampagnenstart im Menü
- Hauptmenüpunkt
Kampagneneditor. - Editorseite mit Kampagnenliste.
- Kampagne neu erstellen.
- Kampagne öffnen.
- Kampagne speichern.
Etappe C: Missionseditor
- Missionsliste innerhalb einer Kampagne.
- Mission hinzufügen, entfernen, auswählen.
- Missionsdaten bearbeiten.
- Branches bearbeiten.
- Ziele bearbeiten.
- Kartenparameter bearbeiten.
Etappe D: Verzweigungen spielen
- Mission endet bei Branch-Erfolg.
- Folgemission wird entsperrt.
- Progress speichert
completed_choice. - Carry übernimmt den gewählten Pfad.
- Kampagnenübersicht zeigt verzweigten Fortschritt.
Etappe E: Zoneneditor
- Kreis zeichnen.
- Rechteck zeichnen.
- Polygon zeichnen.
- Tiles markieren.
- Linien setzen.
- Zonen automatisch ins Skript übernehmen.
Etappe F: Scripteditor
- Texteditor mit Syntax-Highlighting.
- Rhai-Syntaxprüfung.
- Mission-API-Hilfe.
- Testmission starten.
Etappe G: Kampagnentests
- Beispielkampagne bauen.
- Branch-Kampagne bauen.
.dccampexportieren..dccampimportieren.- Fortschritt speichern und laden.
16a. Umgesetzter Stand
src/campaign_doc.rs:CampaignDoc,MissionDoc,BranchDoc,ZoneDoc,ScriptDoc,CampaignProgress.- JSON-Dateien
.dccampkönnen geladen, validiert, gespeichert und mitCampaignDoc::file_name()benannt werden. - Hauptmenü:
Btn::CampaignEditorführt aufPage::CampaignEditor. - Editorseite: Kampagnenname, Missionsliste, Auswahl, Neu, Add, Delete, Open, Save, Start.
- Feldbearbeitung im ersten Schritt: Titel, Ziele, nächste Mission, Branches und Rhai-Skriptquelle über Browser-/Desktop-Eingabeaufforderungen.
- Missionsstart:
campaign::mission_def_from_doc()undcampaign::start_mission_doc()können eine einzelne Editormission spielen. - Branch-Auswahl:
CampaignDoc::next_mission_choice()wählt nach Missionsende die nächste Mission;CampaignProgressspeichertdoneundcompleted_choice. - Script-Basisruntime:
src/script.rsführton_start(m)in Rhai aus. Unterstützt sind vorerstm.log(text),m.objective(text),m.spawn_at(side, kind, x, y, count, level),m.spawn_count_in_zone(zone, kind, count, side, level)undm.spawn_wave(count); Seiten können als Zahlen (0bis4) oder Namen (PLAYER,HEROES,MOSSGRIM,NIGHTWELL,RIVAL3) geschrieben werden. Meldungen, objective, Spawns und eine sofortige Missionswelle werden inGameangewendet. - Script-Editor (einfach):
Btn::CampaignEditScriptspeichert eine Rhai-Quelle inMissionDoc.script; die Quelle wird vor dem Speichern mitscript::run_start()undvalidate_script_source()geprüft. Syntaxfehler, unbekannte Kreaturnamen und unbekannte Zonennamen werden abgelehnt. - Debug-Anzeige: Einstellungen haben eigene Debug-Optionen für FPS, Framezeit, Ping, Triggerzonen, Debug-Befehlsmarker und Pfadlinien.
guard,attackundflag/gatherzeigen feste Befehlsmarker; alle übrigen Befehlsmarker sind debugpflichtig. - Noch offen: grafischer Zoneneditor, ausgebauter Rhai-Skripteditor mit Textfeld und Syntax-Highlighting, weitere Mission-Hooks (
on_unit_reaches,on_unit_crosses, Flags, Zonen, zeitgesteuerte Hooks), Zonen-Spawn-API, vollständige Kartenparameter, Tragen von Einheiten/Gold durch Branches.
17. Kurzfassung
Ein Kampagneneditor für ganze Kampagnen braucht nicht nur Missionstexte, sondern einen Missionsgraphen.
Kernidee:
Mission my01
-> Ziel A erreicht: Mission my02a
-> Ziel B erreicht: Mission my02b
Die Kampagne wird als .dccamp gespeichert:
"branches": [
{ "goal": "KillAll", "next": "my02a" },
{ "goal": "Loot(3000)", "next": "my02b" }
]
Der Editor muss diese Verzweigungen anzeigen, prüfen und speichern können. Der Spieler sieht dann alternative objectives und die Kampagne führt je nach erreichten Zielen zu unterschiedlichen Folgemissionen.