Benutzer-Werkzeuge

Webseiten-Werkzeuge


scripting:tutorials:level3:armies

Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen der Seite angezeigt.

Link zu der Vergleichsansicht

Beide Seiten, vorherige ÜberarbeitungVorherige Überarbeitung
Nächste Überarbeitung
Vorherige Überarbeitung
scripting:tutorials:level3:armies [2025/07/08 10:13] fritz_98scripting:tutorials:level3:armies [2025/07/11 14:55] (aktuell) – [Beispiel für komplexe Armeesteuerung] fritz_98
Zeile 13: Zeile 13:
 Falls du den Abschitt zu [[ scripting:tutorials:level2:bandit_camps#die_armee-basisparameter |Armee-Basisparametern]] schon kennst, kannst du diesen Abschnitt überspringen. Die Inhalte sind hier der Vollständigkeit halber enthalten. Falls du den Abschitt zu [[ scripting:tutorials:level2:bandit_camps#die_armee-basisparameter |Armee-Basisparametern]] schon kennst, kannst du diesen Abschnitt überspringen. Die Inhalte sind hier der Vollständigkeit halber enthalten.
  
-Armeen werden mit der Funktion ''SetupArmy(_ArmyTable)'' aufgesetzt. Der Anker wird auf die angegebene Position gelegt und der angegebene Radius grenzt das Aktionsgebiet ein. Der Parameter ''_ArmyTable'' ist ein Table mit den folgenden Key-Value-Paaren:+Armeen werden mit der Funktion ''SetupArmy(_ArmyTable)'' aufgesetzt. Der Anker wird auf die angegebene Position gelegt und der angegebene Radius grenzt das Aktionsgebiet ein. Der Parameter ''_ArmyTable'' ist ein [[scripting:tutorials:level1:tables#tables_als_woerterbuecher|assoziatives Table]] mit den folgenden Key-Value-Paaren:
 ^ Key ^ Value-Typ ^ Bedeutung ^ ^ Key ^ Value-Typ ^ Bedeutung ^
 | **player** | Player Id | Spieler-Id des Spielers, dem die Armee gehören soll | | **player** | Player Id | Spieler-Id des Spielers, dem die Armee gehören soll |
Zeile 87: Zeile 87:
  
 ===GetClosestEntity=== ===GetClosestEntity===
-''GetClosestEntity(_ArmyTable, _Radius)'' gibt die [[scripting:tutorials:level1:place_entities#exkursentity-id_vs_skriptname|Entity-Id]] des nächsten Gegners an, der innerhalb des angegebenen ''_Radius'' dem aktuellen Anker am nächsten liegt. Ist kein ''_Radius'' angegeben, wird der aktuelle Aktionsradius/rodeLength verwendet. Die genaue Beschreibung der Funktion findest du [[scripting:reference:comfort:getclosestentity|hier]].+''GetClosestEntity(_ArmyTable, _Radius)'' gibt die [[scripting:tutorials:level1:place_entities#exkursentity-id_vs_skriptname|Entity-Id]] des nächsten Gegners an, der innerhalb des angegebenen ''_Radius'' der aktuellen ''_position'' am nächsten liegt. Ist kein ''_Radius'' angegeben, wird der aktuelle Aktionsradius/rodeLength verwendet. Die genaue Beschreibung der Funktion findest du [[scripting:reference:comfort:getclosestentity|hier]].
  
 ===GetPosition=== ===GetPosition===
Zeile 160: Zeile 160:
 ''Synchronize(_ArmyTable1, _ArmyTable2)'' ermittelt die Position der Armee mit ''_ArmyTable1'' und setzt den Anker der zweiten Armee mit ''_ArmyTable2'' auf genau diese Position. Beachte, dass die Position vom Armee 1 nicht notwendigerweise der im Table angegebenen ''position'' entsprechen muss, da sie durch Aufrufe von beispielsweise ''Redeploy'', ''Defend'' oder ''FrontalAttack'' verändert worden sein könnte. ''Synchronize(_ArmyTable1, _ArmyTable2)'' ermittelt die Position der Armee mit ''_ArmyTable1'' und setzt den Anker der zweiten Armee mit ''_ArmyTable2'' auf genau diese Position. Beachte, dass die Position vom Armee 1 nicht notwendigerweise der im Table angegebenen ''position'' entsprechen muss, da sie durch Aufrufe von beispielsweise ''Redeploy'', ''Defend'' oder ''FrontalAttack'' verändert worden sein könnte.
  
-Der Zweck dieser Funktion ist es, eine Armee einfach einer anderen folgen lassen zu können. Es wäre zum Beispiel denkbar, für Armee 1 einen komplexen Controller zu schreiben, dem Armee 2 dann einfach nur folgen soll, anstatt ihren eigenen zu verwenden.+Der Zweck dieser Funktion ist es, eine Armee einfach einer anderen folgen lassen zu können. Es wäre zum Beispiel denkbar, für Armee 1 einen komplexen Controller zu schreiben, dem Armee 2 dann nur folgen soll, anstatt ihren eigenen zu verwenden.
  
 ---- ----
Zeile 173: Zeile 173:
 ====Truppen spawnen==== ====Truppen spawnen====
  
-CreateFormation + ConnectLeader+Um Truppen zu spawnen und einer Armee hinzuzufügen, ist die Funktion ''EnlargeArmy(_ArmyTable, _TroopDescription)'' vorgesehen. Zur Doku-Seite geht es [[scripting:reference:comfort:enlargearmy|hier entlang]]. Der Parameter ''_TroopDescription'' ist ein [[scripting:tutorials:level1:tables#tables_als_woerterbuecher|assoziatives Table]] mit folgenden Key-Value-Paaren: 
 + 
 +^ Key ^ Value-Typ ^ Bedeutung ^ 
 +| **leaderType** | EntityType | Der Entity-Typ des Hauptmanns, für den ein Trupp erschaffen werden soll. **Achtung:** Wenn hier kein gültiger Entity-Typ angegeben wird, stürzt die Funktion ohne Fehlermeldung ab! | 
 +| **position** | Position (Table der Form ''{X = x, Y = y}'') | Die Position, an der der Trupp entstehen soll. Falls die Position blockiert ist (beispielsweise durch ein Gebäude oder andere Truppen), wird die nächste freie Position gewählt, um zu vermeiden, dass der Trupp feststeckt. Wenn keine Position angegeben wird, entsteht der Trupp an der ''position'', die im ArmyTable angegeben ist. | 
 +| **experiencePoints** | Integer | Die Erfahrung, mit der der Hauptmann starten soll. In der [[scripting:reference:comfort:enlargearmy|Doku]] ist genau angegeben, welche Optionen man hier hat. Wird nichts angegeben, erhält der Hauptmann keine Erfahrung. | 
 +| **maxNumberOfSoldiers** | Integer | Die Anzahl an Soldaten, mit denen der Hauptmann startet und auf die ggf. wieder verstärkt wird. Wenn nichts angegeben wird, wird automatisch ein voller Trupp erstellt. | 
 +| **minNumberOfSoldiers** | Integer | Die Anzahl an Soldaten, ab der die KI versucht, den Trupp an einer Kaserne zu verstärken. Bei ''0'' wird der Trupp nie automatisch verstärkt. | 
 + 
 +**Wichtig:** Wenn du eine ''minNumberOfSoldiers'' angibst, wird die KI versuchen, den Trupp zu verstärken, sobald seine Größe unter den angegebenen Minimalwert fällt und ein passendes Rekrutierungsgebäude vorhanden ist. Solange der Trupp verstärkt wird, ist er **nicht** Teil der Armee, was Auswirkungen auf die Funktionen ''GetNumberOfLeaders'', ''IsWeak'', ''IsVeryWeak'', ''HasFullStrength'' und ''IsDead'' hat! 
 + 
 +Wir wollen ein Beispiel geben, in dem zwei verschiedene Truppentypen vor dem passenden Militärgebäude gespawnt werden. Dazu muss jeweils ein ''XD_ScriptEntity'' mit dem Namen ''"BarracksSpawn"'' vor einer Kaserne und eines mit dem Namen ''"ArcherySpawn"'' vor einem Schießplatz existieren (zusätzlich zur ''"PositionSpawnArmy"'' irgendwo in der Nähe). Natürlich muss auch die KI für Spieler 2 aktiviert werden. 
 +<code lua> 
 +function CreateSpawnArmy() 
 +    SpawnArmy = { 
 +        player = 2, 
 +        -- wir wählen die Id 0 
 +        -- bei mehreren Armeen für Spieler 2 müssen alle unterschiedliche Ids haben 
 +        id = 0, 
 +        position = GetPosition("PositionSpawnArmy"), 
 +        rodeLength = 4000, 
 +        beAgressive = true, 
 +        strength = 6 
 +    } 
 + 
 +    SetupArmy(SpawnArmy) 
 +     
 +    local TroopDescriptionSword = { 
 +        leaderType = Entities.PU_LeaderSword1, 
 +        position = GetPosition("BarracksSpawn"), 
 +        experiencePoints = MEDIUM_EXPERIENCE, 
 +        maxNumberOfSoldiers = 4, 
 +        minNumberOfSoldiers = 0 
 +    } 
 +     
 +    local TroopDescriptionPoleArm = { 
 +        leaderType = Entities.PU_LeaderPoleArm2, 
 +        position = GetPosition("BarracksSpawn"), 
 +        experiencePoints = LOW_EXPERIENCE, 
 +        maxNumberOfSoldiers = 4, 
 +        minNumberOfSoldiers = 0 
 +    } 
 +     
 +    local TroopDescriptionBow = { 
 +        leaderType = Entities.PU_LeaderBow1, 
 +        position = GetPosition("ArcherySpawn"), 
 +        experiencePoints = HIGH_EXPERIENCE, 
 +        maxNumberOfSoldiers = 4, 
 +        minNumberOfSoldiers = 0 
 +    } 
 +     
 +    -- Erstelle jeweils 2 Trupps Schwertkämpfer, Speerträger und Bogenschützen 
 +    for _ = 1, 2 do 
 +        EnlargeArmy(SpawnArmy, TroopDescriptionSword) 
 +        EnlargeArmy(SpawnArmy, TroopDescriptionPoleArm) 
 +        EnlargeArmy(SpawnArmy, TroopDescriptionBow) 
 +    end 
 +end  
 +</code> 
 + 
 +Im [[scripting:tutorials:level3:armies#beispiel_fuer_komplexe_armeesteuerung|Beispiel für komplexe Armeesteuerung]] zeigen wir außerdem beispielhaft, wie ein Respawnverhalten umgesetzt werden kann (ähnlich zum [[scripting:tutorials:level2:bandit_camps#das_respawnverhalten||AITroopSpawnGenerator]]).
  
 ====Truppen rekrutieren==== ====Truppen rekrutieren====
  
-BuyLeader + ConnectUnemployedLeader+Für das Rekrutieren von Truppen gibt es leider keine vorgefertigte Comfortfunktion, weshalb wir hier eine bereitstellen: 
 + 
 +<code lua> 
 +function ArmyBuyLeader(_ArmyTable, _UpgradeCategory) 
 +    -- Check if Army is already at full strength 
 +    if HasFullStrength(_ArmyTable) then 
 +        return 
 +    end 
 +     
 +    local NumberOfLeaders = GetNumberOfLeaders(_ArmyTable) 
 +    -- Try to add a leader to the army first 
 +    AI.Entity_ConnectUnemployedLeaderToArmy(_ArmyTable.player, _ArmyTable.id, _ArmyTable.strength) 
 +     
 +    -- Check if a new leader could be connected. If yes, no further action is required 
 +    if GetNumberOfLeaders(_ArmyTable) > NumberOfLeaders then 
 +        return 
 +    end 
 +     
 +    -- If no unemployed leader was added, recruit a new one 
 +    AI.Army_BuyLeader(_ArmyTable.player, _ArmyTable.id, _UpgradeCategory) 
 +    AI.Entity_ConnectUnemployedLeaderToArmy(_ArmyTable.player, _ArmyTable.id, _ArmyTable.strength) 
 +end 
 +</code> 
 + 
 +Die ''_UpgradeCategory'' ist dabei **nicht** der Entity-Typ des gewünschten Hauptmanns, sondern seine "Kategorie", also zum Beispiel ein Schwertkämpfer: 
 +<code lua> 
 +ArmyBuyLeader(RecruitArmy, UpgradeCategories.LeaderSword) 
 +</code> 
 +Das hat den Grund, dass sich die KI an ihre Technologiestufe halten muss. Im Skript kann man nur die Truppenkategorie angeben, die rekrutiert werden soll. Der konkrete Truppentyp muss der KI via [[scripting:tutorials:level3:ai_development|Technologien bereitgestellt werden]]. Beachte außerdem, dass die KI für die Rekrutierung von Truppen ausreichend [[scripting:tutorials:level1:resources|Rohstoffe]], aber **keinen** Bevölkerungsplatz benötigt. Eine Liste aller UpgradeCategories findest du [[https://github.com/mcb5637/s5LuaReference/blob/master/Cpp/tables/UpgradeCategories.lua|hier]]. 
 + 
 +**Achtung:** Falls du mit obiger Funktion Kanonen ausbilden lassen willst, musst du wiederum den Entity-Typen angeben, also ''Entities.PV_Cannon1'' usw. 
 + 
 +Auch hierfür wollen wir ein kleines Beispiel schreiben. Die Voraussetzungen für das folgende Skript sind eine Map, die folgendes enthält: 
 +  * Mindestens eine Kaserne und ein Schießplatz für Spieler 2 
 +  * Ein ''XD_ScriptEntity'' mit dem Namen ''"PositionRecruitArmy"'' 
 +  * Ausreichend Taler, Holz und Eisen für Spieler 2 
 +  * Eine aktive KI für Spieler 2 
 +<code lua> 
 +function CreateRecruitArmy() 
 +    RecruitArmy = { 
 +        player = 2, 
 +        -- wir wählen die Id 0 
 +        -- bei mehreren Armeen für Spieler 2 müssen alle unterschiedliche Ids haben 
 +        id = 0, 
 +        position = GetPosition("PositionRecruitArmy"), 
 +        rodeLength = 4000, 
 +        beAgressive = true, 
 +        strength = 6 
 +    } 
 + 
 +    SetupArmy(RecruitArmy) 
 + 
 +    -- Rekrutieren muss etwas zeitverzögert stattfinden, da die Hauptmänner sonst nicht korrekt 
 +    -- zur Armee hinzugefügt werden. Wir starten darum einen SimpleJob, der nach einigen Sekunden 
 +    -- Verzögerung die Truppen rekrutieren lässt 
 +    StartSimpleJob("InitRecruitArmy"
 +end 
 + 
 +function InitRecruitArmy() 
 +    if Counter.Tick2("InitRecruitArmy", 5) then 
 +        -- Erstelle jeweils 3 Trupps Schwertkämpfer und Bogenschützen 
 +        for _ = 1, 3 do 
 +            ArmyBuyLeader(RecruitArmy, UpgradeCategories.LeaderSword) 
 +            ArmyBuyLeader(RecruitArmy, UpgradeCategories.LeaderBow) 
 +        end 
 +        -- Sobald das passiert ist, beende den Job 
 +        return true 
 +    end 
 +end 
 +</code>
  
 ====Probleme beim Rekrutieren==== ====Probleme beim Rekrutieren====
 +
 +Einer KI ihre Truppen über Rekrutierung zu stellen ist die optisch schönere Lösung, bringt aber einige Probleme mit sich. Diese zu umgehen ist mit den Armee-Funktionen, die das Spiel von sich aus bereitstellt, nicht möglich, sondern müssen mit Armee-Funktionen aus der Community behandelt werden. Diese zu besprechen würde an der Stelle aber den Rahmen des Wikis sprengen. Wir gehen darum auf die wichtigsten Nachteile ein, die die Rekrutierung von Truppen mit der Standardarmee mit sich bringt.
  
 ===Wenig Kontrolle über die Truppentypen=== ===Wenig Kontrolle über die Truppentypen===
 +
 +Wenn du dir den Code für die Funktion ''ArmyBuyLeader'' genau anschaust, wirst du sehen, dass nicht notwendigerweise der rekrutierte Hauptmann der Armee hinzugefügt wird, sondern ggf. **irgendeiner**, der noch nicht bereits einer Armee zugewiesen wurde.
 +
 +Das liegt daran, dass die Funktion ''AI.Army_BuyLeader'' den rekrutierten Hauptmann **nicht** automatisch der Armee hinzufügt und außerdem die Entity-Id des Hauptmanns nicht zurückgibt. Dadurch hat man an der Stelle (fast) keine Handhabe, genau den gewünschten Hauptmann der Armee zuzuweisen. Die Rekrutierung von Kanonen, deren Entity-Id beim Aufruf der Funktion noch lange nicht existiert, erschwert die Sache zusätzlich.
 +
 +Folglich ist die Rekrutierung von beispielsweise reinen Reiterarmeen nicht zuverlässig möglich, wenn auch andere Armeetypen entstehen sollen.
  
 ===Wenig Kontrolle über das Verstärkungsverhalten=== ===Wenig Kontrolle über das Verstärkungsverhalten===
 +
 +Wie weiter oben angemerkt, wird die KI versuchen, angeschlagene Truppen zurückzuziehen und an der nächsten Kaserne zu verstärken. Bei Spawntruppen lässt sich der Schwellwert der Truppenstärke, ab der das geschieht, präzise einstellen. Rekrutierte Truppen allerdings werden häufig zurückgezogen, sobald sie auch nur einen ihrer Soldaten verloren haben. Das hat zur Folge, dass die KI-Truppen gerade im Angriff sehr ineffizient kämpfen, weil sie oft gar nicht dazu kommen, einen Gegner zu treffen, sondern sich sehr früh wieder zurückziehen.
  
 ===Wenig Kontrolle über den Ausbildungsort=== ===Wenig Kontrolle über den Ausbildungsort===
 +
 +Wie bei [[scripting:tutorials:level1:enemy_ai#mapeditor_setupai|MapEditor_SetupAI]] hat man auch bei der händischen Armee-Rekrutierung keine Kontrolle darüber, welche Rekrutierungsgebäude die KI benutzt (letztendlich verwendet ''MapEditor_SetupAI'' ebenfalls ''AI.Army_BuyLeader''). Außerdem ist man wieder auf genau eine Kaserne, einen Schießplatz usw. eingeschränkt, weil die KI aus jedem weiteren Rekrutierungsgebäude der gleichen Gattung nicht rekrutieren wird.
 +
 +----
  
 =====Beispiel für komplexe Armeesteuerung===== =====Beispiel für komplexe Armeesteuerung=====
 +
 +Wir wollen die Status-, Steuerungs- und Spawnfunktionen, die oben beschrieben sind, zu einer komplexeren Armeesteuerung zusammenbringen. Die Beispielmap, die den lauffähigen Code enthält, kannst du {{ scripting:tutorials:level3:armies:armydemomap.zip | hier }} herunterladen. Wir verzichten darauf, den kompletten Quellcode hier im Wiki zu posten.
 +
 +Um die Armeesteuerung zu demonstrieren, entwerfen wir folgenden Rahmen: Vor den Toren Crawfords haben Banditen ein Lager errichtet. Die Truppen von Crawford sollen sich sammeln und die Banditen vertreiben.
 +
 +Das Verhalten der Banditen soll dabei ganz einfach gehalten sein: Sie sollen nur verteidigen und in regelmäßigen Abständen aus ihren Türmen respawnen. Nach einiger Zeit stoppt der Respawn, sodass Crawford letztendlich die Oberhand gewinnt.
 +
 +Das Verhalten der Truppen von Crawford soll komplexer sein:
 +  * Während die Armee nicht die volle Truppenzahl umfasst, soll sie eine defensive Position einnehmen
 +  * Auf der defensiven Position wird die Armee durch den Spawn neuer Truppen verstärkt
 +  * Sobald die Armee ihre volle Stärke besitzt, soll sie zu einer offensiven Position vorrücken
 +  * Auf der offensiven Position läuft ein Counter. Sobald dieser abgelaufen ist, greift die Armee an
 +  * Falls sie so viele Truppen verliert, dass sie als "VeryWeak" gilt, zieht sie sich auf die defensive Position zurück
 +  * Falls keine Gegner mehr im Angriffsgebiet sind, zieht sie sich auf die offensive Position zurück
 +Beide Armeen sollen den Respawn einstellen, wenn die Spawngebäude zerstört wurden. Der Ablauf wird in diesem Zustandsdiagramm noch einmal verdeutlicht:
 +
 +{{ scripting:tutorials:level3:armies:armystatemachine.png?650 }}
 +
 +Um die Zustände in Lua umzusetzen, programmieren wir einen [[https://de.wikipedia.org/wiki/Endlicher_Automat|Zustandsautomaten]]. Falls du damit noch nicht vertraut bist, lohnt es sich, den Wikipedia-Artikel durchzulesen.
 +
 +Folgende Positionen und Gebäude setzen wir auf die Karte:
 +  * **Für die Crawford-Armee:**
 +    * Eine Kaserne mit dem Namen ''"Barracks"'' und einen Schießplatz mit dem Namen ''"Archery"''
 +    * Vor der Kaserne und dem Schießplatz jeweils ein ''ScriptEntity'' mit dem Namen ''"BarracksSpawn"'' bzw. ''"ArcherySpawn"''
 +    * Dem Banditenlager abgewandt ein ''ScriptEntity'' namens ''"ArmyCrawfordDefense"'' und dem Banditenlager zugewandt ''"ArmyCrawfordOffense"''
 +  * **Für das Banditenlager:**
 +    * Zwei Banditentürme mit den Namen ''"BanditTower1"'' und ''"BanditTower2"''
 +    * Ein ''ScriptEntity'' mit dem Namen ''"ArmyBanditDefense"''
 +
 +Damit dieser Artikel nicht mit Lua-Code überquillt, verweisen wir auf den {{ scripting:tutorials:level3:armies:armydemomap.zip | Download der Demo-Map }}, in der der komplett kommentierte Code enthalten ist. Im Spiel wird der Zustandswechsel der Crawford-Armee außerdem gesondert angezeigt.
 +
 +Wichtig sind insbesondere folgende Aspekte:
 +
 +===Definition der Zustände===
 +Wir nummerieren die Zustände einer Armee durch, sodass sie im Skript besser lesbar sind und beim Programmieren Fehler vermieden werden. Dafür definieren wir ein globales Table, das aussagekräftige Namen verwendet:
 +<code lua>
 +ArmyStates = {
 +    Defense = 1,
 +    Offense = 2,
 +    Attack = 3
 +}
 +</code>
 +In der Kontrollfunktion für die Crawford-Armee prüfen wir dann immer zuerst, in welchem Zustand sich die Armee befindet, fragen dann ab, ob ein Zustandsübergang möglich ist und führen anschließend die dem Zustand zugehörige Aktion aus.
 +
 +===Zusätzliche Parameter im Army-Table===
 +Für beide Armeen brauchen wir zusätzliche Parameter, die über die [[scripting:tutorials:level2:bandit_camps#die_armee-basisparameter|Basisparameter]] hinaus gehen. Dazu zählen beispielsweise Parameter, die das Spawnverhalten der Armeen steuern:
 +<code lua>
 +spawnDelay = 3,
 +spawnAmount = 2,
 +spawnCounter = 0,
 +spawnCycles = 12,
 +spawnLifelines = {"BanditTower1", "BanditTower2"}
 +</code>
 +Diese Parameter müssen wir selbst in den jeweiligen eigenen Kontrollfunktionen verwenden - sie haben keinen Einfluss auf die vom Spiel bereitgestellten Funktionen.
 +
 +===Nur eingeschränkt zum Copy-Paste geeignet===
 +Die Kontrollfunktionen, an denen beispielhaft Armeeverhalten gezeigt wird, sollen möglichst viele der im Artikel vorgestellten Funktionen demonstrieren und anschaulich machen. Dadurch ist aber nicht gewährleistet, dass sie für deine Map genau passend ist. Wichtig ist, dass du die Funktionen verstehst! Nur so kannst du deinen eigenen Army-Controller schreiben, der das KI-Verhalten in deiner Karte am besten umsetzt (wir sind hier schließlich im Tutorial-Abschnitt für "Individuelle Kartenabläufe").
 +
 +====Beispielmap von Play4Fun====
 +
 +Play4Fun hat ebenfalls eine Karte erstellt, die komplexe Armeesteuerung demonstriert. Wenn du dir Armeeverhalten anschauen möchtest, das über den Rahmen dieses Wikis hinaus geht, findest du hier den Downloadlink:
 +
 +[[https://www.siedler-maps.de/maps/map-1706.htm]]
 +
 +----
 +
 +Im nächsten Kapitel wird beschrieben, wie du die KI einzelne Gebäude aufbauen lassen kannst.
 +
 +[[ scripting:tutorials:level3:ai_construction | Nächstes Kapitel: Eigenständiger Aufbau ]]\\
 +[[ scripting:tutorials:level3:armies | Zurück nach oben ]]
 +
scripting/tutorials/level3/armies.1751969598.txt.gz · Zuletzt geändert: 2025/07/08 10:13 von fritz_98