Seekampf 2.0 — API-Spezifikation

Das gesamte Spiel ist über diese API spielbar (Web-Client & externe Bots). Basis: REST + JSON unter /api/v1, plus WebSocket für Live-Kämpfe & Events. FastAPI liefert zusätzlich automatische OpenAPI/Swagger-Doku unter /docs. Status: Bereit für Umsetzung · 2026-07-02


1. Konventionen


2. Auth & Account

GET  /auth/captcha                                 → Bot-Schutz für die Registrierung:
                                                      {challenge_id, frage} — einfache
                                                      Rechenaufgabe, NUR EINMAL gültig
                                                      (verfällt nach dem ersten
                                                      POST /auth/register-Versuch,
                                                      egal ob richtig/falsch
                                                      beantwortet), Ablauf nach
                                                      CAPTCHA["gueltigkeit_s"] (10 Min.)
                                                      ohnehin. Rate-Limit "captcha"
                                                      (großzügiger als "register" —
                                                      legitime Tippfehler/Neuladen).
POST /auth/register        {name, email, password,
                            challenge_id, captcha_antwort}
                                                    → Account + Startinsel (Login erst
                                                      nach E-Mail-Bestätigung!). 422
                                                      invalid_captcha bei falscher/
                                                      fehlender/abgelaufener Antwort —
                                                      dann NEUE Aufgabe über
                                                      GET /auth/captcha holen (die alte
                                                      ist verbraucht). Betrifft NUR die
                                                      Registrierung selbst — ab einem
                                                      bestehenden Account ist wie immer
                                                      jede Spielaktion per API/Bot ohne
                                                      jede weitere Einschränkung nutzbar
                                                      (BOT-GUIDE.md §0).
POST /auth/login           {name|email, password}  → {access, refresh}; 403 email_not_verified vor Bestätigung
POST /auth/refresh         {refresh}               → {access}
POST /auth/logout
POST /auth/verify-email    {token}                 → E-Mail bestätigen (schaltet Login frei)
POST /auth/resend-verification {email}             → Bestätigungsmail erneut senden (neutrale Antwort)
POST /auth/forgot-password {email}                 → Reset-Mail (SMTP, siehe ARCHITEKTUR §3a)
POST /auth/reset-password  {token, new_password}   → Passwort neu setzen
POST /auth/change-password {old_password, new_password} → Passwort ändern (eingeloggt)
GET  /me                                           → Profil, Punkte, Allianz, Bio
                                                      — `punkte` hier LIVE (einzige
                                                      Ausnahme im gesamten System,
                                                      überall sonst kommen Punkte
                                                      aus dem täglichen Snapshot)
POST /me/profile           {bio}                   → kurzes Spieler-Profil setzen
GET  /users/{user_id}/profile                       → öffentliches Profil (Bio,
                                                      Punkte, Inselliste Name/
                                                      Koordinaten/Punkte/
                                                      Tageszuwachs — KEINE
                                                      Rohstoffe/Gebäude/Truppen;
                                                      zusätzlich: `online` (bool,
                                                      letzte Aktivität ≤
                                                      ONLINE_SCHWELLE_MIN=5 Min.),
                                                      `allianz` ({id,tag,name} oder
                                                      null), `quests_erfuellt`
                                                      (öffentlich sichtbare
                                                      Auszeichnungen, siehe §12).
                                                      `punkte` (Gesamt UND je Insel
                                                      in `inseln`) kommt aus dem
                                                      täglichen Tages-Snapshot,
                                                      NICHT live — gilt auch fürs
                                                      EIGENE Profil unter dieser
                                                      Route (nur `/me` ist live).
                                                      Jede Insel in `inseln` hat
                                                      zusätzlich ein `differenz`-Feld
                                                      = fertig berechneter
                                                      Punkte-Zuwachs/-Verlust
                                                      VOM VORTAG (NICHT der
                                                      laufende Zuwachs seit
                                                      Mitternacht) — kommt aus
                                                      `Island.punkte_differenz`,
                                                      einmal täglich beim
                                                      Rollover aus dem ALTEN
                                                      `punkte_snapshot`
                                                      berechnet, danach stabil
                                                      bis zum nächsten Tag).
                                                      `null` vor dem zweiten
                                                      Snapshot der Insel.
GET  /users/lookup         ?name=                   → wie oben, per Name gesucht
GET  /me/islands                                   → Liste eigener Inseln
GET  /me/islands/overview                          → alle eigenen Inseln kompakt:
                                                      je Insel {id, name, koordinaten,
                                                      punkte, rohstoffe (inkl. kapazitaet),
                                                      truppen, schiffe, gebaeude
                                                      (Stufen, OHNE deaktivierte wie
                                                      Marktplatz/Labor), bauauftrag_aktiv,
                                                      bedrohung_im_anflug,
                                                      handel_im_anflug} — für den
                                                      Inseln-Tab (Web-Client zeigt
                                                      Einheiten/Schiffe ODER Gebäude,
                                                      umschaltbar). Zwei getrennte Booleans
                                                      statt einem: `bedrohung_im_anflug`
                                                      NUR bei Angriff/Spionage/Kolonisation,
                                                      `handel_im_anflug` bei ausschließlich
                                                      bestätigtem Handel zu dieser Insel
                                                      (rot vs. blau im Web-Client) — beide
                                                      schließen sich gegenseitig aus.
POST /me/current-island    {island_id}             → aktive Insel wechseln
# API-Keys für Bots
GET    /me/apikeys                                 → Liste eigener Keys (id, prefix,
                                                      label, scopes, last_used_at,
                                                      created_at — NIE der Klartext,
                                                      der Server speichert nur den Hash)
POST   /me/apikeys         {label, scopes}         → einmalig Klartext-Key
                                                      ({id, key, prefix, label, hinweis})
                                                      — wird SONST NIE WIEDER angezeigt,
                                                      auch nicht über GET /me/apikeys
DELETE /me/apikeys/{id}                            → widerrufen (sofort ungültig)
# DSGVO
GET    /me/export                                  → Datenexport (Auskunft, JSON)
DELETE /me                 {password}              → Account löschen (Recht auf Vergessenwerden)

2a. Rechtliche Seiten (statisch, ohne Login)

GET /legal/impressum           → Impressum (HTML/Markdown)
GET /legal/datenschutz         → Datenschutzerklärung
GET /legal/regeln              → Spielregeln

Im Footer des Web-Clients verlinkt. Inhalte editierbar (Template/Markdown), Werte liefert der Betreiber.

3. Insel & Rohstoffe

GET /islands/{id}                → Insel-Detail (Rohstoffe live, Produktion/h,
                                   Lager, Gebäude+Stufen, Garnison, Schiffe,
                                   laufende Aufträge, Punkte)
GET /islands/{id}/resources      → aktueller Bestand + Produktion + Kapazität

Lagerkapazität ist eine harte Grenze für JEDEN Zugang — nicht nur für die passive Produktion (die sowieso stoppt, sobald das Lager voll ist), sondern genauso für Flottenrückkehr (Beute, §7), Quest-Belohnungen (§12) und Ausbildungs- Stornierung (§5): POST/interner economy.gutschreiben() kappt IMMER auf lager_kapazitaet(lagerhaus_stufe). Was über die Kapazität hinausgeht, geht in diesen Fällen ersatzlos verloren — kein Überlauf, keine API gibt einen Hinweis darauf zurück, wie viel tatsächlich verloren ging (die Antwort enthält weiterhin den vollen angeforderten/erwarteten Betrag, nicht den tatsächlich gutgeschriebenen). Ein bereits VORHANDENER Überbestand (z. B. nach Lagerhaus-Rückstufung durch Razzia) wird dadurch nicht zerstört — nur was NEU dazukäme, wird gekappt.

Zwei Ausnahmen, bei denen ein Überschuss NICHT verloren geht: 1. Allianzkasse-Auszahlung (§9) rechnet ihre Kapazitätsgrenze selbst separat und leitet Überschuss aktiv zurück in die Kasse. 2. Handel/Transport-Lieferung (§7, inkl. Handelsrouten §10): passt eine Rohstoff-Lieferung nicht komplett ins Lager der Zielinsel, nimmt die Flotte den Überschuss mit zurück zur Heimatinsel (services/fleets._deliver() setzt fleet. resources auf den nicht zugestellten Rest, NUR bei mission="transport" — echte Handelsschiffe kehren ohnehin heim). Der Handelsbericht nennt explizit sowohl die tatsächlich angekommene Menge als auch den zurückfahrenden Überschuss (payload.ueberschuss). Erst wenn AUCH die Heimatinsel bei Rückkehr keinen Platz mehr hat, geht der Rest endgültig verloren (normale Kappung bei _unload_at_home()). Eine abstrakte Markt-Lieferung (mission="market", aktuell irrelevant — Marktplatz deaktiviert) hat KEINE Rückfahrt, dort bleibt ein Überschuss verloren.

4. Gebäude & Warteschlange

GET  /islands/{id}/buildings                         → alle Typen + Stufen
                                                       (haupthaus_ab, verfuegbar)
GET  /islands/{id}/buildings/{type}/detail            → Beschreibung + volle
                                                       Stufentabelle (Kosten/Zeit/
                                                       Effekt/Freischaltung)
POST /islands/{id}/buildings/{type}/upgrade          → in Bau-Warteschlange (max 3)
GET  /islands/{id}/construction                       → aktiver Bau + Queue
DELETE /islands/{id}/construction/{orderId}           → Auftrag abbrechen
POST /islands/{id}/rename  {name}                     → Insel umbenennen

Fehler wenn: Voraussetzung (Haupthaus-Stufe — Kaserne ab 5, Hafen ab 7, Labor ab 10, Marktplatz ab 10, Steinmauer/Wachturm ab 1) fehlt, Rohstoffe fehlen, Queue voll, Max-Stufe erreicht, oder das Gebäude deaktiviert ist (aktuell: Marktplatz, Labor — 409 building_disabled, siehe balancing.DEAKTIVIERTE_GEBAEUDE; Code/Daten bleiben vollständig erhalten).

5. Militär (Kaserne/Hafen) & Forschung

POST /islands/{id}/training      {facility: kaserne|hafen, item, count}
GET  /islands/{id}/training       → aktive Ausbildung + Queue
DELETE /islands/{id}/training/{orderId}
GET  /islands/{id}/research
POST /islands/{id}/research/{tech}/upgrade

Baubarkeit prüft Kaserne-/Hafen-Stufe (z. B. Koloschiff = Hafen 20). Forschung (speer/schild/bogen/segel/spionage/engineering, je Stufe +10 % Effekt auf Angriff/Verteidigung/Schiffsgeschwindigkeit/Spionage-Chance/ Bauzeit) ist aktuell deaktiviert: POST /research/{tech}/upgrade → 409 feature_disabled, und research.forschung_stufen() liefert serverseitig nur noch 0 zurück (wirkt sich also auch nirgends mehr aus) — Code/Daten bleiben vollständig erhalten, System ist für eine Reaktivierung vorbereitet.

6. Karte / Ocean View

GET /map/sector/{x}/{y}          → 5×5 Inseln des Sektors (Basis-Infos:
                                   Besitzer/Name/Allianz/Punkte; KEINE Truppen/Rohstoffe)
GET /map/region/{x}/{y}          → 3×3-Sektoren-Ausschnitt (15×15 Inseln) in
                                   einer Antwort — fürs Ozean-Raster
GET /map/island/{x}/{y}/{z}      → öffentliche Hover-Infos einer Insel; für
                                   eingeloggte Betrachter zusätzlich
                                   `eigene_berichte` — letzte 10, neueste
                                   zuerst: eigene Angriffe auf GENAU diese
                                   Insel UND
                                   eingehende Angriffe vom aktuellen Besitzer
                                   dieser Insel gegen eine eigene Insel;
                                   ungefiltert nach `Message.hidden` — im
                                   Postfach ausgeblendete Berichte bleiben
                                   hier trotzdem sichtbar
# Detail-Infos (Truppen/Rohstoffe) NUR via Spionage (siehe Kampfbericht)

punkte in ALLEN drei Endpunkten oben (Sektor/Region/Insel) kommt aus dem täglichen Tages-Snapshot, NICHT live — eine brandneue Insel ohne bisherigen Snapshot zeigt live, sonst stünde sie permanent mit 0 Punkten da.

7. Flotten & Missionen

POST /fleets   {origin_island_id, mission_type, target:{x,y,z},
                ships:{type:count}, units:{type:count}, resources:{...},
                razzia_ziel?, razzia_prioritaet?}
               (beide nur für mission_type=attack)
               → berechnet Fahrzeit/Ankunft; validiert Ladevolumen & Kapazität
                                   (Fahrzeit-Minimum 5 Min., innerhalb
                                   desselben Sektors nur 2 Min., `formulas.
                                   fahrzeit_minuten(..., gleicher_sektor)`)
GET  /fleets                      → eigene Flotten (outbound/returning) + Status,
                                   inkl. `origin` ({x,y,z,name} —
                                   `origin_island_id` allein ist für den
                                   Client nur eine ID) und `target`
                                   ({x,y,z,name}). `name` ist NULL, wenn dort
                                   keine Insel-Zeile existiert (leeres Wasser) —
                                   sonst frei einsehbar wie im Ozean-Popup,
                                   keine zusätzliche Aufklärung.
GET  /fleets/incoming             → auf eigene Inseln anfliegende Flotten,
                                   eigene UND fremde (Sichtbarkeit je
                                   Wachturm-Stufe DER ZIELINSEL — auch eigene
                                   Flotten erst innerhalb der Sichtweite).
                                   Bei fremden Flotten sind `absender`,
                                   `allianz_flagge` ({tag, icon, farbe} |
                                   null, s. §9) UND `mission` (attack/transport/
                                   spy/colonize) IMMER Teil der Antwort,
                                   unabhängig von der Wachturm-Stufe —
                                   `ships`/Einheiten-Anzahl fehlen bei fremden
                                   Flotten dagegen GRUNDSÄTZLICH (keine
                                   Wachturm-Stufe schaltet das frei,
                                   `WACHTURM_DETAILS_AB_STUFE` entfernt; der
                                   Wachturm erhöht nur noch die Sichtweite:
                                   1 Feld/Stufe, gedeckelt auf max. 20 Felder —
                                   `WACHTURM_SICHTWEITE_PRO_STUFE`/
                                   `WACHTURM_SICHTWEITE_MAX`).
                                   Eigene Flotten in der Liste haben immer
                                   volle Details (`ships`, `units`).
                                   + eigene Flotten, die eine eigene
                                   Insel gerade verlassen haben
                                   ({abfahrend: true, herkunft_insel_id,
                                   herkunft_koordinaten, sichtbar_bis} —
                                   sichtbar exakt solange, wie sie auch
                                   zurückgerufen werden könnte) +
                                   vorbeifahrende fremde Flotten
                                   ({vorbeifahrend: true, ziel_insel_id,
                                   absender, allianz_flagge, richtung} —
                                   "da fliegt was",
                                   Absender ("Flagge") + grobe Himmelsrichtung
                                   (eine von acht 45°-Sektoren: Norden,
                                   Nordosten, Osten, Südosten, Süden,
                                   Südwesten, Westen, Nordwesten) sichtbar,
                                   aber KEINE Herkunft/Ziel-Koordinaten/Mission)
POST /fleets/{id}/recall          → Rückruf (nur solange in Wachturm-Sichtweite)

mission_type: nur noch "attack" oder "handel" — die konkrete Abwicklung (in GET /fleets als mission sichtbar: attack/colonize/spy/transport) ergibt sich automatisch aus der Fracht: - attack + NUR Spähschiffe an Bord (keine Einheiten/Rohstoffe) → Spionage — geht auf JEDE existierende Insel, auch unbewohnte (eine Ruine kann noch eine Restgarnison/Rohstoffe haben). Nur echtes leeres Wasser (kein Inselplatz) ist kein gültiges Ziel. - attack + Kolonisationsschiff an Bord + Ziel ist eine FREIE Insel (owner = niemand) → Kolonisation. Ist das Ziel bewohnt, bleibt es ein normaler Angriff — das mitgeführte Kolonisationsschiff ist dann Teil der Eroberungs-Mechanik (gelingt die Eroberung, wird es verbraucht, genau wie bei einer normalen Kolonisation), NICHT der Kolonisation einer freien Insel. - attack sonst → normaler Angriff/Plünderung. - handel → Transport: Einheiten/Rohstoffe an eine EXISTIERENDE Insel liefern (bewohnt oder unbewohnt/Ruine — z. B. um eine Ruine als Vorposten zu bestücken). Einheiten UND Rohstoffe an JEDE existierende Insel — egal ob eigene, Allianzmitglied, fremder Spieler oder unbewohnt (die frühere D-Reinforce-Einschränkung auf eigene/Allianz-Inseln bei bewohnten fremden Zielen ist entfernt).

Regeln: nur Einheiten+Rohstoffe transportierbar (keine Schiffe verlegen); Rückkehr = gleiche Dauer; Beute fährt automatisch zur Heimatinsel; per handel verschickte Einheiten gehen dauerhaft in die Garnison der Zielinsel über.

Handelsbericht: bei Ankunft einer Transport-/Markt-Lieferung (services/fleets._deliver()) bekommt der Absender IMMER eine Postfach-Nachricht (payload.typ == "handel", folder="combat" — liegt im selben Ordner/Reiter wie Kampf-/Spionage- berichte, NICHT im normalen Postfach; kein battle_id/Bericht-Link, reiner Text) mit der TATSÄCHLICH angekommenen Menge (Vorher/Nachher-Differenz, gedeckelt auf die Lagerkapazität der Zielinsel — Einheiten haben kein Kapazitätslimit). Gehört die Zielinsel jemand anderem als dem Absender, bekommt zusätzlich der Insel-Besitzer einen eigenen Bericht mit Absendername (payload.absender). Gilt für JEDE _deliver()-Lieferung, also auch für automatische Handelsrouten-Läufe (§ Handel) und — sobald der Marktplatz reaktiviert wird — Marktangebots-Zustellungen. Wie Kampf-/Spionageberichte wird ein gelöschter Handelsbericht nur hidden gesetzt (historisches Archiv), nicht wirklich gelöscht (DELETE /messages/{id}, folder == "combat"-Zweig).

Kolonisation zielt NUR auf eine existierende, unbewohnte Insel (owner = niemand) — leeres Wasser ohne Inselplatz ist nicht kolonisierbar. Der Weltgenerator legt regelmäßig neue unbewohnte Inseln an (siehe ARCHITEKTUR). razzia_ziel (Gebäudetyp) ist optional — ohne Angabe raziert der Server bei Sieg automatisch das Haupthaus.

Kolonisation übernimmt die Insel 1:1: services/fleets._kolonisieren() setzt NUR owner_id/name/ punkte_snapshot neu — Gebäudestufen bleiben komplett unverändert (bei einer Ruine also inkl. bereits ausgebauter Minen etc.), mitgeführte Fracht der Flotte wird zum bisherigen Rohstoffbestand der Insel ADDIERT (nicht ersetzt), normale Lagerkapazitäts-Deckelung. Eine evtl. vorhandene Restgarnison/Schiffe bleiben ebenfalls erhalten. Dasselbe gilt für eine Eroberung (§8) — dort war es schon immer so. Bei JEDER Kolonisation (Erfolg UND Fehlschlag) geht eine Nachricht in folder="combat" (payload.typ == "kolonisation", payload.erfolg: bool) — analog zum Handelsbericht oben.

razzia_prioritaet (Rohstofftyp gold/stein/holz, optional): steuert die Plünderung bei Sieg. Ohne Angabe wie bisher "water filling" — die Handelskapazität wird gleichmäßig auf alle noch verfügbaren Rohstoffsorten verteilt (§ Kampf-Details/combat.pluenderung()). MIT Angabe wird NUR dieser eine Rohstoff geplündert — alle anderen Sorten werden komplett ignoriert, auch wenn dadurch das Ladevolumen nicht ausgeschöpft wird (Schiffe kommen ggf. nicht voll beladen zurück, z. B. wenn weniger vom priorisierten Rohstoff auf der Zielinsel liegt als Kapazität da ist). Landet auf Fleet.meta.razzia_prioritaet, analog zu razzia_ziel.

8. Kampf

GET /battles/{id}                 → Bericht, SOFORT ab Ankunft der Flotte
                                   vollständig verfügbar (kein Live-Playback,
                                   keine Berichtssperre — beides 2026-07-19
                                   entfernt). Sichtbarkeit je Rolle:
                                   Verteidiger immer vollständig, Angreifer
                                   Kampfverlauf IMMER + Zusatz-Aufklärung nur
                                   bei Spähschiff+Sieg, s. 14a.1/14a.4.

9. Allianzen

POST /alliances                {name, tag}
GET  /alliances/{id}            → Info, Mitglieder (inkl. Inselanzahl je
                                   Mitglied), Diplomatie, Punkte
POST /alliances/{id}/invite     {user_id}      (leader/diplomat)
POST /alliances/{id}/join | /leave | /kick
POST /alliances/{id}/roles      {user_id, role}
POST /alliances/{id}/kasse/deposit | /withdraw  {island_id, resources}
                                   → {id, richtung, resources, fertig_at, kasse}
                                   (deposit: kein kasse-Feld. withdraw als
                                   Anführer: fertig_at gesetzt, status="aktiv".
                                   withdraw als Diplomat: fertig_at=null,
                                   status="wartet_auf_freigabe" — s. u.)
POST /alliances/{id}/kasse/withdraw/{auftrag_id}/approve | /reject
                                   (nur Anführer — Freigabe/Ablehnung eines
                                   wartenden Diplomaten-Auszahlungsantrags)
POST /alliances/{id}/kasse/{auftrag_id}/cancel
                                   (nur der Antragsteller — bricht einen
                                   LAUFENDEN Transfer ab, s. u.)
GET  /alliances/{id}/kasse/log  ?limit&offset (Default 15/0) → ABGESCHLOSSENE Ein-/
                                   Auszahlungen (wer, wann, Richtung, Menge),
                                   neueste zuerst — für jedes Mitglied
                                   einsehbar. Bleibt eine reine Liste (kein
                                   Gesamt-Zähler) — eine volle Seite
                                   (Länge == limit) heißt "es könnte mehr
                                   geben".
GET  /alliances/{id}/kasse/bilanz
                                   → Buchhaltung je Mitglied (s. u.), für
                                   jedes Mitglied einsehbar.
POST /alliances/{id}/diplomacy  {other_alliance_id, type: krieg|buendnis|nap|neutral}
POST /alliances/{id}/broadcast  {message}       (Rundbrief/Chat)
POST /alliances/{id}/description {text}         (leader/diplomat — öffentlicher Beschreibungstext)
GET  /alliances/flaggen-optionen                → {icons: [...], farben: {schluessel: hex}}
                                   (festes wählbares Set, s. u. — Route MUSS
                                   vor /{id} geroutet sein)
POST /alliances/{id}/flagge     {icon, farbe}    (leader/diplomat) → {icon, farbe}
GET  /users/lookup             ?name=           → öffentliches Profil (für Einladungen; siehe §2)

Diplomatie-Semantik: krieg wirkt einseitig & sofort. buendnis/nap sind Angebote — aktiv, sobald die Gegenseite denselben Typ setzt. neutral kündigt Angebote/Bündnisse/NAPs sofort; ein laufender Krieg endet erst, wenn BEIDE Seiten neutral setzen (Friedensangebot wird protokolliert). handel-Missionen (§7) mit Einheiten sind zu JEDER Insel erlaubt, bewohnt oder unbewohnt, unabhängig von Allianz-Zugehörigkeit. Die punkte je Mitglied in der Mitgliederliste kommen aus dem täglichen Tages-Snapshot, NICHT live — Name/Inselanzahl/Rolle bleiben live.

Allianz-Flagge: Icon + Grundfarbe aus einem festen Set (balancing.ALLIANZ_FLAGGEN, kein Freitext/Upload), setzbar von Anführer/Diplomat über POST /alliances/{id}/flagge. GET /alliances/{id} liefert zusätzlich flagge: {icon, farbe} | null (null = noch keine gewählt). farbe ist der SCHLÜSSEL aus ALLIANZ_FLAGGEN["farben"] (z. B. "blau"), nicht der Hex-Wert — den liefert GET /alliances/flaggen-optionen. Dieselbe Flagge erscheint außerdem bei fremden Flotten in Wachturm-Sichtweite: GET /fleets/incoming (§7) liefert für jeden fremden absender-Eintrag (ankommend UND vorbeifahrend) zusätzlich allianz_flagge: {tag, icon, farbe} | null.

24h-Sperrzeit: Sobald eine Diplomatie- Beziehung einen settled Zustand erreicht (Krieg erklärt, Bündnis/NAP bestätigt, Frieden geschlossen, Bündnis/NAP gekündigt), ist ein Wechsel in einen ANDEREN Typ für ALLIANZ["diplomatie_sperre_stunden"] (Default: 24h) gesperrt — verhindert Hin- und Her-Wechseln zwischen den Zuständen im Minutentakt. Ein Versuch während der Sperre liefert 409 diplomacy_locked mit dem Zeitpunkt, ab dem wieder geändert werden darf. Laufende Angebote/ Friedensverhandlungen (Status angeboten/friede_angeboten) sind von der Sperre NICHT betroffen — Annehmen/Ablehnen/Zurückziehen geht immer sofort. Eine gekündigte Beziehung wird intern als neutral mit Sperrfrist gespeichert (statt gelöscht) und verschwindet aus der Diplomatie-Liste, sobald die Sperre abgelaufen ist.

Kasse-Transferzeit: Ein-/Auszahlung wirken NICHT mehr sofort, sondern erst nach ALLIANZ["kasse_transferzeit_h"] (Default 2h, unter Turbo TURBO_FAKTOR-mal kürzer wie überall sonst). Einzahlung: Rohstoffe werden SOFORT von der Insel abgezogen, kommen aber erst nach der Transferzeit in der Kasse an. Auszahlung durch den ANFÜHRER: der Betrag wird SOFORT aus der Kasse gebucht (reserviert, kein Doppel- Ausgeben), kommt aber erst nach der Transferzeit auf der Zielinsel an — Lagerkapazität wird ERST BEI ANKUNFT geprüft (kann sich in 2h ändern), was dann nicht passt, geht zurück in die Kasse.

Kasse-Deckel: ALLIANZ["kasse_kappung"] (150.000) je Rohstoff — greift ebenfalls erst bei ANKUNFT einer fälligen Ein-/Auszahlung, analog zur Lagerkapazität einer Insel. Was darüber ankäme, geht verloren (kein Zurückbuchen, keine Fehlermeldung beim Einzahlen selbst — der Betrag wird wie gehabt sofort von der Insel abgezogen). Der Kasse-Log (GET /alliances/{id}/kasse/log) zeigt dabei die TATSÄCHLICH gutgeschriebene Menge, nicht den ursprünglich eingezahlten Betrag.

GET /alliances/{id} liefert für Mitglieder zusätzlich kasse_unterwegs: [{id, richtung, resources, spieler, user_id, fertig_at, status, insel}] — alle noch nicht abgeschlossenen Transfers. status: "wartet_auf_freigabe" | "aktiv" | "storniert" (s. u.). user_id identifiziert den Antragsteller — der Client zeigt ihm damit den Abbrechen-Button nur bei eigenen Transfers. insel: {name, koordinaten} | null — NUR beim eigenen Eintrag gefüllt (user_id == Aufrufer), bei fremden Einträgen immer null, auch wenn der Rest sichtbar bleibt.

Einzahlung gesperrt bei erkanntem Angriff: POST .../kasse/deposit liefert 409 attack_incoming, wenn der Wachturm der einzahlenden Insel gerade eine anfliegende Angriffsflotte in Sichtweite hat (dieselbe Sichtweiten-Logik wie GET /fleets/incoming) — verhindert, Rohstoffe kurz vor einem erkannten Angriff in die Kasse zu "retten". Betrifft nur Einzahlungen, keine Auszahlungen.

Erwartete Zahlung: GET /islands/{id} liefert zusätzlich kasse_zahlung_erwartet_at: datetime | null — Zeitpunkt der nächsten bereits laufenden (nicht mehr wartenden) Allianz-Kasse- Auszahlung, die auf DIESER Insel ankommt. GET .../kasse/log zeigt weiterhin nur ABGESCHLOSSENE Bewegungen (mit der tatsächlich angekommenen Menge, bei Auszahlung ggf. weniger als angefordert wegen Lagerkapazität).

Kasse-Gebühr: jede erfolgreiche Ein- ODER Auszahlung kostet ALLIANZ["kasse_gebuehr_anteil"] (Default 12%) Gebühr, abgerundet (floor). Die Gebühr wird ERST BEI ANKUNFT abgezogen (nicht beim Antrag) — resources in der POST .../deposit//withdraw-Antwort und in kasse_unterwegs ist weiterhin der VOLLE angeforderte Betrag; erst der tatsächlich gutgeschriebene (Netto-)Betrag in GET .../kasse/log und .../kasse/bilanz ist bereits um die Gebühr bereinigt. Bei Auszahlung wird der Kasse trotzdem der VOLLE angeforderte Betrag belastet (die Kasse muss also den vollen Betrag decken können, nicht nur den Netto-Betrag) — die Gebühr "verschwindet" einfach, sie fließt niemandem zu. Ein stornierter Transfer (s. u.) ist GEBÜHRENFREI — das ist keine Ein-/Auszahlung, sondern nur die Rückgabe des eigenen Geldes zum Ursprung.

Rolle "Schatzmeister" + Auszahlungs-Freigabe: vierte Allianz-Rolle NUR für die Kasse zuständig (keine invite/diplomatie/rundbrief-Rechte wie Anführer/Diplomat). JEDES Mitglied darf eine Auszahlung ANFRAGEN (POST .../kasse/withdraw, vorher nur Anführer/Diplomat). Anführer UND Schatzmeister können sich dabei selbst jederzeit SOFORT auszahlen (status: "aktiv", fertig_at gesetzt — wie oben) UND fremde Anträge freigeben/ablehnen. Alle anderen Anträge (Diplomat, Mitglied) belasten die Kasse NOCH NICHT — der Auftrag bekommt fertig_at: null, status: "wartet_auf_freigabe". Erst POST .../kasse/withdraw/{auftrag_id}/approve (Anführer ODER Schatzmeister) belastet die Kasse (erneute Prüfung — kann sich seit dem Antrag geändert haben) und startet die Transferzeit; POST .../reject verwirft den Antrag ersatzlos (Kasse wurde nie belastet). Beide Aktionen schicken dem Antragsteller eine Postfach-Meldung (Ergebnis). Die Benachrichtigung AN Anführer/Schatzmeister über einen NEUEN Antrag ist dagegen KEINE Postfach-Meldung, sondern rein eine farbige Hervorhebung des Allianz-Navireiters im Web-Client (aus kasse_unterwegs abgeleitet, kein eigener Endpunkt). kasse_unterwegs-Einträge mit status: "wartet_auf_freigabe" stehen zuerst in der Liste, unabhängig von fertig_at (das ist bei denen ja null).

Transfer abbrechen: POST /alliances/{id}/kasse/{auftrag_id}/cancel — NUR der Antragsteller (auftrag.user_id), NUR für einen LAUFENDEN Transfer (fertig_at gesetzt, status: "aktiv"), NUR einmal (ein bereits "storniert"er Transfer kann nicht erneut abgebrochen werden — 404 not_found). Die Rohstoffe fahren zum URSPRUNG zurück (Insel bei Einzahlung, Kasse bei Auszahlung) — die Rückfahrt dauert GENAUSO LANGE wie die bisher verstrichene Zeit seit Transferbeginn (analog zu fleets.recall_fleet, Rückweg = bisherige Strecke), NICHT sofort und NICHT erneut die volle Transferzeit. Beispiel: nach 10 Minuten abgebrochen ⇒ fertig_at wird auf "jetzt + 10 Minuten" gesetzt. Antwort: {id, fertig_at, storniert: true}. Ein wartender ("wartet_auf_freigabe") Antrag kann NICHT über diesen Endpunkt storniert werden (dafür gibt es .../reject, die Kasse wurde da ja noch nie belastet).

Buchhaltung: GET /alliances/{id}/kasse/bilanz[{user_id, spieler, einzahlung: {gold, stein, holz}, auszahlung: {gold, stein, holz}, saldo: {gold, stein, holz}}] — Summe ALLER abgeschlossenen Bewegungen je Mitglied, aus AllianceKasseLog aggregiert (nicht aus kasse_unterwegs, die sind ja noch nicht abgeschlossen). Die Beträge sind die NETTO-Beträge nach Abzug der Kasse-Gebühr (s. o.) — ein stornierter Transfer zählt dabei ohne Gebühr (voller Rückfluss). saldo = einzahlung − auszahlung je Rohstoff, bewusst OHNE Untergrenze — ein Mitglied, das mehr entnommen als eingezahlt hat, zeigt hier negative Werte ("Summiere eingezahlt und ausgezahlt zusammen, auch wenn dann Minuszahlen entstehen"). Absteigend nach Gesamtvolumen (Einzahlung + Auszahlung, NICHT nach Saldo) sortiert. Ein stornierter und zurückgekehrter Transfer zählt hier als die TATSÄCHLICHE Bewegung (z. B. eine abgebrochene Einzahlung, die zur Insel zurückkehrt, zählt als Auszahlung), nicht als der ursprünglich beantragte Transfer. Gelöschte Accounts UND Mitglieder, die die Allianz inzwischen verlassen haben oder gekickt wurden, werden zu einer gemeinsamen Zeile user_id: null, spieler: "unbekannt" zusammengefasst — nur AKTUELLE Mitglieder erscheinen namentlich (wie beim Kasse-Protokoll, aber zusätzlich für Ex-Mitglieder mit noch existierendem Account).

10. Marktplatz & Handel

GET  /market/offers            ?want=&offer=      → Angebote filtern
POST /market/offers            {offer_res, offer_amount, want_res, want_amount}
POST /market/offers/{id}/accept {from_island_id}  → erzeugt Transport-Fleet
DELETE /market/offers/{id}
# Freier (riskanter) Versand:
POST /transfers                {from_island_id, target:{x,y,z}, resources}
# Handelsrouten (wiederkehrend, eigene Inseln):
GET/POST/DELETE /trade-routes      POST: {from_island_id, to_island_id,
                                          resources, interval_h}

Marktplatz/Marktangebote sind aktuell deaktiviert (POST /islands/{id}/ buildings/marktplatz/upgrade → 409 building_disabled; POST /market/offers → 409 feature_disabled) — Endpunkte, Code und Daten bleiben vollständig erhalten, siehe balancing.DEAKTIVIERTE_GEBAEUDE. Markt-Semantik (F16b- Defaults, Werte in balancing.py → MARKT), falls/wenn reaktiviert: Angebot braucht Marktplatz ≥ 1; angebotene Menge + Gebühr wird beim Einstellen hinterlegt (Escrow — das Verhältnis ist abgesichert). Kapazität je Angebot/ Annahme = 500 × Marktplatz-Stufe; max. Angebote je Insel = 2 + Stufe; Gebühr = 10 % − 0,4 %/Stufe. Lieferungen fahren als abstrakte Markt- Handelsschiffe (4 Knoten, echte Fahrzeit) — keine eigenen Schiffe nötig. Zurückziehen/Ablauf erstattet die Menge, die Gebühr verfällt.

Freier Versand (/transfers) nutzt EIGENE Handelsschiffe (automatisch gewählt, größte zuerst) — ohne jede Absicherung. Handelsrouten liefern mit eigenen Handelsschiffen; fehlen Schiffe/Rohstoffe, wird der Lauf übersprungen. Beide sind AKTIV, unabhängig vom deaktivierten Marktplatz.

11. Postfach

GET  /messages                 ?folder=inbox|combat  (ignoriert, wenn archiv=true)
                                &archiv=true          → NUR archivierte (hidden=true),
                                                        ordnerübergreifend gemischt
                                &limit=&offset=        (Pagination, s. u.)
POST /messages                 {recipient, subject, body}
POST /messages/{id}/read
POST /messages/{id}/archive    → Soft-Hide (hidden=true), jeder Nachrichtentyp
DELETE /messages/{id}
GET  /messages/verlauf/{other_user_id}
                                → Nachrichtenverlauf mit einem Spieler

DELETE /messages/{id}: bei folder=combat (Kampf-/Spionageberichte) NUR ein Soft-Hide (hidden=true, verschwindet aus GET /messages) — Kampfberichte werden nie wirklich gelöscht, sie bleiben für spätere Statistiken im System (GRUNDBEDINGUNGEN §2.6b). Bei folder=inbox (normale Nachrichten) echtes Löschen der Zeile.

Archiv: POST /messages/{id}/archive setzt bei JEDER Nachricht (Kampf-/Spionagebericht UND Postfach) hidden=true — dieselbe Soft-Hide-Mechanik wie das bisherige DELETE bei folder=combat, nur als eigene benannte Aktion. Für Postfach- Nachrichten ist das eine ZUSÄTZLICHE Option NEBEN dem weiterhin bestehenden echten DELETE (Löschen) — der Web-Client zeigt dort beide Aktionen. Für Kampf-/Spionageberichte ersetzt "archivieren" im Web-Client das bisherige "ausblenden" 1:1 (kein separates Löschen dort, wie schon vorher). GET /messages?archiv=true zeigt NUR archivierte Nachrichten, ignoriert dabei folder komplett und mischt Kampf-/Spionageberichte UND Postfach- Nachrichten chronologisch (neueste zuerst) in einer Liste — Pagination über limit/offset wie gehabt (Web-Client nutzt limit=25 mit Vor-/Zurück).

GET /messages/verlauf/{other_user_id}{spieler, nachrichten: [{id, subject, body, created_at, von_mir}]} — BEIDE Richtungen chronologisch (älteste zuerst), nur echte Spieler- Nachrichten (folder=inbox UND sender_id gesetzt — System-/Kampf-/ Spionageberichte sind kein Zwiegespräch und tauchen hier nie auf). Markiert dabei automatisch alle noch ungelesenen Nachrichten VOM anderen Spieler AN mich als gelesen (wie beim Öffnen einer Einzelnachricht). Message-Objekte aus GET /messages tragen dafür zusätzlich sender_id (null bei System-/ Kampfberichten) — der Web-Client zeigt "Antworten" nur, wenn sender_id gesetzt UND folder=inbox UND kein Kampf-/Spionage-/Einladungs-Payload.

11a. Forum (öffentlich + Allianz-intern)

GET  /forum/threads            ?sichtbarkeit=oeffentlich|allianz  (optional; ohne
                                Parameter beide, je nach Sichtbarkeitsregel gemischt)
                                &limit=&offset=
POST /forum/threads            {titel, body, sichtbarkeit?: "oeffentlich"|"allianz"}
                                sichtbarkeit-Default: "oeffentlich"
GET  /forum/threads/{id}       ?limit=&offset=   → Thema + alle Beiträge
POST /forum/threads/{id}/posts {body}            → Antwort auf ein Thema
POST /forum/posts/{id}/report                     → melden (jeder eingeloggte Spieler)
DELETE /forum/posts/{id}                          (eigener Beitrag, Admin, oder
                                                    Allianz-Führung bei Allianz-Themen)
DELETE /forum/threads/{id}                        (eigenes Thema, Admin, oder
                                                    Allianz-Führung bei Allianz-Themen)

Ein bewusst stark vereinfachtes In-Game-Brett — kein Editieren, keine Kategorien/Unterforen, nur Themen + Antworten. Nur eingeloggt erreichbar (kein öffentlicher HTTP-Endpunkt wie bei §2a).

Sichtbarkeit: jedes Thema ist "oeffentlich" (für alle sichtbar) oder "allianz" (nur für Mitglieder GENAU dieser Allianz + Admin). Ein allianzinternes Thema anlegen ohne eigene Allianz → 403 not_member. Zugriff auf ein fremdes Allianz-Thema (View/Antworten/Melden) → 404 not_found (keine Info-Preisgabe über die Existenz, analog zu fremden Inseln). Admins sehen und moderieren alle Themen unabhängig von eigener Allianz-Mitgliedschaft.

Moderation: POST /forum/posts/{id}/report ist idempotent (mehrfaches Melden durch denselben Spieler erzeugt keinen zweiten Eintrag) und für jeden eingeloggten Spieler erlaubt — nur klein sichtbar im Frontend, kein Sofort-Effekt. Die Felder gemeldet (Themenliste) und melde_anzahl (Beitrag-Detail) kommen von der API nur mit, wenn der Aufrufer is_admin ist — normale Spieler sehen nie, ob/wie oft etwas gemeldet wurde.

Löschen: jeder Spieler darf seinen EIGENEN Beitrag oder sein eigenes Thema jederzeit löschen. Admin darf zusätzlich alles löschen (protokolliert im Admin-Audit-Log). Bei einem Allianz-Thema (sichtbarkeit: "allianz") darf zusätzlich die Führung DIESER Allianz (Rolle anfuehrer oder diplomat) fremde Beiträge/Themen löschen — aber nur innerhalb der eigenen Allianz, nicht im öffentlichen Forum (dort bleibt fremdes Löschen admin-only). Löscht man den letzten Beitrag eines Themas, verschwindet das dann leere Thema automatisch mit. Themenliste und Thema-Detail liefern je Eintrag darf_loeschen: bool, damit der Client den Löschen-Link nur zeigt, wenn die API ihn auch tatsächlich zulässt — die eigentliche Prüfung ist serverseitig.

Link-Verbot: Titel und Beitragstext (Thema wie Antwort) werden serverseitig gegen eine Link-Heuristik geprüft (Protokoll-URLs, www., wortartige domain.tld-Muster mit KLEINGESCHRIEBENER Endung ohne Leerzeichen) — bei Treffer 422 link_verboten. Eine Heuristik, kein vollständiger Parser; erkennt nicht jede Verschleierung, löst aber nicht bei normalen Satzenden ohne Leerzeichen aus (Bugfix 2026-08-19: die Endung musste ursprünglich nur 2+ Buchstaben sein, GROSS ODER klein — jeder Satz ohne Leerzeichen nach einem Punkt vor einem großgeschriebenen Folgewort, z. B. "Danke.Bis später", wurde dadurch fälschlich blockiert; echte Domains/TLDs sind praktisch nie großgeschrieben, die Endung ist jetzt auf Kleinbuchstaben beschränkt).

12. Ranglisten, Quests

GET /rankings/players          → nach Gesamtpunkten
GET /rankings/alliances        → nach Punkten
GET /stats/overview            → öffentlich, KEIN Login nötig — Server-Kennzahlen
                                  (Spieler/Inseln/Allianzen/Flotten unterwegs),
                                  Top-10-Spieler/Top-5-Allianzen, weltweiter
                                  Einheiten-/Schiffsbestand (Häfen) UND was
                                  gerade auf See ist (Schiffe + Einheiten in
                                  Flotten). Auch als Seite unter /statistik
                                  (HTML, von der Startseite verlinkt).
GET /quests                    → Tutorial + Meilensteine + heutige Dailies + Fortschritt
POST /quests/{id}/claim        → Belohnung (Rohstoffe) abholen

Rangliste live vs. eingefroren: Rang/Punkte/ Differenz kommen aus dem täglichen Snapshot und ändern sich erst beim nächsten Tageswechsel. Name/Inseln/Allianz werden dagegen bei JEDEM Abruf frisch aus der Datenbank nachgeladen (live) — ein Allianzwechsel oder gelöschter Account ist also sofort sichtbar, nur die Punktzahl/der Rang selbst bleibt bis Mitternacht fix. Gelöschte Spieler/Allianzen werden aus dem Snapshot entfernt, sobald sie beim nächsten Abruf nicht mehr existieren. GET /rankings/players liefert außerdem online: bool je Spieler — ebenfalls live (letzte Aktivität < ONLINE_SCHWELLE_MIN Minuten), analog zum online-Feld im öffentlichen Profil (§ Nutzer); die genaue letzte Aktivität selbst bleibt privat, nur der Bool wird ausgeliefert. rang_aenderung: int | null (beide Endpunkte): Platzveränderung seit dem VORHERIGEN Tages-Snapshot — positiv = aufgestiegen, negativ = abgestiegen, 0 = unverändert, null = kein Vortages-Rang vorhanden (Konto/Allianz ist neu). Wie differenz nur beim täglichen Snapshot neu berechnet, nicht live.

Allianz-Flagge (beide Endpunkte, live wie Name/Allianz): GET /rankings/alliances liefert je Allianz zusätzlich flagge: {icon, farbe} | null; GET /rankings/players liefert sie verschachtelt unter allianz.flagge (§9). Tageswechsel = Mitternacht in der Server-Zeitzone Europe/Berlin (DST- sicher via app.core.timeutil.server_datum()), NICHT UTC-Mitternacht — Fix 2026-07-05, vorher wechselte der Snapshot erst 1–2h nach der deutschen Mitternacht, was wie ein Berechnungsfehler wirkte.

Öffentliche Auszeichnungen: quests_erfuellt im öffentlichen Profil (§2) zeigt NUR Quests aus einer fest hinterlegten Whitelist, die nichts über die eigene Insel oder deren Ausbaustufen verrät (z. B. Allianz beigetreten, Kampf gewonnen, Insel erobert, geplündert, spioniert, Kontoalter, sowie die drei Lieferungs-/Kolonisations-Quests unten) — Gebäude-/Forschungs-/Punkte-Quests bleiben privat.

Meilensteine M13–M15 (neu): - M13 Truppentransport — 1.000 Steinewerfer in EINER Lieferung verschickt (Maximum je Einzellieferung zählt). - M14 Großhändler — 100.000 Rohstoffe INSGESAMT verschickt (Summe über alle Lieferungen — nicht auf einmal, bewusst andere Semantik als M13). - M15 Gescheiterte Landung — 100 gescheiterte Kolonisationsversuche erlebt.

Dailies-Pool auf 15 Aufgaben erweitert (balancing.DAILIES["pool"]); pro Tag werden weiterhin 3 davon angeboten, deterministisch per HMAC(Server-Seed, Datum) ausgewählt — für alle Spieler am selben Tag garantiert identisch.

Fortschritt je Quest: jeder Eintrag in GET /quests (tutorial/meilensteine/dailies) trägt zusätzlich fortschritt: {aktuell, ziel, prozent}prozent ist bereits auf 100 gedeckelt und für die Anzeige gerundet (1 Nachkommastelle), erfuellt bleibt weiterhin die maßgebliche Quelle für "abholbar" (nicht prozent >= 100, auch wenn beides deckungsgleich ist). Bei kombinierten Bedingungen (gebaeude_stufen, forschung_max — mehrere Anforderungen auf EINER Insel) ist ziel die Anzahl der Teilbedingungen und aktuell die Summe der je Teilbedingung anteilig erfüllten Fortschritte (nicht alles-oder-nichts) der besten Insel.

13. Admin

Interne Admin-/Testhilfe-Endpunkte (nur is_admin) — bewusst NICHT Teil dieser öffentlichen API-Dokumentation und aus der automatischen OpenAPI/ Swagger-Doku (/docs) ausgeschlossen.

14. WebSocket-Kanäle

WS /ws                          → Benachrichtigungen (Auftrag fertig, Flotte
                                  angekommen, Angriff im Anflug, neue Nachricht)

Auth beim WS-Handshake via ?token= (JWT) oder ?api_key=. Es gibt KEINEN Live-Kampf-Playback-Kanal mehr (WS /ws/battles/{id}, entfernt 2026-07-19) — der Kampfausgang UND der volle Bericht stehen sofort bei Ankunft der Flotte fest, keine Berichtssperre mehr (§8, §14a.3).


14a. Dynamische Payloads & Events (vollständige Referenz)

Viele Antworten sehen je nach Rolle/Status/Admin-Recht unterschiedlich aus — Felder werden nicht nur mit null befüllt, sondern weggelassen. Diese Referenz listet ALLE Varianten, damit ein Bot nicht raten muss, welche Felder auftauchen können.

14a.1 Message.payload (Postfach, §11) — je nach Nachrichtentyp

Kampfbericht — Angreifer, Sieg:

{
  "battle_id": 42, "rolle": "angreifer", "sieg": true,
  "insel_name": "Nebelinsel", "insel_koordinaten": "12:34:5",
  "eingesetzt": {"angreifer": {"units": {}, "ships": {}},
                "verteidiger": {"units": {}, "ships": {}}},
  "verluste": {"angreifer": {}, "verteidiger": {}},
  "razzia": {"goldmine": 1},
  "stein_rueckgewinnung": 35,
  "holz_rueckgewinnung": 14,
  "loot": {"gold": 120, "stein": 40},
  "erobert": false,
  "aufklaerung": null,
  "aufklaerung_status": "kein_spaehschiff",
  "rng": {"eroberung": {"wurf": 0.71, "schwelle": 0.5}},
  "text_voll": "…"
}

insel_name/insel_koordinaten stehen in JEDER Kampfbericht-Variante (Angreifer Sieg/Niederlage, Verteidiger) sowie in GET /battles/{id} — Inselnamen sind ohnehin überall öffentlich sichtbar (GET /map/island), keine zusätzliche Info-Preisgabe. text_voll ist identisch zu Message.body (§11) — sofort ab Erstellung verfügbar, keine Verzögerung/Freischaltung mehr (bis 2026-07-19 gab es hier noch einen neutralen Platzhalter-Body bis Kampfende, s. 14a.3). aufklaerung ist entweder null oder ein Schnappschuss (siehe 14a.2). aufklaerung_status: "kein_spaehschiff" (kein Spähschiff dabei) | "erfolgreich" | "fehlgeschlagen" (Würfel nicht getroffen). rng-Keys variieren je nachdem, welche Zufallsentscheidungen im Kampf gefallen sind (angriffsaufklaerung, eroberung, …), jeweils {wurf, schwelle}. aufklaerung.rohstoffe wird NACH der Plünderung erfasst — zeigt bei erfolgreicher Zusatz-Aufklärung also den Bestand ABZÜGLICH loot, nicht den Stand vor dem Angriff. stein_rueckgewinnung/holz_rueckgewinnung sind die Stein-/Holz-Mengen, die der VERTEIDIGER (nicht der Angreifer) aus den razzierten Gebäuderesten zurückbekommt (8 % bzw. 5 % des jeweiligen Baukostenanteils der zerstörten Stufen; Gold ist verloren, kein gold_rueckgewinnung-Feld) — stehen in BEIDEN Rollen-Payloads (Angreifer-Sieg und Verteidiger) zur Info, gutgeschrieben wird nur der Zielinsel. Beide 0, wenn keine Stufe zerstört wurde oder das Gebäude vorher schon Stufe 0 war. razzia kann seit 2026-07-24 ZWEI Einträge enthalten (statt nur einem): war razzia_ziel die Steinmauer und wurde diese vollständig zerstört, geht überschüssige Angriffskraft im selben Angriff auf das Haupthaus über, z. B. {"steinmauer": 1, "haupthaus": 2}stein_rueckgewinnung/ holz_rueckgewinnung sind dann bereits über beide Gebäude aufsummiert. erobert: true: die gelandeten Truppen des Angreifers fahren, soweit Kämpfer-Kapazität der überlebenden Kriegsschiffe reicht, mit der Flotte zurück — nur der Überhang bleibt als neue Garnison auf der eroberten Insel. Die zurückfahrende Flotte kann also weniger Einheiten enthalten als am Ende des Landkampfs überlebt hatten.

Kampfbericht — Angreifer, Niederlage (eingesetzt/verluste beider Seiten sind AUCH bei Niederlage sichtbar, nicht nur bei Sieg):

{
  "battle_id": 42, "rolle": "angreifer", "sieg": false,
  "insel_name": "Nebelinsel", "insel_koordinaten": "12:34:5",
  "eingesetzt": {"angreifer": {"units": {}, "ships": {}},
                "verteidiger": {"units": {}, "ships": {}}},
  "verluste": {"angreifer": {}, "verteidiger": {}},
  "verteidiger_name": "SpielerName",
  "text_voll": "…"
}

Kein razzia/loot/aufklaerung (die gibt's nur bei Sieg) — die Zusatz-Aufklärung (Gebäude/Rohstoffe/Schiffsbestand der Insel) bleibt weiterhin exklusiv an Spähschiff + Sieg gebunden, NUR der reine Kampfverlauf (wer mit wieviel eingesetzt/verloren hat) ist jetzt immer sichtbar. verteidiger_name ist null, wenn das Ziel unbewohnt war.

Kampfbericht — Verteidiger (immer vollständig, unabhängig vom Ausgang):

{
  "battle_id": 42, "rolle": "verteidiger", "sieg": true,
  "insel_name": "Nebelinsel", "insel_koordinaten": "12:34:5",
  "eingesetzt": {"angreifer": {}, "verteidiger": {}},
  "verluste": {"angreifer": {}, "verteidiger": {}},
  "razzia": {}, "stein_rueckgewinnung": 0, "holz_rueckgewinnung": 0, "loot": {},
  "angreifer": "SpielerName", "text_voll": "…"
}

Spionage — Ergebnis versenkt:

{"typ": "spionage", "ergebnis": "versenkt", "ziel": "12:34:5",
 "insel_name": "Nebelinsel",
 "rng": {"chance": 0.3, "wurf": 0.81, "entdeckt": true, "versenkt": true}}

insel_name steht in allen drei typ: "spionage"-Varianten (versenkt/erfolg/fehlschlag) — NICHT in den spionage_abwehr-Varianten (die betreffen die eigene Insel des Empfängers, der kennt sie bereits). Spionage-Abwehr — Ziel wurde ausspioniert, Späher versenkt:

{"typ": "spionage_abwehr", "ergebnis": "versenkt", "angreifer": "SpielerName"}

Spionage — Erfolg:

{"typ": "spionage", "ergebnis": "erfolg", "ziel": "12:34:5",
 "bericht": {"info_tiefe": 3, "rohstoffe": {"gold": 900, "stein": 400, "holz": 200},
            "gebaeude": {"haupthaus": 5}, "truppen": {"steinewerfer": 20}},
 "rng": {"chance": 0.6, "wurf": 0.22}}

bericht ist nach info_tiefe gestaffelt — Felder kommen ERST ab der jeweiligen Tiefe dazu (niedrigere Tiefe = weniger Felder, nicht null): info_tiefe 1: nur rohstoffe. ≥2: zusätzlich gebaeude (ALLE Gebäudetypen inkl. noch nicht gebauter mit Stufe 0 — deaktivierte Gebäude wie Labor/Marktplatz sind ausgenommen). ≥3: zusätzlich truppen. ≥4: zusätzlich schiffe, forschung, punkte (int), produktion_pro_h ({gold, stein, holz} je Std.), lagerkapazitaet (int, je Rohstoff), pluenderschutz (int, je Rohstoff) und bauauftraege (Liste aller laufenden/wartenden Bauaufträge der Zielinsel, je Eintrag {gebaeude, ziel_stufe, status: "aktiv"|"wartend", finish_at}finish_at ISO-8601 oder null solange der Auftrag noch wartet). Spionage — Fehlschlag (kein bericht-Feld):

{"typ": "spionage", "ergebnis": "fehlschlag", "ziel": "12:34:5", "rng": {...}}

Spionage-Abwehr — nur gesichtet (Angreifer bleibt anonym, kein angreifer-Feld):

{"typ": "spionage_abwehr", "ergebnis": "gesichtet"}

Allianz-Einladung: {"alliance_id": 3, "invite": true} Allianz-Rundbrief: {"alliance_id": 3, "rundbrief": true}

Neue Insel nach Verlust der letzten Insel — geht an einen Spieler, der durch eine Eroberung GAR KEINE Insel mehr besitzt; er bekommt automatisch (noch im selben resolve_attack()-Lauf) eine neue Startinsel inkl. Anfängerschutz, analog zur Registrierung (services/islands.py create_start_island()):

{"typ": "insel_verloren", "neue_insel": "53:48:20", "neue_insel_id": 163}

folder="combat", wie Kampf-/Kolonisationsberichte. Besitzt der Spieler nach der Eroberung noch andere Inseln, entfällt diese Nachricht komplett.

14a.2 Schnappschuss-Shape (in aufklaerung und Spionage-bericht verwendet)

{"rohstoffe": {"gold": 900, "stein": 400, "holz": 200},
 "gebaeude": {"haupthaus": 5, "goldmine": 3},
 "truppen": {"steinewerfer": 20}, "schiffe": {"spaehschiff": 1},
 "forschung": {"segel": 2}}

Welche Top-Level-Keys vorhanden sind, hängt vom Kontext ab (Aufklärung durch Spähschiff im Angriff vs. gestufter Spionagebericht, siehe oben).

14a.3 (entfernt) — vormals Gesperrter Bericht während der Berichtssperre

Es gab hier früher eine 3-minütige Sperre (davor Live-Playback, davor 15 Min) zwischen Flotten-Ankunft und vollem Kampfbericht — GET /battles/{id} und GET /messages/GET /messages/{id} lieferten bis dahin nur einen reduzierten Platzhalter (fertig: false, hinweis/Fortschrittsbalken-Felder). Komplett entfernt 2026-07-19 — Kampfausgang UND voller Bericht stehen jetzt im selben Moment fest, battle.end_at == battle.start_at, es gibt kein fertig-Feld und keinen Platzhalter mehr.

14a.4 GET /battles/{id} — volle Ansicht, sofort ab Ankunft (§8)

Alle Varianten haben zusätzlich insel_name/insel_koordinaten (§14a.1). Verteidiger (immer voll): id, start_at, end_at, rolle:"verteidiger", ergebnis, timeline, eingesetzt, verluste, razzia, stein_rueckgewinnung, holz_rueckgewinnung, loot, conquered, angreifer_name. Angreifer + Sieg: id, start_at, end_at, rolle:"angreifer", ergebnis, timeline, eingesetzt, verluste, razzia, stein_rueckgewinnung, holz_rueckgewinnung, loot, conquered, aufklaerung, aufklaerung_status, rng, verteidiger_name. Angreifer + Niederlage (nicht rudimentär — nur die Zusatz-Aufklärung fehlt): id, start_at, end_at, rolle:"angreifer", ergebnis, timeline, eingesetzt, verluste, verteidiger_name, hinweis. Kein razzia/loot/conquered/aufklaerung (die gibt's nur bei Sieg). verteidiger_name ist null bei unbewohntem Ziel.

timeline-Einträge (phase: "see"|"land") tragen seit 2026-07-09 zusätzlich angriffsstaerke/verteidigungsstaerke (die tatsächlich verglichenen Werte, die über Sieg/Niederlage entscheiden — VOR den Verlusten). Der "land"- Eintrag trägt außerdem basis_inselverteidigung (0 oder KAMPF ["basis_inselverteidigung"]=50 — 50 gilt bereits ab 1 verteidigender Einheit, 0 nur bei einer komplett leeren Zielinsel). Ältere, vor diesem Datum gespeicherte Battles haben diese Felder nicht (fehlen einfach, kein null).

Der "see"-Eintrag trägt seit 2026-07-24 zusätzlich rest_angriffsstaerke — die Angriffsstärke der ÜBERLEBENDEN Kriegsschiffe des Angreifers, 0, wenn der Angreifer das Seegefecht verliert (auch bei Teil-Überleben). Dieser Wert geht, sofern der Angreifer am Ende auch gewinnt, zusätzlich zur Landtruppen-Rest-Angriffsstärke in die Belagerung ein — s. u.

14a.5 WS /ws — genau 4 Event-Typen (kein weiterer)

{"typ": "verbunden"}
{"typ": "nachricht", "id": 7, "folder": "inbox", "subject": "Hallo"}
{"typ": "flotte_ankunft", "fleet_id": 12, "state": "outbound|returning|done"}
{"typ": "angriff_im_anflug", "ziel": "12:34:5", "mission": "attack",
 "arrive_at": "2026-07-06T14:00:00Z"}

14a.6 (entfernt) — vormals WS /ws/battles/{id} Live-Playback-Frames

Es gab hier früher einen Live-Playback-Kanal, der die Kampf-Timeline frame-weise über WebSocket "abspielte", plus eine kurze Berichtssperre danach. Beides 2026-07-19 komplett entfernt — Kampfausgang UND voller Bericht stehen jetzt im selben Moment fest, keine Live-Daten, kein Platzhalter, keine Wartezeit mehr (§14a.3).

Der timeline-Eintrag der belagerung-Phase in battle.timeline (§14a.4) enthält weiterhin stein_rueckgewinnung und holz_rueckgewinnung (beide int) — identisch zu den Top-Level-Feldern im Kampfbericht (14a.1). Seit 2026-07-24 zusätzlich angriffsstaerke (die tatsächlich für die Razzia verbrauchte Gesamt-Rest-Angriffsstärke) sowie die Aufschlüsselung angriffsstaerke_land/angriffsstaerke_schiffe — wie viel davon von überlebenden Landtruppen bzw. überlebenden Kriegsschiffen stammt.

14a.7 Rollen-/Admin-abhängige Felder außerhalb von Kampf/Spionage


15. Beispiel

Angriff starten: (Typ-Schlüssel sind IMMER Deutsch, siehe balancing.SCHIFFE/ EINHEITEN — z. B. kleines_kriegsschiff, nicht small_warship)

POST /api/v1/fleets
X-API-Key: sk_live_...
{
  "origin_island_id": 12,
  "mission_type": "attack",
  "target": { "x": 20, "y": 8, "z": 20 },
  "ships": { "kleines_kriegsschiff": 10, "spaehschiff": 1, "kleines_handelsschiff": 5 },
  "units": { "bogenschuetze": 80, "steinewerfer": 40 },
  "razzia_ziel": "goldmine",
  "razzia_prioritaet": "gold"
}
→ 201 {
  "id": 88, "mission": "attack", "origin_island_id": 12,
  "target": { "x": 20, "y": 8, "z": 20 },
  "ships": { "kleines_kriegsschiff": 10, "spaehschiff": 1, "kleines_handelsschiff": 5 },
  "units": { "bogenschuetze": 80, "steinewerfer": 40 }, "resources": {}, "loot": {},
  "state": "outbound", "speed_knots": 10, "distanz_felder": 42,
  "depart_at": "2026-07-02T18:20:00Z", "arrive_at": "2026-07-02T18:40:00Z",
  "return_at": null, "recallable_until": "2026-07-02T18:30:00Z"
}

mission im Response ist die vom Server aus der Fracht ABGELEITETE konkrete Mission (hier attack, da weder nur Spähschiffe noch ein Kolonisationsschiff an Bord sind) — siehe §7.

zurück zu Seekampf