docs: Kampagnen-Vorlage + Research-Reports
docs/CAMPAIGN-TEMPLATE.md (wiederverwendbare, tool-verknuepfte Vorlage), docs/research/CAMPAIGN-RESEARCH.md (Faehigkeits-/Gap-Analyse), docs/research/TERRAIN-AUTOMATION.md (Feasibility + MVP). README verlinkt Vorlage und neue Tools. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aba9759400
commit
7c8fc37472
4 changed files with 624 additions and 0 deletions
13
README.md
13
README.md
|
|
@ -14,6 +14,16 @@ Die ganze Karten-Manipulation läuft über die Python-Bibliothek
|
|||
|
||||
---
|
||||
|
||||
## Kampagne planen
|
||||
|
||||
Bevor du baust: die wiederverwendbare, tool-verknüpfte Planungs-Vorlage
|
||||
[`docs/CAMPAIGN-TEMPLATE.md`](docs/CAMPAIGN-TEMPLATE.md) kopieren und Schritt für Schritt
|
||||
abarbeiten (Kampagnen-Header → Per-Mission-Block → Abschluss-Check). Hintergrund,
|
||||
code-abgesicherte Fähigkeitsübersicht und vollständige Gap-Analyse:
|
||||
[`docs/research/CAMPAIGN-RESEARCH.md`](docs/research/CAMPAIGN-RESEARCH.md).
|
||||
|
||||
---
|
||||
|
||||
## In 3 Schritten loslegen
|
||||
|
||||
### 1. Server starten (ein Befehl)
|
||||
|
|
@ -83,6 +93,8 @@ die erste Mission bauen kannst.
|
|||
| Tool | Zweck |
|
||||
|------|-------|
|
||||
| `sc_list_maps` | listet alle Karten/Missionen im Verzeichnis *(nur lesen)* |
|
||||
| `sc_list_templates` | listet vorgefertigte Terrain-Templates mit Tileset/Größe *(nur lesen)* |
|
||||
| `sc_new_from_template` | erzeugt aus einem Template eine neue Arbeits-Basiskarte |
|
||||
| `sc_describe_map` | Übersicht: Tileset, Größe, Player-Setup, Locations, Trigger-Zusammenfassung *(nur lesen)* |
|
||||
| `sc_list_locations` | Locations auflisten *(nur lesen)* |
|
||||
| `sc_create_location` | Location anlegen (Mittelpunkt + Größe in Pixeln) |
|
||||
|
|
@ -170,6 +182,7 @@ Für HTTP lokal: `SC_TRANSPORT=http SC_PORT=8000 SC_MAPS_DIR=./data/maps python
|
|||
| `SC_HOST` | `0.0.0.0` (HTTP) | Bind-Adresse |
|
||||
| `SC_PORT` | `8000` | Port |
|
||||
| `SC_MAPS_DIR` | `/data/maps` | Karten-/WAV-/Missions-Verzeichnis |
|
||||
| `SC_TEMPLATES_DIR` | `<SC_MAPS_DIR>/templates` | Verzeichnis mit Terrain-Templates |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
185
docs/CAMPAIGN-TEMPLATE.md
Normal file
185
docs/CAMPAIGN-TEMPLATE.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# 🛠️ Star-Edit — Kampagnen-Vorlage (Missions-Baumeister)
|
||||
|
||||
**So benutzt du diese Datei:** Kopiere sie pro Kampagne (z. B. nach `docs/campaigns/<name>.md`), fülle den **Kampagnen-Header** einmal aus, und dann **dupliziere den Per-Mission-Block** für jede Mission. Jeder Schritt ist eine abhakbare `[ ]`-Checkliste und nennt direkt das `sc_`-Tool + Kern-Parameter — Claude arbeitet sie der Reihe nach ab.
|
||||
|
||||
> **Faktenbasis:** Alle Tool-/Trigger-Angaben sind code-abgesichert. Das *Warum* + die vollständige Gap-Analyse (was geht, was nur per Workaround, was gar nicht) steht in [`research/CAMPAIGN-RESEARCH.md`](research/CAMPAIGN-RESEARCH.md). **Lies dort Abschnitt C, bevor du etwas planst, das ein Briefing, ein Talking-Portrait, Switches oder feine Cutscene-Taktung braucht** — das geht mit den aktuellen Tools nicht (siehe „Bekannte Lücken" unten).
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Grundregeln (immer gültig)
|
||||
|
||||
- **Session = Dateiname.** Alle Edits an einer Karte sammeln sich im Speicher des Servers an; erst `sc_save_map` schreibt eine **neue** Datei. Die Basis-Karte bleibt unangetastet → 1 Basis kann viele Missionen erzeugen.
|
||||
- **Reihenfolge zählt:** Locations & WAVs **zuerst**, dann die Trigger, die sie referenzieren.
|
||||
- **Verworfen?** `sc_reset_map(<map>)` lädt frisch von Platte (alle ungespeicherten Edits weg).
|
||||
- **Koordinaten** in Pixeln (32 px = 1 Kachel). `right/bottom` werden nicht an den Kartenrand geklammert → nicht überlaufen lassen.
|
||||
- **Location-Namen** sind exakt & case-sensitiv. **Enum-Werte** (Units, Player, Comparator …) sind tolerant: Identifier (`TERRAN_MARINE`), Anzeigename (`Terran Marine`) oder Zahl (`0`).
|
||||
- **`preserve`**: `True` = Trigger bleibt nach Auslösen (Wellen, Win/Lose). `False` = einmalig (Init, Intro).
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Asset-Pipeline: woher kommt die Karte? (Vorbedingung)
|
||||
|
||||
Star-Edit **generiert kein Terrain**. Jede Mission startet von einer fertigen Basis-Karte. Aktueller Weg:
|
||||
|
||||
1. **Terrain in SCMDraft 2** bauen (Map-Größe, Tileset, Terrain, ggf. Start-Doodads/Einheiten) → speichern als `.scx`. *(SCMDraft = `http://www.stormcoast-fortress.net/`)*
|
||||
2. `.scx` nach `SC_MAPS_DIR` legen (lokal: `G:\Claude\Star-Edit\data\maps\`).
|
||||
3. Star-Edit fügt die **Logik** hinzu (dieser Vorlage folgend).
|
||||
|
||||
> ⚠️ **Caveat 1 — nicht gleichzeitig:** Datei **nie** parallel in SCMDraft offen haben *und* per MCP schreiben. Einer überschreibt den anderen.
|
||||
> ⚠️ **Caveat 2 — Round-Trip einmal verifizieren:** Bevor du eine ganze Kampagne baust, einmal eine Mini-Mission durch die ganze Pipeline jagen und prüfen, dass die vom Server geschriebene `.scx` **in SCMDraft und im Spiel** sauber lädt. *(Dieser Test ist zugleich das Gate für die Terrain-Automatisierung — siehe unten.)*
|
||||
|
||||
### 🚧 Terrain-Automatisierung — wohin die Reise geht
|
||||
|
||||
Recherche-Ergebnis (Details: [`research/TERRAIN-AUTOMATION.md`](research/TERRAIN-AUTOMATION.md)):
|
||||
|
||||
- **SCMDraft hat keine CLI** → lässt sich nicht sinnvoll fernsteuern (GUI-Klick-Automation = letzte Wahl).
|
||||
- **RichChk *kann* Terrain-Bytes schreiben** (DIM/ERA/MTXM/TILE/ISOM/MASK), **aber** es generiert kein ISOM (Blending/Cliffs/Pathing) — das ist der harte Kern.
|
||||
- **Empfohlener Weg (MVP):** **Template-Bibliothek** statt Generator. Einmalig 3–6 saubere Basiskarten von Hand in SCMDraft bauen (je Größe/Tileset, mit korrektem ISOM) → der MCP wählt nur noch aus und kopiert. So entfällt das „jedes Mal Terrain bauen", bei Editor-Qualität und null Engine-Risiko.
|
||||
- **Gate (programmatisch ✅ bestanden, 2026-06-22):** `tools/terrain_roundtrip_test.py` belegt, dass RichChk eine `.scx` section-stabil durchreicht (alle 38 CHK-Sections inkl. ISOM/VCOD identisch). **Offen:** der manuelle „öffnet in SCMDraft + StarCraft"-Check. Details: [`research/TERRAIN-AUTOMATION.md`](research/TERRAIN-AUTOMATION.md).
|
||||
|
||||
→ Bis der Template-Katalog existiert, gilt weiterhin: Basis-Karte manuell in SCMDraft bauen (Schritte 1–3 oben).
|
||||
|
||||
---
|
||||
|
||||
## 📋 KAMPAGNEN-HEADER (einmal pro Kampagne ausfüllen)
|
||||
|
||||
```
|
||||
Kampagne: ______________________________
|
||||
Episode/Akt: __________ · Geplante Missionen: 7–12 (Standalone) | 3×10 (Trilogie)
|
||||
Protagonist/Fraktion: __________ · Rasse: TERRAN | ZERG | PROTOSS
|
||||
|
||||
Arc-Beats (ein Eintrag pro Mission — welcher Story-Beat, welches NEUE Konzept):
|
||||
M1: Tutorial / Helden-only (keine Ökonomie)
|
||||
M2: ______________________________
|
||||
M3: ______________________________
|
||||
…
|
||||
Finale: Set-Piece / Defend-then-counter
|
||||
```
|
||||
|
||||
**Cast / Charaktere** (wer spricht, welche Unit/„Portrait"):
|
||||
|
||||
| Charakter | Rolle | Unit (für Funksprüche/Auftritt) |
|
||||
|---|---|---|
|
||||
| | | |
|
||||
|
||||
**Globale Konventionen** (über ALLE Missionen identisch halten):
|
||||
- [ ] Player/Force-Schema fix (PLAYER_1..8, FORCE_1..4) — wer ist Spieler, wer Gegner, wer Ally?
|
||||
- [ ] Rassen-Zuordnung je Player
|
||||
- [ ] Ressourcen-Startwerte & Allianz-Defaults
|
||||
- [ ] Audio-Spec: **PCM 16-bit, mono, 11025 Hz**, Dateinamen **ohne Leerzeichen**, abgelegt in `SC_MAPS_DIR`
|
||||
- [ ] Location-Namens-Schema festlegen (exakt & case-sensitiv)
|
||||
|
||||
**Asset-Inventar:**
|
||||
- [ ] `sc_list_maps()` → welche Basis-Karten liegen schon in `MAPS_DIR`?
|
||||
- [ ] Pro Mission: Basis-Karte vorhanden? WAVs vorhanden?
|
||||
|
||||
---
|
||||
|
||||
## 🎯 PER-MISSION-BLOCK ⟶ *für jede Mission kopieren*
|
||||
|
||||
> Ersetze `<map>` durch die Basis-Kartendatei (= Session-Key). Erst alles sammeln, am Ende **einmal** `sc_save_map`.
|
||||
|
||||
### Mission `<NN>` — `<Titel>`
|
||||
- **Archetyp:** (Destroy-all · Defend/Survive · Escort · Hero-only · Hunt · Gather · Rescue/Hold · Assassination · Puzzle · Defend-then-counter)
|
||||
- **Story-Beat:** ______ · **Neues Konzept:** ______
|
||||
- **Pflichtziel (→ Victory):** ______
|
||||
- **Niederlage-Bedingung(en) (→ Defeat):** ______
|
||||
- **Optionale Ziele (0–2):** ______
|
||||
|
||||
#### Schritt 0 — Bestandsaufnahme
|
||||
- [ ] `sc_describe_map(map=<map>)` — Tileset, Größe, 8 Player, Forces, Locations, Trigger, `pending_wav_embeds`.
|
||||
- [ ] `sc_list_locations(map=<map>)` · `sc_list_triggers(map=<map>)`
|
||||
- [ ] *(Nur falls Basis-Trigger ersetzt werden sollen — DESTRUCTIVE)* `sc_clear_triggers(map=<map>)`
|
||||
|
||||
#### Schritt 1 — Player- & Force-Setup
|
||||
- [ ] `sc_set_player_setup(map=<map>, players=[ {"player":"PLAYER_1","type":"HUMAN","race":"TERRAN","force":"FORCE_1"}, {"player":"PLAYER_2","type":"COMPUTER","race":"ZERG","force":"FORCE_2"} ], force_names=[ {"force":"FORCE_1","name":"<Allianz>"} ])`
|
||||
- *jeder Eintrag braucht `player` (Pflicht); `type`/`race`/`force` optional; nur FORCE_1..4.*
|
||||
|
||||
#### Schritt 2 — Locations *(vor den Triggern!)*
|
||||
- [ ] `sc_create_location(map=<map>, name="<eindeutig>", center_x=<px>, center_y=<px>, width=96, height=96)` *(je benötigte Stelle wiederholen)*
|
||||
- [ ] *(optional)* `sc_rename_location(map=<map>, old_name="…", new_name="…")`
|
||||
|
||||
#### Schritt 3 — Audio *(vor `play_wav`-Triggern)*
|
||||
- [ ] WAV in `MAPS_DIR` ablegen (Spec siehe Header).
|
||||
- [ ] `sc_embed_wav(map=<map>, wav_filename="<funk1.wav>")` → `wav_path` notieren (= `staredit\wav\funk1.wav`).
|
||||
|
||||
#### Schritt 4 — Mission-Init-Trigger *(einmalig, `preserve=False`)*
|
||||
- [ ] `sc_add_trigger(map=<map>, players=["ALL_PLAYERS"], conditions=[{"type":"always"}], actions=[`
|
||||
`{"type":"set_mission_objectives","text":"<Pflichtziel>"},`
|
||||
`{"type":"set_resources","player":"PLAYER_1","resource":"ORE_AND_GAS","amount":<n>,"modifier":"SET_TO"},`
|
||||
`{"type":"set_alliance_status","player":"FORCE_2","alliance_status":"ENEMY"},`
|
||||
`{"type":"run_ai_script","ai_script":"<SCRIPT_ODER_4CHAR>"},`
|
||||
`{"type":"create_unit","player":"PLAYER_1","unit":"TERRAN_MARINE","location":"<Basis>","amount":4},`
|
||||
`{"type":"center_view","location":"<Basis>"} ], preserve=False)`
|
||||
|
||||
#### Schritt 5 — Intro / „Briefing"-Ersatz *(Workaround — es gibt KEIN echtes MBRF-Briefing)*
|
||||
- [ ] `sc_add_trigger(map=<map>, players=["PLAYER_1"], conditions=[{"type":"always"}], actions=[`
|
||||
`{"type":"center_view","location":"<Szene>"},`
|
||||
`{"type":"play_wav","wav_path":"staredit\\wav\\funk1.wav","duration_ms":<ms>},`
|
||||
`{"type":"display_text","text":"<Intro-Zeile>"},`
|
||||
`{"type":"minimap_ping","location":"<Ziel>"} ], preserve=False)`
|
||||
- *Kein Portrait, kein erzwungener „Hold". Lieber mehrere kurze Zeilen als eine lange Rede.*
|
||||
|
||||
#### Schritt 6 — Spiel-Logik *(archetyp-spezifisch)*
|
||||
- [ ] **Gegner-Wellen** (`preserve=True`): `conditions=[{"type":"elapsed_time","seconds":<t>,"comparator":"AT_LEAST"}]`, `actions=[{"type":"create_unit","player":"PLAYER_2","unit":"<Zerg>","location":"<Spawn>","amount":6},{"type":"run_ai_script_at_location","ai_script":"<ATTACK>","location":"<Ziel>"}]`
|
||||
- [ ] **Phasen-Flag / Zähler** (Switch-Ersatz via Death-Counter):
|
||||
- setzen: `actions=[{"type":"set_deaths","player":"PLAYER_1","unit":"<MARKER_UNIT>","amount":1,"modifier":"SET_TO"}]`
|
||||
- lesen: `conditions=[{"type":"deaths","player":"PLAYER_1","unit":"<MARKER_UNIT>","amount":1,"comparator":"AT_LEAST"}]`
|
||||
- [ ] **Optionale Ziele:** eigener Trigger → bei Erfüllung `give_units`/`set_resources` belohnen + `set_mission_objectives` aktualisieren.
|
||||
- [ ] **Timer-Logik** (falls Survive/Defend): `set_countdown_timer` / `pause_timer` / `unpause_timer`.
|
||||
|
||||
#### Schritt 7 — Win / Lose *(immer explizit, `preserve=True`)*
|
||||
- [ ] **Win:** `sc_add_trigger(players=["PLAYER_1"], conditions=[ <siehe Archetyp> ], actions=[{"type":"display_text","text":"Mission erfüllt"},{"type":"victory"}], preserve=True)`
|
||||
- Destroy-all: `{"type":"opponents","player":"PLAYER_1","amount":0,"comparator":"AT_MOST"}`
|
||||
- Escort: `{"type":"bring","player":"PLAYER_1","amount":1,"unit":"<VIP>","location":"<Exit>","comparator":"AT_LEAST"}`
|
||||
- Assassination: `{"type":"deaths","player":"PLAYER_1","unit":"<TARGET>","amount":1,"comparator":"AT_LEAST"}`
|
||||
- Gather: `{"type":"accumulate","player":"PLAYER_1","amount":<X>,"resource":"ORE_AND_GAS","comparator":"AT_LEAST"}`
|
||||
- Survive: `{"type":"countdown_timer","seconds":0,"comparator":"AT_MOST"}` *(oder `elapsed_time`)*
|
||||
- [ ] **Lose:** `sc_add_trigger(players=["PLAYER_1"], conditions=[{"type":"deaths","player":"PLAYER_1","unit":"<VIP_ODER_KEY_STRUCTURE>","amount":1,"comparator":"AT_LEAST"}], actions=[{"type":"defeat"}], preserve=True)`
|
||||
|
||||
#### Schritt 8 — Kontrolle vor dem Speichern
|
||||
- [ ] `sc_list_triggers(map=<map>)` — Reihenfolge / Player / Conditions / Actions gegen den Plan prüfen.
|
||||
- [ ] `sc_describe_map(map=<map>)` — alle erwarteten WAVs in `pending_wav_embeds`? Locations vollständig?
|
||||
- [ ] Falsch eingefügt? `sc_remove_trigger(map=<map>, index=<i>)` *(0-basiert)*.
|
||||
|
||||
#### Schritt 9 — Speichern *(neue Missionsdatei)*
|
||||
- [ ] `sc_save_map(map=<map>, output_name="mission<NN>.scx", overwrite=False)`
|
||||
- Rückgabe prüfen: `embedded_wavs`, `triggers`, `locations`. Bei `FileExistsError` → neuer Name oder `overwrite=True`.
|
||||
|
||||
#### ✅ Mission-Designcheck (vor Schritt 9 abhaken)
|
||||
- [ ] Genau **ein** Pflichtziel treibt `victory`; Text in `set_mission_objectives`.
|
||||
- [ ] **Alle** Lose-Pfade verdrahtet (Held/VIP-Tod, Timer, Key-Structure).
|
||||
- [ ] Gegner-AI gesetzt; Aggression passt zum Beat.
|
||||
- [ ] Wellen `preserve=True`, Init/Intro `preserve=False`.
|
||||
- [ ] Jede in Triggern referenzierte Location existiert & ist exakt benannt.
|
||||
- [ ] WAVs vorab in `MAPS_DIR`, `sc_embed_wav` aufgerufen, `wav_path` korrekt.
|
||||
- [ ] Im Spiel getestet: **Win-Pfad UND jeder Lose-Pfad**.
|
||||
|
||||
---
|
||||
|
||||
## 🏁 KAMPAGNEN-ABSCHLUSS (closeout)
|
||||
|
||||
- [ ] Alle Missionen als eigene `.scx` in `MAPS_DIR`; Namensschema `mission01..NN` konsistent.
|
||||
- [ ] Player/Force/Rassen-Schema über alle Missionen identisch (per `sc_describe_map` stichprobenartig).
|
||||
- [ ] Audio-Budget gesamt geprüft (Anzahl/Größe je Karte; Spec eingehalten).
|
||||
- [ ] Jede Mission: Win- **und** jeder Lose-Pfad manuell getestet.
|
||||
- [ ] **Round-Trip ok:** fertige `.scx` lädt in SCMDraft **und** im Spiel.
|
||||
- [ ] **Missionsverkettung extern gelöst** (`Set Next Scenario` geht NICHT über die Tools).
|
||||
- [ ] Briefing-Ersatz (In-Mission-Intro) in jeder Mission vorhanden (kein echtes MBRF).
|
||||
- [ ] Basis-Karten unverändert gesichert (Save erzeugt stets neue Dateien).
|
||||
|
||||
---
|
||||
|
||||
## 🚧 Bekannte Lücken (was die Tools NICHT können — Workarounds einplanen!)
|
||||
|
||||
| Will ich… | Geht das? | Stattdessen |
|
||||
|---|---|---|
|
||||
| Pre-Mission-**Briefing** (MBRF-Screen) | ❌ | In-Mission-„Intro" zu Spielbeginn (`always` + `display_text`/`play_wav`/`center_view`) |
|
||||
| **Transmission** mit Talking Portrait | ❌ Portrait / 🟉 Rest | `play_wav` + `display_text` + `center_view` (+`minimap_ping`) — ohne Gesicht, ohne erzwungenen Hold |
|
||||
| **Switches** (echte benannte Flags) | ❌ | State/Phasen via Death-Counter (`set_deaths` / `deaths`) mit Marker-Unit |
|
||||
| **Hyper Triggers / `Wait`** (feine Taktung) | ❌ | Nur grobe Taktung über `elapsed_time`/`countdown_timer` (~2-Sek-Raster) |
|
||||
| **Set Next Scenario** (Karten verketten) | ❌ | Extern/manuell lösen |
|
||||
| **Create Unit With Properties** / HP-Modify (Helden-Stats) | ❌ | Nur schlichtes `create_unit` |
|
||||
| Leaderboard / Score / „the Most" | ❌ | — |
|
||||
|
||||
→ Vollständige, code-belegte Liste: [`research/CAMPAIGN-RESEARCH.md`](research/CAMPAIGN-RESEARCH.md) Abschnitt C.
|
||||
321
docs/research/CAMPAIGN-RESEARCH.md
Normal file
321
docs/research/CAMPAIGN-RESEARCH.md
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
# Star-Edit MCP — Grounded Research Report für eine SC:BW-Kampagnen-Vorlage
|
||||
|
||||
> Geltungsbereich: Der Star-Edit-MCP-Server bearbeitet ausschließlich **Daten/Logik existierender Karten** (Trigger, Texte, Locations, Player-Setup, Sounds). **Kein Terrain-Generieren.** Jede Mission startet von einer bestehenden Basis-Karte in `SC_MAPS_DIR` (Default `/data/maps`). Alle Fähigkeitsaussagen in Abschnitt A sind strikt aus dem Quellcode abgeleitet — autoritativ sind `server.py`, `triggers.py`, `enums.py`, `workspace.py`. Abschnitt B/C/D verknüpfen das mit der SC:BW-Domänenforschung.
|
||||
|
||||
---
|
||||
|
||||
## A) Was Star-Edit heute wirklich kann
|
||||
|
||||
### A.1 Architektur & Session-Modell
|
||||
|
||||
- **Server:** `FastMCP("starcraft-campaign-mcp")` mit **13** `sc_`-Tools. Transport per Env `SC_TRANSPORT` (Default `stdio`; Streamable-HTTP-Branch unter `http://0.0.0.0:8000/mcp` vorhanden, server.py:505–511). *(Hinweis: Der oft genannte Docker-Default `http`, die Python-3.12-Laufzeit und konkrete Dependency-Pins wie `richchk==0.1.1`/`mcp[cli]==1.28.0`/`PyYAML>=6.0` lassen sich aus den vier autoritativen Quelldateien `server.py`/`triggers.py`/`enums.py`/`workspace.py` **nicht** verifizieren — sie stammen aus `Dockerfile`/`requirements.txt`, die hier nicht als geprüfte Quelle vorliegen, und sind daher unbestätigt.)*
|
||||
- **Session = Dateiname.** Es gibt **keinen** Session-Token. Jeder Tool-Call ruft `workspace.open_workspace(map)`; Schlüssel ist `os.path.basename(map)`. Workspaces liegen in einem **prozessweiten Singleton-Dict** (`_WORKSPACES`, lock-geschützt) und leben **so lange wie der Prozess** — kein TTL, keine Per-Client-Isolation. Alle Aufrufer im Prozess teilen sich denselben Workspace pro Karte.
|
||||
- **In-Memory-Editmodell.** Edits ersetzen ganze CHK-Sektionen (`Workspace.replace(...)`) und akkumulieren auf dem gecachten Workspace. **Nichts wird auf Platte geschrieben außer bei `sc_save_map`.** Edit-Tools liefern `"hinweis": "Aenderung ist im Speicher. Mit sc_save_map persistieren."`
|
||||
- **Basis-Karte unangetastet.** `sc_save_map` schreibt **immer eine neue Ausgabedatei** (Basis als Template). Aus einer Basis lassen sich viele Missionen erzeugen.
|
||||
- **Reset/Discard.** `sc_reset_map` → `open_workspace(map, fresh=True)`: liest frisch von Platte, **verwirft alle ungespeicherten Edits**.
|
||||
- **Pfad-Confinement.** `map_path()` reduziert jeden Namen auf `basename` → kein Ausbrechen aus `SC_MAPS_DIR`. Gilt für Lesen, Speichern und WAV-Quellen.
|
||||
- **Fehler:** Validierungsfehler werfen (`ValueError`/`FileNotFoundError`/`KeyError`/`FileExistsError`), statt Error-Dicts zurückzugeben.
|
||||
- **Annotations:** `_READONLY`, `_EDIT`, `_DESTRUCTIVE` (Letzteres: `sc_remove_trigger`, `sc_clear_triggers`, `sc_reset_map`; server.py:352, 379, 463).
|
||||
|
||||
### A.2 Die 13 Tools (Signatur + Kernverhalten)
|
||||
|
||||
**Read**
|
||||
| Tool | Parameter | Liefert / Verhalten |
|
||||
|---|---|---|
|
||||
| `sc_list_maps` | — | `{maps_dir, count, maps}` aus `workspace.list_maps()` (alle `.scm`/`.scx` in `MAPS_DIR`). |
|
||||
| `sc_describe_map` | `map` | Überblick: `tileset`, `size{width,height}`, **genau 8** `players` (PLAYER_1..8; fehlend → `"?"`), `forces`, `locations` (Box `[left_x1,top_y1,right_x2,bottom_y2]`), `trigger_count`, `triggers` (Klartext-Summaries), `pending_wav_embeds`. Spiegelt In-Memory-Stand. |
|
||||
| `sc_list_locations` | `map` | Locations mit `name`/`index`/`box`. |
|
||||
| `sc_list_triggers` | `map` | `trigger_count` + pro Trigger `index`, `players`, Bedingungs-/Aktions-Summaries. |
|
||||
|
||||
**Location**
|
||||
| Tool | Parameter (Defaults) | Verhalten / Grenzen |
|
||||
|---|---|---|
|
||||
| `sc_create_location` | `map`, `name`, `center_x`, `center_y`, `width=96`, `height=96` | `ws.add_location(...)`. Name muss **eindeutig** sein (sonst `ValueError`). Liefert `index` + `box`. Tool-Doku nennt explizit die Konvention „Koordinaten in Pixeln (32 px = 1 Kachel)" (server.py:173–174). |
|
||||
| `sc_rename_location` | `map`, `old_name`, `new_name` | Umbenennen; wirft bei fehlendem `old_name` oder kollidierendem `new_name`. Kein `hinweis` im Return (server.py:209 liefert `{"ok", "name", "index"}`). |
|
||||
|
||||
**Player-Setup**
|
||||
| Tool | Parameter | Verhalten |
|
||||
|---|---|---|
|
||||
| `sc_set_player_setup` | `map`, `players: list[dict]`, `force_names: Optional[list[dict]]=None` | Pro Eintrag `player` **Pflicht** (`KeyError` sonst, server.py:242); `type`/`race`/`force` optional. Setzt OWNR/SIDE/FORC; `force_names` benennt FORCE_1..4 (`force`+`name` Pflicht, server.py:271–272). |
|
||||
|
||||
Erlaubte Strings (toleranter Resolver, case-insensitiv, Identifier/Display-Name/Numeric-ID):
|
||||
- **type (OWNR):** `INACTIVE, COMPUTER_GAME, HUMAN_OCCUPIED, RESCUE_PASSIVE, COMPUTER, HUMAN, NEUTRAL, CLOSED`
|
||||
- **race (SIDE):** `ZERG, TERRAN, PROTOSS, INDEPENDENT, NEUTRAL, USER_SELECT, RANDOM, INACTIVE`
|
||||
- **force:** `FORCE_1..FORCE_4`
|
||||
|
||||
**Trigger**
|
||||
| Tool | Parameter (Defaults) | Verhalten / Grenzen |
|
||||
|---|---|---|
|
||||
| `sc_add_trigger` | `map`, `players: list[str]`, `conditions: list[Condition]`, `actions: list[Action]`, `preserve=True` | Fügt **genau einen** Trigger an. `players` leer → `ValueError`; jeder Eintrag via `resolve_player`, dedupliziert in `set`. `conditions` leer → **`always`**. `preserve=True` hängt `PreserveTrigger()` an (zählt als Aktion!). Ohne Aktion **und** `preserve=False` → `ValueError`. |
|
||||
| `sc_remove_trigger` | `map`, `index` | Entfernt Trigger per 0-basiertem Index; Out-of-Range → `ValueError`. |
|
||||
| `sc_clear_triggers` | `map` | Entfernt **alle** Trigger. Nur per `sc_reset_map` rückholbar. |
|
||||
|
||||
**Sound / Save / Reset**
|
||||
| Tool | Parameter (Defaults) | Verhalten |
|
||||
|---|---|---|
|
||||
| `sc_embed_wav` | `map`, `wav_filename` | WAV **muss als Datei in `MAPS_DIR` liegen** (Existenzprüfung `os.path.exists` im Tool, server.py:408). Registriert In-MPQ-Pfad `staredit\wav\<basename>` und queued physisches Einbetten (`pending_wavs`). Liefert `wav_path` (= genau diesen In-MPQ-Pfad) für `play_wav`. Fehlende Datei → `FileNotFoundError` mit Liste verfügbarer `.wav`. |
|
||||
| `sc_save_map` | `map`, `output_name`, `overwrite=False` | Schreibt neue `.scm`/`.scx`. Endung `.scx` wird **nur** auto-ergänzt, wenn der Name nicht bereits auf `.scx` **oder** `.scm` endet (workspace.py:178–180) → ein angegebenes `.scm` bleibt erhalten. Existiert Ziel & `overwrite=False` → `FileExistsError`. Bettet `pending_wavs` physisch ein. Liefert `output`, `path`, `embedded_wavs`, `triggers`, `locations`. **Leert `pending_wavs` nicht** → erneutes Speichern bettet erneut ein. |
|
||||
| `sc_reset_map` | `map` | Frisch von Platte; verwirft Edits. |
|
||||
|
||||
### A.3 Unterstützte Trigger-CONDITIONS (exakte `type`-Strings)
|
||||
|
||||
`type` wird `strip().lower()`-normalisiert. `comparator` Default `AT_LEAST` (`AT_LEAST`/`AT_MOST`/`EXACTLY`; triggers.py:143). `amount` ist bei allen Vergleichs-Conditions effektiv Pflicht (`_require`-geprüft). Alle neun `type`-Strings entsprechen exakt `build_condition` (triggers.py:239–294).
|
||||
|
||||
| `type` | Pflichtfelder | Optional | Bedeutung |
|
||||
|---|---|---|---|
|
||||
| `always` | — | — | Immer wahr (Default bei leerer Liste). |
|
||||
| `never` | — | — | Nie wahr (deaktiviert). |
|
||||
| `elapsed_time` | `seconds` | `comparator` | N Sekunden seit Spielstart. |
|
||||
| `countdown_timer` | `seconds` | `comparator` | Countdown-Timer-Vergleich. |
|
||||
| `bring` | `player`,`amount`,`unit`,`location` | `comparator` | N Einheiten eines Typs **an Location**. |
|
||||
| `command` | `player`,`amount`,`unit` | `comparator` | N Einheiten gesamt (ganze Karte). |
|
||||
| `kill` | `player`,`amount`,`unit` | `comparator` | N getötete Einheiten. |
|
||||
| `deaths` | `player`,`amount`,`unit` | `comparator` | N Death-Count eines Unit-Typs. |
|
||||
| `accumulate` | `player`,`amount`,`resource` | `comparator` | N Ressource (`ORE`/`GAS`/`ORE_AND_GAS`; triggers.py:154–156). |
|
||||
| `opponents` | `player`,`amount` | `comparator` | N verbleibende Gegner. |
|
||||
|
||||
### A.4 Unterstützte Trigger-ACTIONS (exakte `type`-Strings)
|
||||
|
||||
`amount` Default `1` (`0` = „Alle" bei Unit-Aktionen; triggers.py:184–186). `modifier` Default `SET_TO` (`SET_TO`/`ADD`/`SUBTRACT`; triggers.py:195–197). Alle `type`-Strings entsprechen exakt `build_action` (triggers.py:305–393).
|
||||
|
||||
| `type` | Pflichtfelder | Optional | Bedeutung |
|
||||
|---|---|---|---|
|
||||
| `display_text` | `text` | — | Display Text Message. |
|
||||
| `set_mission_objectives` | `text` | — | Set Mission Objectives (In-Game-Ziele). |
|
||||
| `play_wav` | `wav_path` | `duration_ms` | Play WAV (`wav_path` = Return von `sc_embed_wav`). |
|
||||
| `create_unit` | `player`,`unit`,`location` | `amount` | Create Unit. |
|
||||
| `kill_unit_at_location` | `player`,`unit`,`location` | `amount` | Kill Unit at Location (mit Death-Count). |
|
||||
| `remove_unit_at_location` | `player`,`unit`,`location` | `amount` | Remove Unit (lautlos). |
|
||||
| `move_unit` | `player`,`unit`,`location`,`destination` | `amount` | Move Unit (Quelle→Ziel). |
|
||||
| `give_units` | `player`,`target_player`,`unit`,`location` | `amount` | Give Units to Player. |
|
||||
| `set_resources` | `player`,`amount`,`resource` | `modifier` | Set Resources. |
|
||||
| `set_deaths` | `player`,`unit`,`amount` | `modifier` | Set Death Counts (Integer-Variable; Pflichtfelder `player`/`unit`/`amount`, triggers.py:358–364). |
|
||||
| `set_countdown_timer` | `seconds` | `modifier` | Set Countdown Timer. |
|
||||
| `pause_timer` / `unpause_timer` | — | — | Timer pausieren/fortsetzen. |
|
||||
| `run_ai_script` | `ai_script` | — | Run AI Script (ganze Karte). |
|
||||
| `run_ai_script_at_location` | `ai_script`,`location` | — | Run AI Script at Location (Wellen/Angriffe). |
|
||||
| `center_view` | `location` | — | Center View (Kamera). |
|
||||
| `minimap_ping` | `location` | — | Minimap Ping. |
|
||||
| `set_alliance_status` | `player`,`alliance_status` | — | Set Alliance Status. Werte `ENEMY`/`ALLY`/`ALLIED_VICTORY` stammen aus dem RichChk-Enum `AllianceStatus` (importiert, enums.py:20) und der Docstring (triggers.py:49, 207) — also eine dokumentierte, nicht direkt aus diesen Dateien enumerierbare Angabe. |
|
||||
| `victory` / `defeat` | — | — | Mission gewonnen/verloren. |
|
||||
|
||||
**`ai_script`:** entweder bekannter Name (z. B. `JUNKYARD_DOG`) oder exakt 4-stelliger Rohcode (z. B. `TMCx`). Sonst `ValueError` (triggers.py:220–233).
|
||||
|
||||
**Wert-Parsing (`enums.resolve`):** alle `player`/`unit`/`comparator`/`modifier`/`resource`/`alliance_status` akzeptieren Identifier (`TERRAN_MARINE`), Display-Name (`Terran Marine`) oder Numeric-ID (`0`) — case-insensitiv, getrimmt, **exakt** (kein Fuzzy). Der Index mappt Identifier, Display-Name und `str(member.id)` (enums.py:38–40). `player` umfasst u. a. `PLAYER_1..8`, `ALL_PLAYERS`, `FORCE_1..4`, `FOES`, `CURRENT_PLAYER`.
|
||||
|
||||
### A.5 WAV-Embedding (zweiphasig)
|
||||
|
||||
1. **`sc_embed_wav` (Registrierung):** In-MPQ-Pfad fest = `staredit\wav\<basename>`; registriert String in WAV-Sektion und queued `(disk_path, in_mpq)` in `pending_wavs` (Dedup nach `in_mpq` via `if not any(...)` → gleicher Basename kollidiert, erster Disk-File gewinnt; workspace.py:167–172). **Keine Existenzprüfung der Bytes in `embed_wav` selbst** — die Existenzprüfung liegt im Tool (`os.path.exists`, server.py:408).
|
||||
2. **`sc_save_map` (physisch):** kopiert `pending_wavs` per StormLib in die Ausgabe-MPQ und `compact_archive` (workspace.py:192–209). Bis dahin existieren keine Audiobytes, nur der String-Tabelleneintrag.
|
||||
|
||||
### A.6 Location-Geometrie
|
||||
|
||||
- Die **Arithmetik** verwendet `center_x/y`, `width/height` **roh** als Koordinaten (workspace.py:108–113); es gibt **keine px-pro-Tile-Konstante in der Rechenlogik**. **Die Tool-Doku dokumentiert die Konvention aber explizit:** `sc_create_location` beschreibt „Koordinaten in Pixeln (32 px = 1 Kachel)" und die Parameter sind als „in Pixeln" dokumentiert (server.py:173–174). Der Code ist also **nicht** stumm zur 32-px-Konvention; nur die Geometrie-Rechnung klammert nicht an ein Raster. Tool-Defaults `width=height=96`.
|
||||
- `half_w = max(1, width//2)`, `half_h = max(1, height//2)`; `left_x1/top_y1 = max(0, center−half)` (auf ≥0 geklammert), `right_x2/bottom_y2 = center+half` (**nicht** nach oben geklammert → kann Kartenrand überschreiten). Ungerade Größen schrumpfen durch Integer-Division.
|
||||
- **Location-Namen: exakt, case-sensitiv** (kein toleranter Resolver, anders als Enums — `resolve_location` nutzt schlichte Dict-Membership `if name in self.locations`, workspace.py:87). Wirft bei Fehlschlag mit Liste verfügbarer Namen. Trigger, die Locations referenzieren, brauchen diese **vorher**.
|
||||
|
||||
### A.7 Verifizierte Ende-zu-Ende-Pipeline (selftest)
|
||||
|
||||
`sc_list_maps → sc_create_location → sc_add_trigger (display_text + create_unit, always) → sc_save_map (.scx) → unabhängiges Reload via RichChk` bestätigt, dass Location „Basis" und beide Action-Typen den MPQ-Roundtrip überleben. Das ist der bewiesene Kernpfad. *(Die selftest-Beschreibung ist hier als Domänen-/Beispielpfad referenziert; `selftest.py` zählt nicht zu den vier autoritativen Quelldateien.)*
|
||||
|
||||
### A.8 Bemerkenswert / planungsrelevant
|
||||
|
||||
- `preserve=True` maskiert den „mind. eine Aktion"-Check (PreserveTrigger zählt; server.py:327–330) → leerer-Aktions-Trigger mit `preserve=True` wird erstellt, ist aber nutzlos.
|
||||
- 8-Player-Cap in `sc_describe_map` ist hart (`range(8)`, server.py:86); Force-Ops nur FORCE_1..4.
|
||||
- Workspace-Cache nur nach Basename (workspace.py:225) → zwei gleichnamige Dateien in verschiedenen Verzeichnissen aliasen.
|
||||
- `sc_save_map` setzt Edits/`pending_wavs` **nicht** zurück (workspace.py:176–210); erneutes Speichern wiederholt das WAV-Embed.
|
||||
- **Switch-Infrastruktur teilweise vorhanden, aber nicht erreichbar:** Das Enum `SwitchState` und der Resolver `resolve_switch_state` existieren in `enums.py` (Zeilen 23, 94–95). Es ist jedoch **keine** Switch-Condition und **keine** Switch-Action in `build_condition`/`build_action` verdrahtet — Switches sind daher über `sc_add_trigger` **nicht ansprechbar** (siehe C). Korrekt formuliert: das Switch-Enum + Resolver sind im Codebestand vorhanden, nur das Trigger-Wiring fehlt.
|
||||
|
||||
---
|
||||
|
||||
## B) Was eine vollständige SC:BW-Kampagne braucht
|
||||
|
||||
### B.1 Kampagnen-Ebene (campaign-level)
|
||||
|
||||
- **Arc-Struktur.** Bewährtes Blizzard-Template: Episode = **7–12 Missionen** (Standalone), Trilogie ≈ **3×10**. Eine Episode = ein Akt mit eigenem Protagonisten, die Missionen verketten zur Saga. Brood War macht Missionen bewusst **strategischer/weniger linear** als Vanilla.
|
||||
- **Schwierigkeitskurve & Story-Beats.** Klassischer Rhythmus: (1) Tutorial/Helden-only ohne Ökonomie → (2–3) sanfter Base-Build, je 1 Unit-Typ → (Mitte) „Twist"-Mission mit Spezialregeln → (7–9) Eskalation (mehrere Basen, Ally-AI, Timer, defend-then-counter) → (Finale) Set-Piece. Höhere Schwierigkeit fügt **Verhalten** hinzu (mehr Wellen, engere Timer, aggressivere AI), nicht nur HP.
|
||||
- **Story-Vermittlung.** Tragende Handlung gehört in **gescriptete Szenen** (Briefing/Debrief/Cutscene). In-Mission-Zeilen tragen Flavour + Zielhinweise, nicht den Kernplot.
|
||||
- **Mission-Verkettung.** `Set Next Scenario` kettet Karten (Kampagnenfortschritt).
|
||||
- **Globale Konsistenz:** Player/Force-Setup, Rassen-Zuordnung, Ressourcen-Startwerte, Allianzen, Namen/Texte, Audio-Budget über alle Missionen.
|
||||
|
||||
### B.2 Missions-Ebene (per-mission)
|
||||
|
||||
- **Konzept & Rolle im Arc** (welcher Beat, ein neues Konzept).
|
||||
- **Objectives:** ein Pflichtziel (treibt `Victory`), explizite `Defeat`-Bedingungen, 0–2 optionale Ziele mit Belohnung; Just-in-time enthüllt.
|
||||
- **Objective-Surfacing über drei Kanäle:** (1) Briefing-Objective-Box (MBRF), (2) In-Game-Liste via **Set Mission Objectives**, (3) On-Screen via **Display Text** + **Transmission** + **Minimap Ping**/**Center View**.
|
||||
- **Trigger-Systeme:** Win/Lose-Trigger, Gegner-AI (`Run AI Script (At Location)`), Wellen (`Create Unit`+Timer+Preserve), Helden (`Create Unit With Properties`), **Hyper Triggers** für feine Taktung, **Switches/Death-Counter** als State/Variablen, benannte Locations.
|
||||
- **Story-Präsentation:** Pre-Mission-**Briefing** (Show Portrait → Speaking Portrait/Transmission → Play WAV → Text → **Wait**), In-Mission-**Transmission** (Talking Portrait + WAV + Text + Center), Cutscenes (Center View + Create/Remove + Transmission + Wait), Debrief-Hook.
|
||||
- **Polish & Test:** Win- und jeden Lose-Pfad testen; Timing prüfen (keine Wait-Blocks, kein 2-Sekunden-Stottern); günstige Conditions zuerst.
|
||||
|
||||
### B.3 Mission-Archetypen (Win/Lose + Trigger-Bedarf)
|
||||
|
||||
| # | Archetyp | Win | Lose | Trigger-Bedarf |
|
||||
|---|---|---|---|---|
|
||||
| 1 | Destroy-all (Base-Build) | alle Gegner weg | keine Gebäude | `opponents==0` / `command/deaths(enemy)==0` → `victory`; Default „no buildings = defeat" |
|
||||
| 2 | Defend / Survive-timed | Timer abgelaufen | Schlüsselobjekt zerstört | `set_countdown_timer`; Wellen `create_unit`+`run_ai_script_at_location` (always+preserve); `deaths(protected)≥1`→`defeat` |
|
||||
| 3 | Escort | VIP an Exit (`bring`) | VIP tot | `bring(VIP,exit)`→`victory`; `deaths(VIP)≥1`→`defeat` |
|
||||
| 4 | Hero-only / RPG | Ziel erreicht/Target tot | Held tot | kein Base-Build; Create-with-Properties (HP/Invinc); `bring`/`kill`; viel Transmission |
|
||||
| 5 | Hunt / Find | Target gefunden/getötet | optional Timer/Heldentod | `bring(any,secret)`; `minimap_ping`+`center_view`; Switch „found" |
|
||||
| 6 | Gather-resources | `accumulate≥X` | Base verloren/Timer | `accumulate`; optional Harass |
|
||||
| 7 | Rescue / Hold | Allies befreit / Location T s gehalten | Allies sterben | `bring`→`give_units`; Command-the-Most-at-Location über Zeit |
|
||||
| 8 | Assassination | `deaths(target)≥1` | Armee weg | `deaths`/`kill` auf eindeutiges Target |
|
||||
| 9 | Puzzle / Spezialregeln | Sequenz fertig | Fehlaktion | Switch-State-Machine; `bring`/`deaths`-Gates |
|
||||
| 10 | Defend-then-counter (Finale) | Phase 1 überleben → Destroy-all | Base in einer Phase weg | Phasen-Flag (Switch/Timer); AI defensiv→passiv flippen |
|
||||
|
||||
---
|
||||
|
||||
## C) Gap-Analyse: Kampagnen-Bedarf ↔ Tool-Fähigkeiten
|
||||
|
||||
Legende: ✅ direkt unterstützt · 🟉 (Workaround) · ❌ nicht möglich mit den `sc_`-Tools.
|
||||
|
||||
| Bedarf (SC:BW-Domäne) | Status | Wie / Workaround / unmöglich |
|
||||
|---|---|---|
|
||||
| Karten auflisten/inspizieren | ✅ | `sc_list_maps`, `sc_describe_map`, `sc_list_locations`, `sc_list_triggers`. |
|
||||
| Locations anlegen/umbenennen | ✅ | `sc_create_location`/`sc_rename_location`. **Achtung:** Geometrie rechnet roh (kein Raster-Clamp), die Tool-Doku nennt aber die Konvention 32 px = 1 Kachel; Namen case-sensitiv & exakt. |
|
||||
| Player-Typ/Rasse/Force/Force-Namen | ✅ | `sc_set_player_setup` (OWNR/SIDE/FORC). Nur FORCE_1..4; `player`-Key Pflicht. |
|
||||
| Allianzen setzen | ✅ | Action `set_alliance_status` (`ENEMY`/`ALLY`/`ALLIED_VICTORY`). |
|
||||
| Ressourcen-Start setzen | ✅ | Action `set_resources` (`SET_TO`/`ADD`/`SUBTRACT`). |
|
||||
| Win/Lose | ✅ | `victory`/`defeat` + Conditions `opponents`/`command`/`deaths`/`bring`/`accumulate`/`elapsed_time`/`countdown_timer`. |
|
||||
| In-Game-Ziele | ✅ | Action `set_mission_objectives`. |
|
||||
| On-Screen-Text | ✅ | Action `display_text`. |
|
||||
| Einheiten erstellen/töten/entfernen/bewegen/geben | ✅ | `create_unit`,`kill_unit_at_location`,`remove_unit_at_location`,`move_unit`,`give_units`. |
|
||||
| Wellen / Reinforcements | ✅ | `create_unit` + Timer-Condition + `preserve=True`; Aggression via `run_ai_script_at_location`. |
|
||||
| Gegner-/Ally-AI | ✅ | `run_ai_script`, `run_ai_script_at_location` (Name oder 4-Char-Code). |
|
||||
| Timer-Logik | ✅ | `set_countdown_timer`/`pause_timer`/`unpause_timer`/`countdown_timer`/`elapsed_time`. |
|
||||
| Kamera / Minimap | ✅ | `center_view`, `minimap_ping`. |
|
||||
| WAV einbetten + abspielen | ✅ | `sc_embed_wav` → `wav_path` → Action `play_wav` (+`duration_ms`); WAV muss vorher in `MAPS_DIR` liegen. Format-Pflege (PCM 16-bit mono 11025 Hz, keine Leerzeichen) liegt beim Autor — Tool prüft nur Existenz. |
|
||||
| **In-Game-Transmission (Talking Portrait + WAV + Text + Center, getimt)** | 🟉 **Workaround** | **Keine native `transmission`-Action.** Komponieren aus `play_wav` + `display_text` + `center_view` (+`minimap_ping`) — exakt dieser Workaround ist auch in der Docstring dokumentiert (triggers.py:53–56). **Ohne animiertes Portrait und ohne echte synchronisierte Halte-Dauer**: `display_text`/`play_wav` feuern sofort; Synchronisation/„Hold" ist nicht erzwingbar (kein `wait`-Primitiv exponiert). `duration_ms` an `play_wav` steuert nur die WAV-Länge. |
|
||||
| **Talking Portrait (animiertes Gesicht)** | ❌ | Kein Action-Typ vorhanden. Nicht reproduzierbar mit den `sc_`-Tools. |
|
||||
| **Pre-Mission-Briefing (MBRF-Sektion)** | ❌ | Tools bearbeiten nur MRGN (Locations), OWNR/SIDE/FORC (Player), TRIG (Trigger), WAV, STR (Force-Namen via RichString) — **keine MBRF-API**. Briefing-Room (Show Portrait/Speaking Portrait/Wait/Objectives-Box) **nicht erstellbar**. Notbehelf: In-Mission-„Intro" zu Spielbeginn via `always`+`display_text`/`play_wav`/`center_view` — kein echter Briefing-Screen. |
|
||||
| **Switches (256 benannte Booleans, Set/Clear/Toggle/Randomize)** | ❌ | Weder Condition `switch` noch Action `set_switch` in `build_condition`/`build_action` verdrahtet → über `sc_add_trigger` **nicht ansprechbar**, kein Sequencing/OR-Emulation/Randomize über Switches. *(Hinweis zur Genauigkeit: Das Enum `SwitchState` + der Resolver `resolve_switch_state` existieren bereits in `enums.py`:23,94–95 — die Switch-Infrastruktur ist also nicht vollständig abwesend, es fehlt nur das Trigger-Wiring. Praktisch bleibt Switch-Funktionalität dennoch unerreichbar.)* |
|
||||
| State-Machine / Phasen-Flags / Zähler | 🟉 **Workaround** | Über **Death-Counter** emulierbar: Condition `deaths` (lesen, triggers.py:275–281) + Action `set_deaths` (`SET_TO`/`ADD`/`SUBTRACT`, triggers.py:358–364). Damit Phasen-Flags/Zähler/Timer. Erfordert „Marker"-Unit-Disziplin; kein `switch`-Komfort. |
|
||||
| **Hyper Triggers (feine Taktung ~12 Hz)** | 🟉 **Workaround/eingeschränkt** | Kein `wait`-Action und kein dedizierter Hyper-Trigger-Helper. Klassische 63×Wait(0)-Konstruktion **nicht baubar**. Engine läuft sonst ~alle 2 s. „Complex Hyper" über Death-Counter-Bedingungen theoretisch nachbaubar, aber umständlich; **flüssige Cutscene-Taktung praktisch nicht erreichbar**. |
|
||||
| `Wait`-Action (Dramaturgie-Pausen, Sync) | ❌ | Nicht exponiert. Timing nur grob über `elapsed_time`/`countdown_timer`/Death-Counter. |
|
||||
| **Set Next Scenario (Missionsverkettung)** | ❌ | Kein Action-Typ. Kampagnen-Verkettung **nicht innerhalb der Karte** setzbar; muss extern/manuell gelöst werden. |
|
||||
| Create Unit **With Properties** (HP/Invinc/Energy/Cloak) | ❌ | Nur schlichtes `create_unit` (Anzahl/Typ/Location, triggers.py:314–320). Helden-Properties nicht setzbar. Teil-Workaround: HP via separate Modify-Action — **auch nicht vorhanden** (kein `modify_unit_hp`). |
|
||||
| Modify Unit HP/Energy/Shields/Hangar | ❌ | Keine entsprechende Action. |
|
||||
| Order / Move Location / Set Doodad State / Set Invincibility | ❌ | Nicht exponiert. (Bewegung nur via `move_unit` Teleport.) |
|
||||
| Leaderboard, Score, Highest/Most/Least-Familie, Kill/Command-the-Most | ❌ | Nicht in den unterstützten Conditions/Actions. |
|
||||
| Mute/Unmute Unit Speech | ❌ | Nicht vorhanden (Default-Unit-Sounds nicht stummschaltbar). |
|
||||
| Mehrere Trigger atomar / Reihenfolge garantieren | 🟉 | `sc_add_trigger` fügt je 1 Trigger **ans Ende** an → Reihenfolge = Einfügereihenfolge; bewusst sequenzieren. |
|
||||
| Terrain / Doodads / Einheiten-Vorplatzierung | ❌ | **Außerhalb des Scopes** — Karten müssen Terrain/Startobjekte bereits enthalten. |
|
||||
|
||||
**Kernbotschaft der Gap-Analyse:** Logik-, Einheiten-, Ressourcen-, AI-, Timer-, Sound- und Kamera-Bausteine sind solide abgedeckt. **Vier echte Lücken** prägen das Template-Design: (1) **kein MBRF-Briefing**, (2) **keine native Transmission / kein Talking Portrait**, (3) **keine erreichbaren Switches & keine Hyper/Wait-Taktung** (State nur per Death-Counter, Timing nur grob; Switch-Enum/Resolver existieren zwar in `enums.py`, sind aber nicht an Trigger gebunden), (4) **kein Set Next Scenario / keine Create-with-Properties / Modify-Unit**. Für all diese existieren entweder dokumentierte Workarounds (Transmission-Komposit, Death-Counter-State) oder die Funktion ist mit den `sc_`-Tools **nicht** umsetzbar.
|
||||
|
||||
---
|
||||
|
||||
## D) Empfohlene Vorlagen-Struktur
|
||||
|
||||
Konkrete, tool-verknüpfte Vorlage. Jeder Schritt nennt das exakte `sc_`-Tool und Kern-Parameter. Designprinzip: **alle Edits sammeln, einmal speichern** (Filename = Session). Reihenfolge respektiert Abhängigkeiten (Locations & WAVs **vor** Triggern, die sie referenzieren).
|
||||
|
||||
### D.1 Kampagnen-Header (einmal pro Kampagne festlegen)
|
||||
|
||||
```
|
||||
Kampagne: <Titel>
|
||||
Episode/Akt: <Name> · Geplante Missionen: 7–12 (Standalone) | 3×10 (Trilogie)
|
||||
Protagonist/Fraktion: <…> · Rasse: TERRAN|ZERG|PROTOSS
|
||||
Arc-Beats (pro Mission ein Eintrag):
|
||||
M1 Tutorial/Helden-only · M2–3 Base-Build · Mitte Twist · 7–9 Eskalation · Finale Set-Piece
|
||||
Globale Konventionen:
|
||||
- Basis-Karten in SC_MAPS_DIR (1 Basis kann mehrere Missionen erzeugen)
|
||||
- Player/Force-Schema (PLAYER_1..8, FORCE_1..4) konsistent über alle Missionen
|
||||
- Ressourcen-Startwerte, Allianz-Defaults
|
||||
- Audio: PCM 16-bit mono 11025 Hz, Dateinamen OHNE Leerzeichen, in MAPS_DIR ablegen
|
||||
- Naming: Location-Namen case-sensitiv & exakt; einmal festlegen
|
||||
- Location-Koordinaten in Pixeln (Tool-Konvention 32 px = 1 Kachel); Rand beachten (right/bottom nicht geklammert)
|
||||
WORKAROUND-/LÜCKEN-Hinweise (siehe C):
|
||||
- Kein MBRF-Briefing → Intro als In-Mission-Sequenz (always + display_text/play_wav/center_view)
|
||||
- Keine Transmission → play_wav + display_text + center_view (kein Portrait, kein echter Hold)
|
||||
- Keine erreichbaren Switches → State/Phasen über deaths/set_deaths (Marker-Unit-Disziplin)
|
||||
- Keine Hyper/Wait → Timing nur grob via elapsed_time/countdown_timer/Death-Counter
|
||||
- Kein Set Next Scenario → Missionsverkettung extern lösen
|
||||
```
|
||||
|
||||
### D.2 Wiederholbarer Per-Mission-Block (Checkliste mit exakten Tool-Calls)
|
||||
|
||||
> **Konvention:** `<map>` = Basis-Kartendatei (Session-Key). Erst sammeln, am Ende `sc_save_map`. Bei Fehlversuch `sc_reset_map(<map>)`.
|
||||
|
||||
**Schritt 0 — Bestandsaufnahme**
|
||||
- [ ] `sc_describe_map(map=<map>)` — Tileset, Größe, 8 Player, Forces, Locations, Trigger, `pending_wav_embeds` prüfen.
|
||||
- [ ] `sc_list_locations(map=<map>)` und `sc_list_triggers(map=<map>)` — vorhandenen Stand erfassen.
|
||||
- [ ] *(Neustart sauber?)* `sc_clear_triggers(map=<map>)` **nur** wenn die Basis-Trigger ersetzt werden sollen (DESTRUCTIVE; nur per `sc_reset_map` rückholbar).
|
||||
|
||||
**Schritt 1 — Player- & Force-Setup**
|
||||
- [ ] `sc_set_player_setup(map=<map>, players=[{"player":"PLAYER_1","type":"HUMAN","race":"TERRAN","force":"FORCE_1"}, {"player":"PLAYER_2","type":"COMPUTER","race":"ZERG","force":"FORCE_2"}, …], force_names=[{"force":"FORCE_1","name":"<Allianz>"}])`
|
||||
- jeder Eintrag braucht `player` (Pflicht); nur FORCE_1..4.
|
||||
|
||||
**Schritt 2 — Locations (vor Triggern!)**
|
||||
- [ ] Pro benötigter Stelle: `sc_create_location(map=<map>, name="<eindeutig>", center_x=<px>, center_y=<px>, width=96, height=96)`
|
||||
- Koordinaten in Pixeln (Tool-Konvention 32 px/Tile); `right_x2/bottom_y2` werden nicht geklammert → Kartenrand beachten.
|
||||
- [ ] Umbenennen falls nötig: `sc_rename_location(map=<map>, old_name=…, new_name=…)`.
|
||||
|
||||
**Schritt 3 — Audio (vor `play_wav`-Triggern)**
|
||||
- [ ] WAV-Datei in `MAPS_DIR` ablegen (PCM 16-bit mono 11025 Hz, kein Leerzeichen im Namen).
|
||||
- [ ] `sc_embed_wav(map=<map>, wav_filename="<funk1.wav>")` → Rückgabe `wav_path` = `staredit\wav\funk1.wav` notieren.
|
||||
|
||||
**Schritt 4 — Mission-Init-Trigger (einmalig zu Spielbeginn)**
|
||||
- [ ] `sc_add_trigger(map=<map>, players=["ALL_PLAYERS"], conditions=[{"type":"always"}], actions=[`
|
||||
`{"type":"set_mission_objectives","text":"<Pflichtziel>"},`
|
||||
`{"type":"set_resources","player":"PLAYER_1","resource":"ORE_AND_GAS","amount":<n>,"modifier":"SET_TO"},`
|
||||
`{"type":"set_alliance_status","player":"FORCE_2","alliance_status":"ENEMY"},`
|
||||
`{"type":"run_ai_script","ai_script":"<SCRIPT_ODER_4CHAR>"},`
|
||||
`{"type":"set_countdown_timer","seconds":<n>,"modifier":"SET_TO"},`
|
||||
`{"type":"create_unit","player":"PLAYER_1","unit":"TERRAN_MARINE","location":"<Basis>","amount":4},`
|
||||
`{"type":"center_view","location":"<Basis>"}], preserve=False)`
|
||||
- **`preserve=False`** für einmalige Init (sonst feuert er erneut).
|
||||
|
||||
**Schritt 5 — Intro-„Briefing"-Ersatz (Workaround, da kein MBRF)**
|
||||
- [ ] `sc_add_trigger(map=<map>, players=["PLAYER_1"], conditions=[{"type":"always"}], actions=[`
|
||||
`{"type":"center_view","location":"<Szene>"},`
|
||||
`{"type":"play_wav","wav_path":"staredit\\wav\\funk1.wav","duration_ms":<ms>},`
|
||||
`{"type":"display_text","text":"<Intro-Zeile>"},`
|
||||
`{"type":"minimap_ping","location":"<Ziel>"}], preserve=False)`
|
||||
- Kein Portrait, keine echte Sync/Hold-Garantie (siehe C). Mehrere kurze Zeilen statt langer Reden.
|
||||
|
||||
**Schritt 6 — Objective-/Wellen-/Phasen-Trigger (archetyp-spezifisch)**
|
||||
- [ ] **Wellen:** `sc_add_trigger(players=["PLAYER_2"], conditions=[{"type":"elapsed_time","seconds":<t>,"comparator":"AT_LEAST"}], actions=[{"type":"create_unit","player":"PLAYER_2","unit":"<Zerg>","location":"<Spawn>","amount":6},{"type":"run_ai_script_at_location","ai_script":"<ATTACK_SCRIPT>","location":"<Ziel>"}], preserve=True)`
|
||||
- [ ] **Phasen-Flag via Death-Counter** (Switch-Ersatz): zum Setzen `actions=[{"type":"set_deaths","player":"PLAYER_1","unit":"<MARKER_UNIT>","amount":1,"modifier":"SET_TO"}]`; zum Lesen `conditions=[{"type":"deaths","player":"PLAYER_1","unit":"<MARKER_UNIT>","amount":1,"comparator":"AT_LEAST"}]`.
|
||||
- [ ] **Optionale Ziele:** eigener Trigger, der bei Erfüllung `give_units`/`set_resources` belohnt und `set_mission_objectives` aktualisiert.
|
||||
|
||||
**Schritt 7 — Win/Lose (immer explizit, je `preserve=True`)**
|
||||
- [ ] **Win:** `sc_add_trigger(players=["PLAYER_1"], conditions=[{"type":"opponents","player":"PLAYER_1","amount":0,"comparator":"AT_MOST"}], actions=[{"type":"display_text","text":"Mission erfüllt"},{"type":"victory"}], preserve=True)`
|
||||
- Archetyp-Varianten: `bring(VIP,exit)` (Escort), `deaths(target)≥1` (Assassination), `accumulate≥X` (Gather), `countdown_timer≤0`/`elapsed_time` (Survive).
|
||||
- [ ] **Lose:** `sc_add_trigger(players=["PLAYER_1"], conditions=[{"type":"deaths","player":"PLAYER_1","unit":"<VIP_ODER_KEY_STRUCTURE>","amount":1,"comparator":"AT_LEAST"}], actions=[{"type":"defeat"}], preserve=True)`
|
||||
- Plus ggf. Timer-Lose / „no buildings"-Lose.
|
||||
|
||||
**Schritt 8 — Kontrolle vor dem Speichern**
|
||||
- [ ] `sc_list_triggers(map=<map>)` — Reihenfolge, Player, Conditions/Actions gegen den Plan prüfen.
|
||||
- [ ] `sc_describe_map(map=<map>)` — `pending_wav_embeds` enthält alle erwarteten WAVs? Locations vollständig?
|
||||
- [ ] Falsch eingefügt? `sc_remove_trigger(map=<map>, index=<i>)` (0-basiert).
|
||||
|
||||
**Schritt 9 — Persistieren (eine neue Missionsdatei)**
|
||||
- [ ] `sc_save_map(map=<map>, output_name="mission<NN>.scx", overwrite=False)`
|
||||
- Rückgabe prüfen: `embedded_wavs`, `triggers`, `locations`. Bei `FileExistsError` neuen Namen oder `overwrite=True`.
|
||||
- **Hinweis:** Endung `.scx` wird nur ergänzt, wenn der Name nicht schon auf `.scx`/`.scm` endet (ein `.scm` bleibt erhalten). Erneutes `sc_save_map` bettet `pending_wavs` erneut ein und nutzt denselben In-Memory-Stand (kein Reset durch Save).
|
||||
|
||||
### D.3 Per-Mission-Designcheck (vor Schritt 9 abhaken)
|
||||
|
||||
- [ ] Genau **ein** Pflichtziel → treibt `victory`; Text in `set_mission_objectives`.
|
||||
- [ ] **Alle Lose-Pfade** verdrahtet (Held/VIP-Tod, Timer, Key-Structure).
|
||||
- [ ] Gegner-AI gesetzt (`run_ai_script`/`_at_location`), Aggression passend zum Beat.
|
||||
- [ ] Wellen-Trigger `preserve=True`; Init-Trigger `preserve=False`.
|
||||
- [ ] Locations für jedes `create_unit`/`move_unit`/`bring`/`center_view` existieren und exakt benannt.
|
||||
- [ ] WAVs vorab in `MAPS_DIR`, `sc_embed_wav` aufgerufen, `wav_path` in `play_wav` korrekt (`staredit\wav\…`).
|
||||
- [ ] Bewusst: **kein** echtes Briefing/Transmission/Portrait, **keine erreichbaren** Switches, **keine** Hyper-Taktung — Workarounds dokumentiert eingesetzt.
|
||||
|
||||
### D.4 Kampagnen-Abschluss-Checkliste (closeout)
|
||||
|
||||
- [ ] Alle Missionen als eigene `.scx`/`.scm` in `MAPS_DIR` gespeichert; Namensschema `mission01..NN` konsistent.
|
||||
- [ ] Player/Force/Rassen-Schema über alle Missionen identisch (per `sc_describe_map` stichprobenartig gegengeprüft).
|
||||
- [ ] Audio-Budget gesamt geprüft (WAV-Anzahl/Größe je Karte; PCM-Spec eingehalten).
|
||||
- [ ] Jede Mission: Win- **und** jeder Lose-Pfad manuell im Spiel getestet (Timing grob, da keine Hyper-Trigger).
|
||||
- [ ] **Missionsverkettung extern gelöst** (kein `Set Next Scenario` über die Tools) — Reihenfolge/Übergänge dokumentiert.
|
||||
- [ ] Briefing-Ersatz (In-Mission-Intro) in jeder Mission vorhanden, da MBRF nicht editierbar.
|
||||
- [ ] Backup der Basis-Karten unverändert (Save erzeugt stets neue Dateien; Basis bleibt Template).
|
||||
- [ ] Bei Server-Neustart: In-Memory-Workspaces sind weg → finalisierte `.scx` sind die einzige Persistenz.
|
||||
|
||||
---
|
||||
|
||||
### Quellenbasis
|
||||
- **Code (autoritativ für A & Fähigkeitsaussagen):** `G:\Claude\Star-Edit\starcraft_mcp\server.py`, `triggers.py`, `enums.py`, `workspace.py`. *(Ergänzend, nicht als autoritativ geprüft: `__init__.py`, `selftest.py`, `requirements.txt`, `Dockerfile` — Aussagen zu Docker-Transport-Default, Python-Version und Dependency-Pins sind aus diesen vier Kerndateien nicht verifizierbar und entsprechend gekennzeichnet.)*
|
||||
- **Domäne (B/C-Kontext):** StarEdit Network Wiki (Triggers, List of Trigger Conditions/Actions, Mission Briefings, Hyper Triggers, Wait blocks, Death Counters, Wav Files, Scenario.chk), Campaign Creations Tutorials, StarCraft Fandom/Wikipedia (Episodenstruktur), StrategyWiki Walkthrough, RPGClassics Atrium Campaign Editor, PC Gamer (Blizzard-Storytelling).
|
||||
105
docs/research/TERRAIN-AUTOMATION.md
Normal file
105
docs/research/TERRAIN-AUTOMATION.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Feasibility-Report: Automatisierung der Terrain-Erstellung für die Star-Edit-MCP-Pipeline
|
||||
|
||||
> ## ✅ STATUS: Round-Trip-Gate bestanden (2026-06-22)
|
||||
> Die zentrale load-bearing Annahme (§3/§4) ist **empirisch belegt**: RichChk reicht eine `.scx`
|
||||
> **section-stabil** durch. Test `tools/terrain_roundtrip_test.py` an `data/maps/base-map.scx`
|
||||
> (256×256 Jungle): Lesen → unverändert Speichern → Neu-Lesen → **alle 38 CHK-Sections
|
||||
> positions-genau identisch**, inkl. `ISOM`/`MTXM`/`TILE`/`MASK`/`DIM`/`ERA` **und** der
|
||||
> Integritäts-Section `VCOD`; Locations/Trigger erhalten.
|
||||
> - **Caveat 1:** Die Ausgabe-Datei ist kleiner (74.509 vs. 103.346 Bytes) — reine **MPQ-Rekompression**, der CHK-*Inhalt* ist identisch.
|
||||
> - **Caveat 2 (offen):** Der finale „öffnet in **SCMDraft** + **StarCraft**"-Check ist **manuell** und steht noch aus.
|
||||
> - Damit ist der **Template-MVP (Ansatz 1)** entriskt; die 5er-Scores für Ansatz 1/2 gelten nun (vorbehaltlich Caveat 2).
|
||||
|
||||
## 1. Kernbefund
|
||||
|
||||
**Ja, Terrain-Erstellung lässt sich automatisieren — aber „automatisierbar" und „gutes Terrain" sind zwei völlig verschiedene Schwierigkeitsstufen. Die ehrliche Antwort: valides, ladbares, flaches Terrain ist trivial; nahtlos geblendetes, korrekt cliffed/pathbares Editor-Qualitäts-Terrain ist hart.**
|
||||
|
||||
Zwei tragende Fakten vorweg, klar ausgesprochen:
|
||||
|
||||
**(a) Schreibt RichChk tatsächlich Terrain-Sections? — JA (quellverifiziert).**
|
||||
Das ist der entscheidende Befund und korrigiert eine verbreitete Annahme. RichChk ist *nicht* auf Logik-Sections beschränkt. Die Basis-Transcoder-Protokoll-Klasse `RichChkSectionTranscoder` definiert sowohl `decode` (Read) *als auch* `encode` (Write) als verpflichtende abstrakte Methoden — Schreibfähigkeit ist also architektonisch garantiert. Es existieren vollwertige Read- *und* Write-Transcoder (low-level Byte `chk_*` + high-level „rich" `rich_*`) für **alle** im Entwurf genannten Terrain-Sections: `DIM` (Dimensionen), `ERA` (Tileset), `MTXM` (die tatsächlich vom Spiel gerenderte Megatile-Karte), `TILE`, `ISOM`, `MASK`, `THG2`, `DD2`.
|
||||
- `RichDimSection` exponiert `width`/`height`.
|
||||
- `RichEraSection.tileset` ist ein `StarCraftTileset`-Enum.
|
||||
- `RichMtxmSection.tiles` ist eine Liste editierbarer `RichTile`-Objekte (mit `id`/`group_index`/`subtile_index`, dekodiert aus dem `u16`) — also echte Tile-Platzierung, keine opaken Bytes. Verifiziert über `rich_mtxm_transcoder.py` (`encode()` baut `DecodedMtxmSection(_tiles=tuple(t.id for t in ...))`).
|
||||
- **Wichtige Einschränkung bei ISOM (quellverifiziert):** Der `rich_isom_transcoder.py` ist ein reiner **Pass-Through** — wörtlich im Docstring: „Simple pass-through: the rich representation is identical to the decoded one". RichChk kann ISOM-Daten verlustfrei lesen, erhalten, kopieren oder von Hand konstruieren — aber es gibt **keinen** Algorithmus, der ISOM aus MTXM/Brushes *generiert*. Das ist der Knackpunkt (siehe §4).
|
||||
|
||||
> Konsequenz: Die Behauptung „Star-Edit braucht eine handgebaute Basiskarte, weil RichChk kein Terrain kann" ist technisch falsch. RichChk *kann* Terrain-Bytes schreiben. **Aber „Bytes schreiben" ≠ „Terrain erzeugen":** RichChk schreibt nur Tile-/ISOM-Daten, die man bereits besitzen muss. Was fehlt, ist nicht die Schreibfähigkeit, sondern die **Terrain-Engine** (die ISOM→Tile-Auflösung), die SCMDraft im Hintergrund leistet.
|
||||
|
||||
**(b) Bietet SCMDraft eine CLI/Automatisierung? — NEIN (quellverifiziert: kein Automatisierungspfad).**
|
||||
SCMDraft 2 hat **keine** offizielle headless/Batch-Automatisierung. Belegt ist nur **ein** Start-Switch: `-profile=` (z. B. `-profile=default`), der lediglich den Profilauswahl-Bildschirm überspringt und trotzdem das GUI-Fenster öffnet. **Achtung (im Entwurf überzogen):** Die zusätzlich behaupteten Switches `-map=` und `-console` sind **nicht quellbelegt** — in den Primärquellen ließ sich kein Nachweis finden (die `-console`-Treffer betrafen StarCraft II, ein anderes Produkt). Sie sind als **unbestätigt** zu behandeln. An der Schlussfolgerung ändert das nichts: Keiner dieser Switches verarbeitet Karten headless. Das Plugin-System (`.sdp`-DLLs) lädt *innerhalb* der laufenden GUI und ist laut API „strictly designed to support TrigEdit" — also auf Trigger-/String-Table-Editierung beschränkt. Es gibt keinen dokumentierten Weg, eine `.scx` ohne manuelle GUI-Interaktion mit SCMDraft selbst zu erzeugen/speichern. Echte programmatische Bearbeitung geht nur über Tools, die SCMDraft komplett umgehen und das CHK-Format direkt schreiben.
|
||||
|
||||
**Fazit Kernbefund:** Die Automatisierung scheitert *nicht* an fehlenden Schreib-Tools (RichChk schreibt nachweislich alle Terrain-Bytes) und *nicht* an SCMDraft-CLI (die existiert nicht und wird auch nicht gebraucht). Sie steht und fällt allein damit, **woher die korrekten Tile-/ISOM-Daten kommen** — denn jeder MTXM-`u16` trägt untrennbar Grafik + Walkability + Höhe + Sicht + Bebaubarkeit, und nahtlose Übergänge sind ein Vier-Nachbarn-Constraint-Problem pro Tile gegen tileset-spezifische Regeltabellen. Genau diesen Constraint-Solver kodiert ISOM — und genau den liefert RichChk *nicht*.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ansätze im Vergleich
|
||||
|
||||
Bewertung 1–5 (5 = bestes Ergebnis in der jeweiligen Spalte). „Passung" = Integration in den bestehenden RichChk/Star-Edit-Stack.
|
||||
|
||||
**Wichtiger Vorbehalt zu den Bewertungen von Ansatz 1 und 2:** Die hohen Robustheits-/Passungs-Scores setzen voraus, dass RichChk eine von SCMDraft gebaute `.scx` **byte-stabil durchreicht** (inkl. `STR`-Rebuild und nicht-modellierter Sections). Diese Round-Trip-Eigenschaft ist **noch nicht empirisch getestet** (siehe §4). Bis dieser Test bestanden ist, sind die Scores für Ansatz 1/2 als **vorläufig** zu lesen.
|
||||
|
||||
| # | Ansatz | Aufwand (5=gering) | Qualität-Terrain | Robustheit | Passung-Stack | Verdict |
|
||||
|---|---|:---:|:---:|:---:|:---:|---|
|
||||
| 1 | **Template-/Stamp-Bibliothek**: kuratierte, in SCMDraft vorgebaute Basiskarten (mit korrektem ISOM+TILE+MTXM), die der MCP nur noch auswählt und mit Logik bespielt | 5 | 5 | 5* | 5* | **EMPFEHLUNG (MVP).** Löst das ISOM-Problem, indem es Editor-Qualität von Hand vorproduziert. Geringer Aufwand, höchste Qualität, robust, passt perfekt zum vorhandenen „Terrain durchreichen, Logik schreiben"-Vertrag. Einschränkung: nur endliche Auswahl, kein freies Generieren. *Robustheit/Passung vorbehaltlich Round-Trip-Test (§4). |
|
||||
| 2 | **Hybrid: Template-Stamping + RichChk-MTXM-Overlay** — vorgebaute Terrain-Bausteine (Plateaus, Rampen, Choke-Stücke) als Tile-Blöcke aus geprüften Karten extrahieren und per RichChk-MTXM zusammensetzen; ISOM aus denselben Quellen mitkopieren | 3 | 4 | 3 | 5* | **STARKE ZWEITWAHL / Ausbaustufe.** Mehr Freiheit als reine Templates, bleibt im Python/RichChk-Stack. Risiko: Block-Ränder müssen sauber zusammenpassen (Edge-Type-Matching), sonst Seams; ISOM-Konsistenz muss mitgepflegt werden. *Passung vorbehaltlich Round-Trip-Test (§4). |
|
||||
| 3 | **MCP via RichChk auf flaches/„square" Terrain erweitern** — DIM/ERA/MTXM direkt aus einem buildable+walkable Megatile-Group füllen | 4 | 2 | 4 | 5 | **Gut für einfache Fälle.** Trivial valid und ladbar, perfekte Stack-Passung. Aber: kein Blending, keine Cliffs/Rampen, brüchig beim Re-Edit in SCMDraft (stale ISOM überschreibt Tiles). Nur für Interiors/flache Melee-Böden. |
|
||||
| 4 | **Prozeduraler Generator → CHK (WaveFunctionDiffusion)** — Off-the-shelf-Tool, das laut Beschreibung echtes Terrain in eine `.scx` schreibt (DIM/ERA/MTXM/TILE/ISOM/MASK + MPQ-Packing) | 2 | 4 | 2 | 3 | **Interessant, aber riskant.** Soll „sieht aus wie Melee-Map"-Terrain inkl. ISOM/MASK erzeugen — **dieser Output-Anspruch (gültiges ISOM+MASK) ist hier NICHT verifiziert** (Tool-Source nicht inspiziert); vor Nutzung prüfen. Nachteile: seit 2023 unmaintained, schwere Deps (PyTorch/CUDA, Modell-Download), Output nicht garantiert balanced/spielbar, separater Stack neben RichChk. Output könnte aber als Quelle für Ansatz 1/2 dienen — sofern das ISOM/MASK valide ist. |
|
||||
| 5 | **Eigener ISOM-Generator in Python** (IsomTerrain-Algorithmus nachbauen: 14 Shapes/Typ, soft/hard Links, terrainTypeMap, Vier-Nachbarn-Matching, radiale Propagation) — danach via RichChk schreiben | 1 | 5 | 4 | 4 | **Höchste Qualität, höchster Aufwand.** Das ist faktisch das Neuimplementieren der SCMDraft-Terrain-Engine. Referenz (`TheNitesWhoSay/IsomTerrain`) ist genau deshalb ein eigenständiges C++-Projekt. Nur sinnvoll als Langzeit-Investition. |
|
||||
| 6 | **C++-Harness über ChkDraft `MappingCoreLib`** — deterministische, native Terrain-API als Bibliothek einbinden, eigenes CLI-Tool bauen | 2 | 5 | 4 | 2 | **Deterministische Profi-Option.** Vollwertige Terrain-Engine vorhanden, aber kein CLI/Scripting-Entry-Point — man muss C++ bauen und linken. Bricht aus dem Python-Stack aus. Sinnvoll, wenn parametrisches/balanciertes Terrain native gebraucht wird. |
|
||||
| 7 | **GUI-Automatisierung von SCMDraft** (pywinauto/AutoHotkey: Brush klicken, Save As) | 2 | 3 | 1 | 2 | **LETZTE WAHL — keine Produktionsoption.** Nur „Save As" ist robust automatisierbar; das eigentliche Terrain-Malen läuft über eine custom-gezeichnete Canvas ohne Control-Tree → hardcodierte Koordinaten + Bilderkennung, brüchig gegen Version/Auflösung/DPI/Theme, kein State-Readback, ISOM-Blending macht Klicks semantisch verlustbehaftet, „silently wrong"-Gefahr. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Empfehlung + MVP
|
||||
|
||||
### Empfohlener Pfad: Template-Stamp-Bibliothek (Ansatz 1), ausbaubar Richtung Hybrid (Ansatz 2)
|
||||
|
||||
Die Logik dahinter ist direkt aus dem Kernbefund abgeleitet: Das einzige wirklich harte Problem ist die **ISOM→Tile-Auflösung** (Editor-Qualitäts-Blending/Cliffs/Pathing). Statt diesen Solver nachzubauen (Ansatz 5/6) oder fragil zu erklicken (Ansatz 7), **vorproduziert man Editor-Qualität einmal von Hand in SCMDraft** und macht sie wiederverwendbar. RichChk kann die Terrain-Bytes nachweislich schreiben/durchreichen — man muss sie also nur aus geprüften Quellen beziehen.
|
||||
|
||||
Das passt exakt zum bestehenden Vertrag der Pipeline: *„Terrain-Stage schreibt Terrain-Sections + minimal valides Scaffold; MCP/RichChk liest die `.scx`, lässt Terrain-Sections unangetastet und schreibt die Logik-Sections."* RichChk reicht nicht-modellierte Sections ohnehin verlustfrei durch — Round-Trip-Korruption wird so vermieden.
|
||||
|
||||
**Erster verpflichtender Schritt (Gate vor dem Katalog):** Genau diese Round-Trip-Eigenschaft ist noch unverifiziert (§4). Bevor der Template-Katalog skaliert wird, muss an *einem* Template end-to-end belegt sein, dass RichChk eine SCMDraft-`.scx` byte-stabil durchreicht (inkl. `STR`-Rebuild und nicht-modellierter Sections). Erst nach bestandenem Test gelten die 5er-Scores für Ansatz 1.
|
||||
|
||||
### Minimaler erster Schritt (MVP), der sofort integriert und Wert liefert
|
||||
|
||||
1. **Round-Trip-Gate zuerst:** Eine einzelne, von Hand in SCMDraft gebaute `.scx` durch RichChk lesen → unverändert zurückschreiben → in SCMDraft und StarCraft öffnen. Bestätigt byte-stabiles Durchreichen (Terrain-Sections + `STR` + nicht-modellierte Sections), bevor in einen Katalog investiert wird.
|
||||
2. **Template-Katalog anlegen (einmalig, manuell in SCMDraft):** 3–6 saubere Basiskarten in den gängigen Größen/Tilesets bauen — z. B. 64×64 / 128×128 / 256×256 je in Badlands/Jungle, mit korrektem `ISOM`+`TILE`+`MTXM`+`MASK`. Diese als `.scx` ablegen (z. B. `templates\badlands_128.scx`).
|
||||
3. **MCP-Erweiterung „terrain_select":** Der MCP bekommt einen Schritt, der eine Template-`.scx` anhand von Parametern (Größe, Tileset, ggf. Spielerzahl) auswählt und als Arbeitskopie kopiert. Keine Terrain-Logik im Code — nur Dateiauswahl.
|
||||
4. **Bestehender Logik-Pfad unverändert:** Star-Edit/RichChk öffnet die Arbeitskopie und schreibt wie gehabt Trigger/Locations/Units/Sound. Terrain-Sections werden durchgereicht.
|
||||
5. **Verifikation:** Output-`.scx` einmal in SCMDraft öffnen und in StarCraft laden — bestätigt, dass Terrain intakt bleibt und Logik korrekt sitzt.
|
||||
|
||||
**Warum das schnell Wert liefert:** Es eliminiert genau den manuellen Schritt, der heute jede Pipeline-Nutzung blockiert (jedes Mal von Hand eine Basiskarte bauen), bei null neuem Terrain-Engine-Risiko. Die Qualität ist per Konstruktion Editor-Niveau — vorbehaltlich des bestandenen Round-Trip-Gates (Schritt 1).
|
||||
|
||||
**Erste Ausbaustufe (Ansatz 2):** Sobald der Katalog steht, einzelne geprüfte Bausteine (Plateau-, Rampen-, Choke-Blöcke) per RichChk-MTXM in die Templates einsetzen — mit mitkopiertem ISOM aus derselben Quelle. Das gibt parametrische Variation, ohne den vollen ISOM-Solver zu bauen.
|
||||
|
||||
---
|
||||
|
||||
## 4. Risiken & offene Fragen
|
||||
|
||||
**ISOM-Blending ist der harte Kern — und RichChk löst ihn nicht.**
|
||||
RichChk schreibt ISOM nur als Pass-Through (quellverifiziert: Docstring „Simple pass-through"). Wer Terrain *generiert* (statt es aus geprüften Quellen zu kopieren), muss ISOM selbst korrekt erzeugen: 14 Shape-Templates pro Terrain-Typ, soft/hard Directional Links (≤48 / >48), eine tileset-spezifische `terrainTypeMap` legaler Übergänge, Vier-Nachbarn-Matching mit radialer Propagation und Variant-Randomisierung. Das ist das Neuimplementieren der Editor-Engine (Referenz: `TheNitesWhoSay/IsomTerrain`, C++). **Offene Frage:** Lohnt sich das je, oder reicht dauerhaft Template+Stamp?
|
||||
|
||||
**MTXM-only ist brüchig beim Re-Edit.**
|
||||
Schreibt man MTXM/TILE ohne passendes ISOM (oder mit stale ISOM) und öffnet die Karte später in SCMDraft, regeneriert jeder isometrische Edit die Tiles aus dem (falschen/leeren) ISOM und **überschreibt das handplatzierte Terrain**. Konsequenz: **immer ISOM + TILE + MTXM konsistent zusammen schreiben** — was im Template-Ansatz automatisch erfüllt ist, im reinen RichChk-MTXM-Ansatz (Nr. 3) aber aktiv beachtet werden muss.
|
||||
|
||||
**Playability/Pathing/Buildability sind nicht frei wählbar.**
|
||||
Walkability und Höhe kommen aus VF4, Bebaubarkeit aus CV5-Group-Flags — beides hängt fest am gewählten Megatile-`u16`. Ein Tile kann wie Gras aussehen, aber als unbegehbar geflaggt sein. Naiv zusammengesetztes Terrain (Ansatz 2/3) riskiert: Einheiten laufen durch sichtbare Wände, Gebäude verweigern Platzierung, durchsichtige/exploitbare Cliffs, falscher High-Ground-Vorteil. **Bei Templates (Ansatz 1) ist das gelöst**, weil SCMDraft beim Bauen korrekte VF4/CV5-konsistente Tiles wählt — ein weiteres Argument für den Template-MVP.
|
||||
|
||||
**Balance/Spielbarkeit von generiertem Terrain.**
|
||||
WaveFunctionDiffusion (Ansatz 4) soll „sieht aus wie Melee" erzeugen — nicht garantiert competitive-balanced (Startpositionen, Ressourcenverteilung, faire Chokes). Zusätzlich ist hier **nicht verifiziert**, dass das Tool überhaupt valides ISOM+MASK ausgibt (Source nicht inspiziert) — vor jeder Nutzung zu prüfen. PSMAGE adressiert Balance, ist aber nur ein Paper, kein nutzbares Tool. **Offene Frage:** Welches Qualitätsniveau braucht der Use-Case wirklich — spielbare Sandbox oder turniertaugliche Map?
|
||||
|
||||
**MPQ-Packaging / ein Schreiber pro CHK.**
|
||||
Beide Stages mutieren letztlich eine CHK in einem MPQ via StormLib. Sauberster Vertrag: Terrain-Stage schreibt Terrain-Sections + minimales Scaffold, der MCP/RichChk lässt diese unangetastet und schreibt nur Logik. **Zwei Tools dürfen nicht dieselben Sections überschreiben** — sonst Round-Trip-Korruption. Bei reinem RichChk/Template-Pfad ist das unkritisch (ein Tool, ein Schreiber). Bei WaveFunctionDiffusion + RichChk muss die Section-Ownership strikt getrennt bleiben.
|
||||
|
||||
**Tileset-Abhängigkeit der Regeln.**
|
||||
ISOM-Shapes, Edge-Types und `terrainTypeMap` sind pro Tileset verschieden. Jeder selbstgebaute Generator/Stamp-Mechanismus muss pro Tileset gepflegt werden — ein Katalog pro Tileset (Ashworld, Badlands, Desert, Ice, Jungle, Platform, Twilight, Installation) statt eines universellen Solvers reduziert diesen Aufwand drastisch.
|
||||
|
||||
**Offene technische Verifikation (load-bearing):** Ob RichChk eine von SCMDraft gebaute `.scx` wirklich byte-stabil durchreicht (inkl. `STR`-Rebuild und nicht-modellierter Sections), ist die zentrale unbestätigte Annahme, auf der die Empfehlung ruht. Sie muss am konkreten Template einmal end-to-end getestet werden (siehe MVP-Schritt 1), bevor der Katalog skaliert wird. Bis dahin sind die Bewertungen für Ansatz 1/2 vorläufig.
|
||||
|
||||
---
|
||||
|
||||
### Quellen / Verifikation
|
||||
- **RichChk (quellverifiziert, master-Branch):** `richchk_section_transcoder.py` (decode+encode Pflicht), `rich_mtxm_transcoder.py` (editierbare `RichTile`), `rich_isom_transcoder.py` (Docstring „Simple pass-through"), Transcoder für `DIM/ERA/MTXM/TILE/ISOM/MASK/THG2/DD2` vorhanden. → https://github.com/sethmachine/richchk
|
||||
- **SCMDraft (quellverifiziert):** Commands-Seite = nur GUI-Eingaben, kein CLI; einziger belegter Switch `-profile=` (öffnet trotzdem GUI). → http://www.stormcoast-fortress.net/cntt/software/scmdraft/Commands/ , http://staredit.net/topic/6634/ . Plugin-API „strictly designed to support TrigEdit" → http://staredit.net/topic/16514/
|
||||
- **ISOM-Referenz-Engine:** `TheNitesWhoSay/IsomTerrain` (C++), ChkDraft `MappingCoreLib`.
|
||||
- **Korrektur aus adversarialer Prüfung:** SCMDraft-Switches `-map=`/`-console` sind **nicht** quellbelegt (nur `-profile=`); Scores für Ansatz 1/2 vorläufig bis Round-Trip-Test bestanden.
|
||||
Loading…
Add table
Add a link
Reference in a new issue