Dev Roadmap Dokumentation
Lumora Engine ← Zurück zum Board

Lumora Engine — Content-Coding-Guide

Diese Seite zeigt, wie du Content für die Lumora-Engine schreibst: Items, Crates, Bosse, Abilities und mehr. Du brauchst dafür kein Wissen über die Engine-Interna — eine kleine, in sich geschlossene Klasse pro Inhalt genügt.

📌
Die goldene Regel

Die Engine entwickelt Gameplay. Content nutzt Gameplay. Content beschreibt nur, was existiert — die Engine entscheidet, wie es umgesetzt wird. Diese beiden Welten werden niemals vermischt.

Lumora ist die neue MMO-Engine (Paper-Plugin), die JxnSMP ablöst. Sie ist strikt in Engine, Systems und Content getrennt. Als Content-Autor arbeitest du fast ausschließlich im Ordner content/ — du legst eine neue Datei an, beschreibst deinen Inhalt und bist fertig. Kein zentrales Register, keine Engine-Klasse, die du anfassen musst.

Getting Started

Bevor wir loslegen, die drei Dinge, die du über das Arbeiten mit Lumora wissen musst. Sie gelten für jeden Content-Typ — Boss, Item, Effekt, Ability.

1. Wo lege ich Content ab?

Aller Content lebt unter src/main/java/dev/lumora/content/<kategorie>/. Die Regel ist klar: ein Unterordner pro Content-Stück (benannt nach der id), in dem die Einstiegsklasse und alle ihre Helfer-Klassen zusammenliegen — auch bei Single-File-Content. Der Bootstrap-Scan findet Klassen in beliebiger Tiefe, also kostet das nichts und hält Ordnung, wenn viele Inhalte dazukommen.

content/ bosses/ icetitan/ # Multi-File: alle Helfer im selben Ordner IceTitan.java # implements Content (Einstieg) IceTitanHealth.java # extends HealthComponent IceTitanLoot.java # extends LootComponent IceTitanFrostbite.java # extends BossComponent IceTitanWard.java # extends BossComponent mobs/ frostwolf/ Frostwolf.java # implements Content (Einstieg) FrostwolfLoot.java # extends LootComponent FrostwolfChill.java # extends MobComponent effects/ frostbite/ Frostbite.java # extends CustomEffect items/ flameblade/ FlameBlade.java # extends CustomItem contentorb/ ContentOrb.java # extends CustomItem abilities/ flameblade/ FlameBlade.java # extends Ability crates/ common/ rare/ legendary/ # je ein Ordner pro Crate quests/ slaythetitan/ SlayTheTitan.java # implements Content

2. Wie wird er gefunden? (Auto-Discovery)

Es gibt keine Registrierungsliste. Beim Serverstart durchsucht ContentBootstrap das Plugin-Jar nach allen Klassen unter dev.lumora.content.**in beliebiger Tiefe, also auch in deinen Unterordnern — die eine Content-Kategorie erfüllen (z. B. implements Content oder extends CustomEffect), erzeugt jede über ihren parameterlosen Konstruktor und ruft register() auf. Hilfsklassen ohne Kategorie (z. B. deine IceTitanHealth) werden ignoriert — sie werden nur von deinem Boss instanziiert, nicht vom Bootstrap.

📁
Die goldene Regel: neuer Ordner statt Engine-Edit

Neuer Content = ein neuer Unterordner unter content/, sonst nichts. Du fasst nie eine Engine- oder Bootstrap-Datei an, um etwas zu „registrieren". Genau das macht parallele Arbeit merge-frei: zwei Devs legen zwei neue Ordner an und kommen sich nie in der Quere.

⚠️
Parameterloser Konstruktor + eigene Datei

Jede Content-Klasse braucht einen no-arg-Konstruktor (der Default reicht). Innere, nicht-statische Klassen und anonyme Klassen werden übersprungen — darum bekommt jeder Inhalt seine eigene .java-Datei.

3. Wie teste ich, dass es ankam?

Nach dem Deploy (siehe unten) prüfst du im Spiel mit /lumora content verify (Alias /lum), ob dein Inhalt erkannt und pro Engine registriert wurde. Der Befehl listet jede gefundene Content-id auf — taucht deine id auf, ist die Discovery durch.

Bauen, Deployen & Testen

Lumora wird mit Gradle gebaut und auf den geteilten Test-Server kopiert. Der komplette Zyklus:

# 1) Bauen + Jar direkt in den Test-Server kopieren
./gradlew deploy        # -> ../Paper/plugins/Lumora-<version>.jar

# (nur Jar bauen, ohne kopieren:)
./gradlew shadowJar     # -> build/libs/Lumora-<version>.jar

Danach den Server neu starten (..\Paper\start.bat) — Lumora discovert Content nur beim Start.

🚫
Niemals /reload

Plugins sauber neu laden geht bei Paper nicht — /reload lässt Lumora in einem halben Zustand zurück. Immer den ganzen Server neu starten.

🧹
Nach yml-Änderungen: alte Configs löschen

Lumora schreibt Standard-Configs nach plugins/Lumora/*.yml und überschreibt sie nicht. Hast du das Default-Schema geändert, lösche die veralteten plugins/Lumora/*.yml, damit sie beim nächsten Start frisch erzeugt werden.

JDK-Hinweis

Gradle ist in gradle.properties auf JDK 21 gepinnt (Gradle 8.x läuft nicht auf JDK 25). Diesen Pin nicht entfernen.

Kurz-Checkliste für jede Änderung:

./gradlew deploy Server-Restart /lumora content verify im Spiel testen

Das Zwei-Schichten-Prinzip

Lumora trennt sauber zwischen Content-Authoring und Engine-Interna. Als Content-Autor berührst du nur Layer A.

Layer A — Du

Content-Authoring

Spielend einfach. Eine in sich geschlossene Klasse pro Inhalt, in einer eigenen Datei unter content/<typ>/. Wird automatisch erkannt — kein zentrales Register.

Layer B — Engine

Engine-Interna

Strukturiert und stabil. Builder, Definition, Registry, Factory, Runtime, EventBus. Davon siehst du als Content-Autor nichts.

Auto-Discovery: zwei Devs, kein Merge-Konflikt

Es gibt keine zentrale Registrierungsliste, in die alle ihre Inhalte eintragen. Jeder Content liegt in seiner eigenen Datei und wird beim Start automatisch gefunden. Dadurch arbeiten zwei Entwickler an unterschiedlichen Inhalten, ohne sich jemals in derselben Datei in die Quere zu kommen.

Zwei gleichwertige Formen

Content gibt es in zwei Ausprägungen — beide leben unter content/:

  • Einfacher Content (Items, Crates): eine Klasse, die CustomItem bzw. CustomCrate erweitert und register() mit einem inline-Fluent-DSL überschreibt.
  • Strukturierter Content (Bosse, Mobs usw.): eine Klasse, die Content implementiert, einen statischen Builder aufruft und Component-Klassen verdrahtet (Health, Loot, Spawn, AI …).

Der geteilte Entity-Core

Boss und Mob fühlen sich beim Schreiben identisch an — und das ist kein Zufall. Genau wie Minecraft selbst (Entity → LivingEntity → Mob, ein Boss ist nur ein Mob + Bossbar) teilen sich Boss und Mob in Lumora einen Core unter engine/entity/. Dort lebt alles, was eine lebende Entität ausmacht — an einer Stelle, von allen geerbt:

  • EntityBuilder<SELF>alle Entity-Regler (Aussehen, Ausrüstung, Attribute, Flags, Immunitäten, FX, Sounds, Selbst-Effekte, Drops). Self-typed (CRTP), damit jede Fluent-Methode den konkreten Builder-Typ zurückgibt.
  • EntityShell — unveränderlicher Snapshot dieser Config (jede Definition hält genau einen).
  • EntityShellApplier — wendet die Shell beim Spawn auf das frische Bukkit-Entity an (Name, Glow, Scale, Attribute, Ausrüstung, Effekte, Flags). Der eine geteilte Spawn-Pfad.
  • EntityComponent<R> — die Hooks (onSpawn/onTick/onHit/onDamaged/onIncomingDamage/onDeath), typisiert auf die jeweilige Runtime.

BossBuilder und MobBuilder erben von EntityBuilder; BossComponent extends EntityComponent<BossRuntime>, MobComponent extends EntityComponent<MobRuntime>. Ergebnis: derselbe Content-Code. Genau deshalb sehen Boss- und Mob-Authoring gleich aus — ein Boss ergänzt nur Bossbar + Phasen, ein Mob lässt sie weg.

EntityBuilder<SELF>┬→ BossBuilder (+ bossbar, phases)
└→ MobBuilder (+ maxHealth, abilities, loot)
🧬
Ein neuer Entity-Regler = eine Stelle

Kommt ein neuer gemeinsamer Knopf dazu (z. B. ein Flag), wird er einmal im EntityBuilder + EntityShell + EntityShellApplier verdrahtet — und steht sofort bei Boss und Mob (und künftigen Entity-Typen) zur Verfügung.

Verfügbare Engines

Lumora besteht aus mehreren spezialisierten Engines. Für jede erstellst du Content auf dieselbe Art:

🐉 boss ability ⚔️ item 🎁 crate 👾 mob 🧙 npc 📜 quest 💬 dialogue 🏰 dungeon 🌌 dimension 🗺️ region 🌩️ world-event 💰 loot

Wohin gehören die Dateien?

Jeder Content-Typ hat seine eigene Kategorie unter content/, und darin bekommt jedes einzelne Content-Stück seinen eigenen Unterordner. Ein Boss bringt alle seine Component-Klassen in diesem Ordner mit; ein Single-File-Content (Effekt, Item) bekommt ebenfalls einen Ordner:

content/ bosses/icetitan/ # IceTitan.java + IceTitanHealth/Loot/Frostbite/Ward.java mobs/frostwolf/ # Frostwolf.java + FrostwolfLoot/Chill.java effects/frostbite/ # Frostbite.java (extends CustomEffect) abilities/flameblade/ # FlameBlade.java (extends Ability) items/flameblade/ # FlameBlade.java (extends CustomItem) items/contentorb/ # ContentOrb.java (extends CustomItem) crates/common/ crates/rare/ crates/legendary/ quests/slaythetitan/ # SlayTheTitan.java (implements Content)
💡
Ein Unterordner pro Content-Stück

Lege für jeden neuen Inhalt einen eigenen Ordner an, statt einen bestehenden zu füllen. Multi-File-Content (wie icetitan) hat alle Helfer im selben Ordner. Die Auto-Discovery findet alles in beliebiger Tiefe automatisch — du musst nirgendwo etwas „anmelden".

Tutorial: Dein erster Boss in 6 Schritten

Vom leeren Editor zum lauffähigen Mini-Boss. Wir bauen einen simplen "frost_imp" — kein Schnickschnack, nur das Nötigste, damit er spawnt und kämpft. Jeder Schritt baut auf dem vorigen auf.

Schritt 1 — Eine leere Content-Klasse anlegen

Lege einen eigenen Ordner an und darin die Datei content/bosses/frostimp/FrostImp.java. Sie implementiert Content — das ist der Einstiegspunkt, den die Auto-Discovery findet.

package dev.lumora.content.bosses.frostimp;

import dev.lumora.api.content.Content;

public final class FrostImp implements Content {
    @Override public void register() {
        // kommt gleich
    }
}

Schritt 2 — Den Builder mit einer id starten

In register() rufst du BossBuilder.create("id"). Die id ist der eindeutige Name, über den der Boss überall referenziert wird.

@Override public void register() {
    BossBuilder.create("frost_imp")
        .register();   // wirft noch — Health fehlt (Schritt 4)
}

Schritt 3 — Aussehen geben: Name & Basis-Mob

displayName(...) nimmt MiniMessage, baseEntity(...) bestimmt den Vanilla-Mob, auf dem der Boss aufsetzt.

BossBuilder.create("frost_imp")
    .displayName("<aqua>Frost Imp")
    .baseEntity(EntityType.VEX)
    .register();

Schritt 4 — Die Pflicht-Component: Health

Ein Boss muss eine HealthComponent haben, sonst wirft register(). Lege dafür eine zweite Datei in denselben Ordner: content/bosses/frostimp/FrostImpHealth.java:

package dev.lumora.content.bosses.frostimp;

import dev.lumora.engine.boss.component.HealthComponent;

public final class FrostImpHealth extends HealthComponent {
    @Override public double maxHealth() { return 60; }
}

Schritt 5 — Health verdrahten + abschließen

Reiche die Component in den Builder und gib dem Imp noch ein paar Werte mit. Das .register() am Ende meldet ihn bei der Engine an.

@Override public void register() {
    BossBuilder.create("frost_imp")
        .displayName("<aqua>Frost Imp")
        .baseEntity(EntityType.VEX)
        .health(new FrostImpHealth())   // Pflicht erfüllt
        .attackDamage(4)
        .scale(1.3)
        .bossBarColor(BossBar.Color.BLUE)
        .register();
}

Schritt 6 — Deployen, spawnen & testen

Bauen, kopieren, Server neu starten — dann den Imp direkt im Spiel spawnen:

./gradlew deploy        # Jar -> ../Paper/plugins/
# Server neu starten (..\Paper\start.bat) — NICHT /reload

# im Spiel (Permission: lumora.boss.admin):
/boss list              # listet alle Boss-ids + aktive Anzahl
/boss spawn frost_imp   # spawnt den Imp an deiner Position
/lumora content verify  # optional: prueft die Auto-Discovery

/boss spawn <id> hat Tab-Vervollständigung für alle registrierten Bosse. Steht frost_imp in /boss list, ist er registriert und spawnbar. (Programmatisch geht es zusätzlich über BossAPI.spawn("frost_imp", location) — z. B. aus einem World-Event.)

🎉
Fertig — ein lauffähiger Mini-Boss

Zwei kleine Dateien, kein Engine-Code. Von hier aus erweiterst du ihn mit Loot, FX, Immunitäten, Phasen oder eigenen Components — alles weiter unten.

Tutorial: Dein erster Mob in 4 Schritten

Ein Mob wird mit denselben Methoden gebaut wie ein Boss — nur ohne Bossbar und Phasen (beide erben vom Entity-Core). Wir bauen einen "snow_rabbit". Wenn du das Boss-Tutorial kennst, kommt dir hier alles bekannt vor.

Schritt 1 — Eine Content-Klasse anlegen

Eigener Ordner, eigene Datei: content/mobs/snowrabbit/SnowRabbit.java. Auch ein Mob implementiert Content.

package dev.lumora.content.mobs.snowrabbit;

import dev.lumora.api.content.Content;
import dev.lumora.engine.mob.MobBuilder;
import org.bukkit.entity.EntityType;

public final class SnowRabbit implements Content {
    @Override public void register() {
        // kommt gleich
    }
}

Schritt 2 — Builder starten & Basis geben

MobBuilder.create("id") statt BossBuilder — sonst dieselben Aussehen-/Basis-Methoden. Anders als beim Boss ist keine HealthComponent nötig: die HP setzt du direkt mit maxHealth(...).

MobBuilder.create("snow_rabbit")
    .displayName("<white>Snow Rabbit")
    .baseEntity(EntityType.RABBIT)
    .maxHealth(12)
    .register();

Schritt 3 — Verhalten mit den geerbten Reglern

Attribute, Immunitäten, FX, Sounds, Drops — alles dieselben Methoden wie beim Boss (sie kommen aus dem geteilten EntityBuilder). Mob-Extras: loot(...) und component(...).

MobBuilder.create("snow_rabbit")
    .displayName("<white>Snow Rabbit")
    .baseEntity(EntityType.RABBIT)
    .maxHealth(12)
    .movementSpeed(0.4)
    .immuneTo(DamageCause.FREEZE)
    .ambientParticle(Particle.SNOWFLAKE, 2)
    .xpDrop(3)
    .register();

Schritt 4 — Deployen & spawnen

./gradlew deploy
# Server neu starten — NICHT /reload

# im Spiel (Permission: lumora.mob.admin):
/mob list                # listet alle Mob-ids + aktive Anzahl
/mob spawn snow_rabbit   # spawnt den Mob an deiner Position
🐇
Gleiches Authoring wie ein Boss

Du hast nichts Neues gelernt — nur MobBuilder statt BossBuilder und /mob spawn statt /boss spawn. Loot und eigene Hook-Components funktionieren identisch (siehe Mob bauen).

Tutorial: Deinen ersten Effekt bauen

Effekte sind ein geteiltes System: einmal gebaut, von Boss, Mob, Ability und Item nutzbar. Wichtig: ein Lumora-Effekt macht echte eigene Logiknicht bloß addPotionEffect(vanilla). Wir bauen einen "burning"-Effekt, der direkt Feuer anlegt, und rufen ihn dann aus einer Component auf.

Schritt 1 — Von CustomEffect erben & register()

Eigener Ordner, eigene Datei: content/effects/burning/Burning.java. In register() deklarierst du id, Standarddauer, einen Anzeigenamen (MiniMessage, wird über der Hotbar gezeigt) und optional einen Panel-Carrier (dazu unten mehr).

package dev.lumora.content.effects.burning;

import dev.lumora.engine.effect.CustomEffect;
import org.bukkit.Particle;
import org.bukkit.entity.LivingEntity;
import org.bukkit.potion.PotionEffectType;

public final class Burning extends CustomEffect {
    @Override protected void register() {
        setId("burning");
        setDisplayName("<red>🔥 Burning");          // Actionbar: "🔥 Burning Ns"
        setPanelCarrier(PotionEffectType.UNLUCK);   // optional: Eintrag im Inventar-Panel
        defaultDuration(100);                       // Ticks, falls apply() 0 übergibt
    }
}

Schritt 2 — Die Hooks mit echter Logik füllen

Du überschreibst nur, was du brauchst: onApply (einmalig beim Start), onTick (jeden Tick), onRemove (am Ende). Innen volle Bukkit-Freiheit — hier setzen wir das Entity tatsächlich in Brand, kein Vanilla-Trank:

@Override public void onTick(LivingEntity entity) {
    entity.setFireTicks(20);   // hält das Feuer am Brennen, solange der Effekt läuft
    entity.getWorld().spawnParticle(Particle.FLAME,
        entity.getLocation().add(0, 1, 0), 5, 0.2, 0.3, 0.2, 0.01);
}

@Override public void onRemove(LivingEntity entity) {
    entity.setFireTicks(0);    // beim Ablauf sauber löschen
}

Schritt 3 — Den Effekt von einer Component aus auslösen

Effekte legt man per id über die statische EffectEngine.apply(...) auf — von überall (Boss, Mob, Ability, Item). Hier aus dem onHit-Hook einer Boss-Component:

public final class FrostImpIgnite extends BossComponent {
    @Override
    public void onHit(BossRuntime runtime, LivingEntity victim, double damage) {
        EffectEngine.apply(victim, "burning", 100);   // id + Dauer in Ticks
    }
}

Mit .component(new FrostImpIgnite()) im Builder hängst du den Hook an den Boss. Die "burning"-Datei wird automatisch entdeckt — kein Verdrahten nötig. Solange der Effekt aktiv ist, zeigt die Engine dem Spieler automatisch 🔥 Burning 5s über der Hotbar (mehr zur Anzeige im Effekt-System).

Tutorial: Eine eigene Component bauen

Eine BossComponent ist der Weg zu beliebigem Boss-Verhalten ohne Engine-Edits. Du erbst, wählst einen Hook und schreibst rohes Bukkit hinein.

Schritt 1 — Erben & einen Hook wählen

Es gibt sechs Hooks — onSpawn, onTick, onHit, onDamaged, onIncomingDamage, onDeath (alle im Hook-Kapitel erklärt). Überschreibe nur, was du brauchst.

Schritt 2 — Reagieren: onHit

Der einfachste Fall ist reagieren. IceTitanFrostbite legt beim Treffer einen Effekt auf das Opfer:

public final class IceTitanFrostbite extends BossComponent {
    @Override
    public void onHit(BossRuntime runtime, LivingEntity victim, double damage) {
        EffectEngine.apply(victim, "frostbite", 60);
    }
}

Schritt 3 — Eingreifen: onIncomingDamage

Components können auch in die Schadens-Pipeline eingreifen. IceTitanWard reduziert jeden eingehenden Schaden um 20 %:

public final class IceTitanWard extends BossComponent {
    @Override
    public void onIncomingDamage(BossRuntime runtime, EntityDamageEvent event) {
        event.setDamage(event.getDamage() * 0.8);   // oder event.setCancelled(true)
    }
}

Schritt 4 — Dein eigenes Mini-Beispiel: onTick mit Partikeln

Jetzt etwas Neues: eine Component, die jeden Tick einen rotierenden Partikelring um den Boss legt. Sie zeigt, dass im Hook das ganze Bukkit-API offen liegt — Entity, Welt, Mathe:

public final class SwirlingAura extends BossComponent {

    private double angle = 0;

    @Override
    public void onTick(BossRuntime runtime) {
        angle += Math.PI / 16;                       // jeden Tick ein Stück weiter
        var entity = runtime.entity();
        Location center = entity.getLocation().add(0, 1, 0);
        for (int i = 0; i < 6; i++) {
            double a = angle + (Math.PI / 3) * i;     // 6 Punkte gleichmäßig im Kreis
            Location p = center.clone().add(Math.cos(a) * 1.5, 0, Math.sin(a) * 1.5);
            entity.getWorld().spawnParticle(Particle.SOUL_FIRE_FLAME, p, 1, 0, 0, 0, 0);
        }
    }
}

Anhängen wie jede Component — .component(new SwirlingAura()). Eine Component darf eigenen Zustand halten (hier angle), weil pro Boss eine Instanz existiert.

🧩
Das ist das Fabric-Level-Versprechen

Kein NMS, kein Mixin. Was du in Fabric per Mixin in den Damage-/Tick-Pfad hängen würdest, schreibst du hier in einen Hook mit rohem Bukkit — in einer kleinen, wiederverwendbaren Klasse.

Ein Item hinzufügen

Items sind einfacher Content. Du erweiterst CustomItem und beschreibst das Item in register() über ein flüssiges DSL. Das ist alles — keine Registrierung, kein Engine-Code.

public final class FlameBlade extends CustomItem {
    @Override protected void register() {
        setId("flame_blade").material(Material.NETHERITE_SWORD)
            .displayName(Component.text("Flame Blade")).model(1).onUse("flame_blade");
    }
}
  • setId(...) — die eindeutige id, mit der überall auf das Item verwiesen wird.
  • material(...) — das Vanilla-Basismaterial.
  • displayName(...) — der angezeigte Name (Adventure Component).
  • model(...) — die CustomModelData für deine Textur im Resource-Pack.
  • onUse("flame_blade") — verknüpft das Item mit einer Ability über deren id.
🔗
Inhalte referenzieren sich per id

Das Item kennt die Ability-Klasse nicht — es nennt nur ihre id ("flame_blade"). Die Engine löst die Referenz zur Laufzeit auf. So bleiben Inhalte entkoppelt.

Eine Crate hinzufügen

Crates sind ebenfalls einfacher Content. Du erweiterst CustomCrate. Rarity, Key, Anzeige, Einmal-Nutzung und Loot werden vollständig im Builder beschrieben.

public final class DragonCrate extends CustomCrate {
    @Override protected void register() {
        setId("dragon").rarity(Rarity.LEGENDARY)
            .key("<gold>Dragon Key", Material.TRIPWIRE_HOOK, 13)  // oder .keyless()
            .display(DisplayKind.ITEM, Material.PAPER, 22, 1.2f)
            .singleUse(SingleUse.PER_PLAYER)                       // none | per_player | global
            .loot(LootEntry.weightedItem("flame_blade", 5, 1));
    }
}
  • key(name, material, model) — definiert den benötigten Schlüssel. Alternativ .keyless() für schlüssellose Crates.
  • display(kind, material, model, scale) — wie die Crate in der Welt dargestellt wird.
  • singleUse(...)NONE, PER_PLAYER oder GLOBAL.
  • loot(...) — gewichtete Loot-Einträge; verweist per id auf andere Inhalte (hier "flame_blade").

Rarity ist niemals auf feste Stufen beschränkt

Es gibt keine harte Liste aus drei Tiers. Du nutzt vorhandene Raritäten oder baust per Builder deine eigene — mit eigenem Namen und eigener Farbe:

// Eigene Rarity — niemals auf 3 Tiers beschränkt:
Rarity MYTHIC = Rarity.builder("mythic")
        .nameColor(NamedTextColor.LIGHT_PURPLE)
        .build();

Einen Boss bauen

Die Boss-Engine ist fertig und auf Fabric-Niveau: Ein Boss wird komplett deklarativ über den fluenten BossBuilder beschrieben — Aussehen, Ausrüstung, Attribute, Immunitäten, Partikel, Sounds, Selbst-Effekte, Loot, Bossbar, Phasen — und alles, was kein eingebauter Regler abdeckt, erledigt eine eigene BossComponent mit vollem Bukkit-Zugriff. Kein NMS, kein Mixin nötig: was Fabric per Mixin macht, machst du hier mit rohem Bukkit in einem Hook.

Die Einstiegsklasse implementiert Content, ruft BossBuilder.create("id"), hängt alle Regler an und schließt mit .register() ab. Sie wird beim Start automatisch gefunden (ContentBootstrap) — null Engine-Edits, merge-frei.

public final class IceTitan implements Content {
    @Override public void register() {
        BossBuilder.create("ice_titan")
            .displayName("<aqua><bold>Ice Titan")
            .baseEntity(EntityType.STRAY)
            .scale(1.6).glowing(true)
            .health(new IceTitanHealth())        // Pflicht: ohne Health wirft register()
            .loot(new IceTitanLoot())
            .component(new IceTitanFrostbite())  // eigener Hook: on-hit Frostbite
            .register();
    }
}
⚠️
Health ist Pflicht

register() wirft eine IllegalStateException, wenn keine HealthComponent gesetzt wurde. Jeder andere Regler ist optional — fehlt er, gilt der Vanilla-Standard.

Der Boss enthält keine Spiellogik. Er beschreibt nur seine Bestandteile. HP und Regeneration leben in einer HealthComponent, die Drops in einer LootComponent — beide haben genau eine Verantwortung und sind wiederverwendbar:

public final class IceTitanHealth extends HealthComponent {
    @Override public double maxHealth()             { return 300; }
    @Override public double regenerationPerSecond() { return 2; }
}
public final class IceTitanLoot extends LootComponent {
    @Override public void build(LootBuilder loot) {
        loot.dropVanilla(Material.DIAMOND, 1.0, 3, 6)
            .dropVanilla(Material.PACKED_ICE, 1.0, 8, 16)
            .dropItem("flame_blade", 0.5, 1, 1);   // CUSTOM-Item per id
    }
}

Die komplette Regler-Tabelle

Jede Methode des BossBuilder — gruppiert. Alle geben den Builder zurück, sind also frei verkettbar. Was hier fehlt, baust du als Component.

Aussehen

  • displayName(String mini) — Anzeigename als MiniMessage (z. B. "<aqua><bold>Ice Titan").
  • displayName(Component c) — Anzeigename als fertige Adventure-Component.
  • baseEntity(EntityType type) — die Vanilla-Mob-Basis (Standard ZOMBIE).
  • glowing(boolean) — Glow-Outline an/aus.
  • scale(double) — Körpergröße über das 1.21-SCALE-Attribut (z. B. 1.6 = 60 % größer).

Ausrüstung (was der Boss trägt/hält)

Jeder Slot nimmt entweder einen ItemStack oder die id eines Custom-Items (zur Spawn-Zeit aufgelöst).

  • equip(EquipmentSlot slot, ItemStack) / equip(EquipmentSlot slot, String customItemId) — generischer Slot-Setter.
  • mainHand(ItemStack) / mainHand(String id) — Haupthand.
  • offHand(ItemStack) — Nebenhand.
  • helmet / chestplate / leggings / boots(ItemStack) — Rüstungsslots.
  • equipmentDropChance(float) — Chance 0..1, dass jedes getragene Teil beim Tod droppt.

Attribute

Generisch jedes Minecraft-Attribut, plus benannte Bequemlichkeits-Setter:

  • attribute(Attribute, double) — beliebiges Attribut auf einen Basiswert.
  • movementSpeed(double), knockbackResistance(double), attackDamage(double), attackKnockback(double), followRange(double), armor(double), armorToughness(double).

Entity-Flags

  • silent(boolean) — keine Vanilla-Geräusche.
  • gravity(boolean) — Schwerkraft an/aus.
  • ai(boolean) — Mob-KI aktiv/inaktiv.
  • invulnerable(boolean) — unverwundbar.
  • collidable(boolean) — Kollision mit anderen Entities.
  • fireTicks(int) — Brenn-Ticks beim Spawn (-1 = keine).

FX & Sounds

  • ambientParticle(Particle, int count) — dauerhaftes Umgebungs-Partikel.
  • spawnSound(String), deathSound(String), hurtSound(String), ambientSound(String) — Sound-Keys (z. B. "minecraft:entity.wither.spawn").

Selbst-Effekte

  • effect(PotionEffect...) — Trank-Effekte, die der Boss beim Spawn auf sich selbst erhält (z. B. dauerhafte Resistenz).

Immunität

  • immuneTo(DamageCause...) — Schadensarten, gegen die der Boss komplett immun ist (z. B. FREEZE, DROWNING, FALL).

Drops & XP

  • xpDrop(int) — fallengelassene Erfahrung (-1 = Vanilla).
  • clearVanillaDrops(boolean) — Vanilla-Drops unterdrücken (nur dein LootComponent zählt).

Bossbar

  • bossBar(boolean show) — Bossbar ein-/ausblenden (Standard an).
  • bossBarColor(BossBar.Color) — Farbe der Bossbar (Standard RED).

Alle Regler als Einzeiler

Zum schnellen Abgucken — ein Beispielaufruf pro Regler. Alles ist frei verkettbar:

BossBuilder.create("demo")
    // Aussehen
    .displayName("<red><bold>Demo Boss")
    .baseEntity(EntityType.WITHER_SKELETON)
    .glowing(true)
    .scale(1.8)
    // Ausrüstung
    .mainHand("flame_blade")                       // Custom-Item per id
    .offHand(new ItemStack(Material.SHIELD))
    .helmet(new ItemStack(Material.NETHERITE_HELMET))
    .chestplate(new ItemStack(Material.NETHERITE_CHESTPLATE))
    .leggings(new ItemStack(Material.NETHERITE_LEGGINGS))
    .boots(new ItemStack(Material.NETHERITE_BOOTS))
    .equip(EquipmentSlot.HAND, new ItemStack(Material.STICK))
    .equipmentDropChance(0.1f)
    // Attribute
    .attribute(Attribute.MAX_HEALTH, 200)          // beliebiges Attribut generisch
    .movementSpeed(0.3)
    .knockbackResistance(0.8)
    .attackDamage(10)
    .attackKnockback(1.5)
    .followRange(40)
    .armor(10)
    .armorToughness(4)
    // Entity-Flags
    .silent(true)
    .gravity(true)
    .ai(true)
    .invulnerable(false)
    .collidable(true)
    .fireTicks(0)
    // FX & Sounds
    .ambientParticle(Particle.SMOKE, 6)
    .spawnSound("minecraft:entity.wither.spawn")
    .deathSound("minecraft:entity.wither.death")
    .hurtSound("minecraft:entity.wither_skeleton.hurt")
    .ambientSound("minecraft:entity.wither.ambient")
    // Selbst-Effekte
    .effect(new PotionEffect(PotionEffectType.FIRE_RESISTANCE, Integer.MAX_VALUE, 0))
    // Immunität
    .immuneTo(DamageCause.FIRE, DamageCause.LAVA, DamageCause.FALL)
    // Drops & XP
    .xpDrop(80)
    .clearVanillaDrops(true)
    // Bossbar
    .bossBar(true)
    .bossBarColor(BossBar.Color.PURPLE)
    // Verhalten
    .health(new MyHealth())                        // Pflicht
    .loot(new MyLoot())
    .abilities("ice_storm")
    .component(new MyComponent())
    .phase(0.5, rt -> { /* ... */ })
    .register();

Verhalten: Components & Phasen

  • health(HealthComponent)Pflicht. HP-Regeln.
  • loot(LootComponent) — Drop-Tabelle (vom Engine beim Tod gerollt).
  • spawn(SpawnComponent) — Spawn-Regeln.
  • ai(AIComponent) — eigenes KI-Verhalten (überladen gegenüber ai(boolean)).
  • abilities(String... ids) — Abilities per id verknüpfen.
  • component(BossComponent... comps) — beliebig viele Hook-Components (siehe unten).
  • phase(double atHpFraction, PhaseAction) — Phasenwechsel bei einem HP-Anteil 0..1.

Components & Hooks — der Fabric-Level-Teil

Eine BossComponent ist der Schlüssel zu unbegrenztem Boss-Verhalten ohne Engine-Edits. Ein Boss trägt beliebig viele davon (.component(a, b, c)); die Engine ruft den passenden Hook auf allen auf. Du überschreibst nur die Hooks, die du brauchst, und darfst innen alles tun: Trank-Effekte, Partikel, Blöcke, Sounds, Summons, Scheduler, rohes Bukkit. Neue Fähigkeit (Freeze-on-hit, Schild, Enrage, Adds spawnen, Wetter …) = eine neue Subklasse. Die Engine ändert sich nie.

BossComponent spezialisiert den geteilten EntityComponent<BossRuntime> — dieselben Hooks nutzt eine MobComponent (mit MobRuntime). Was du hier lernst, gilt 1:1 für Mobs.

Die Hooks

  • onSpawn(BossRuntime rt) — direkt nach dem Spawn.
  • onTick(BossRuntime rt) — jeden Boss-Tick.
  • onHit(BossRuntime rt, LivingEntity victim, double damage) — der Boss hat etwas getroffen (für On-Hit-Effekte).
  • onDamaged(BossRuntime rt, LivingEntity attacker, double damage) — der Boss hat Schaden bekommen (attacker kann null sein).
  • onIncomingDamage(BossRuntime rt, EntityDamageEvent event) — eingehender Schaden bevor er angewendet wird; die ganze Schadens-Pipeline. Mit event.setDamage(...) abschwächen/verstärken oder mit event.setCancelled(true) blocken. Feuert für jede Schadensart.
  • onDeath(BossRuntime rt) — der Boss ist gestorben (bevor Loot gerollt wird).

In jedem Hook erreichst du das lebende Bukkit-Entity über rt.entity() — ab da ist die volle Bukkit-API offen.

Beispiel 1 — On-Hit-Effekt (reagieren)

Wenn der Ice Titan etwas trifft, legt er den geteilten "frostbite"-Effekt auf das Opfer. Zwei Engine-Ideen auf einmal: der offene Hook und das wiederverwendbare Effekt-System:

public final class IceTitanFrostbite extends BossComponent {
    @Override
    public void onHit(BossRuntime runtime, LivingEntity victim, double damage) {
        EffectEngine.apply(victim, "frostbite", 60);
    }
}

Beispiel 2 — Die Schadens-Pipeline verändern (eingreifen)

Components reagieren nicht nur — sie greifen in die Pipeline ein. Diese hier reduziert jeden eingehenden Schaden um 20 %:

public final class IceTitanWard extends BossComponent {
    @Override
    public void onIncomingDamage(BossRuntime runtime, EntityDamageEvent event) {
        event.setDamage(event.getDamage() * 0.8);
    }
}
🧩
Alles mit rohem Bukkit = Fabric-Level

Es gibt keine gesonderte „Boss-Mechanik-API", die dich einschränkt. Im Hook hast du das Entity, die Welt, den Scheduler — also alles. Genau das, was du in Fabric per Mixin tätest, schreibst du hier in eine kleine, wiederverwendbare Component.

Phasen

Eine Phase feuert einmal, sobald der Boss auf (oder unter) einen HP-Anteil seines Maximums fällt. .phase(0.5, rt -> {...}) heißt „bei der Hälfte". Die PhaseAction ist völlig frei — den Boss buffen, Adds spawnen, Partikel umstellen, broadcasten, eine Ability zünden:

BossBuilder.create("ice_titan")
    // ...
    .phase(0.5, rt -> {                               // Enrage bei halber HP
        rt.entity().getWorld().strikeLightningEffect(rt.entity().getLocation());
        rt.entity().addPotionEffect(
            new PotionEffect(PotionEffectType.SPEED, Integer.MAX_VALUE, 1));
    })
    .register();

Das geteilte Effekt-System

Statuseffekte sind nicht boss-spezifisch — es ist ein eigenes, geteiltes System, nutzbar von Boss, Mob, Ability und Item. Ein Effekt liegt unter content/effects/<id>/: eine Klasse extends CustomEffect, die in register() ihre Metadaten deklariert und die Hooks überschreibt, die sie braucht — onApply, onTick (jeden Tick), onRemove.

Die register()-DSL

  • setId(String) — die eindeutige id, mit der der Effekt angewendet wird.
  • defaultDuration(int ticks) — Standarddauer, falls apply() 0 übergibt.
  • setDisplayName(String mini) — der Name, der dem Spieler angezeigt wird (MiniMessage, mit eigener Farbe/Logo).
  • setPanelCarrier(PotionEffectType) — optional: ein Vanilla-Effekt als „Träger", damit der Effekt im Inventar-Panel auftaucht (siehe unten).
⚙️
Ein Custom-Effekt macht ECHTE eigene Logik

Nicht bloß addPotionEffect(vanillaEffekt). Du hast die volle Bukkit-API: Attribute, Partikel, Blöcke, Sounds, Scheduler. Frostbite z. B. nutzt kein Vanilla-Potion, sondern verlangsamt direkt über einen MOVEMENT_SPEED-AttributeModifier.

Beispiel: Frostbite (echter Effekt, kein Vanilla-Trank)

Frostbite senkt das Tempo um 30 % über einen eigenen AttributeModifier mit eigenem NamespacedKey — in onApply hinzugefügt, in onRemove wieder entfernt — und sprüht jeden Tick Schnee:

public final class Frostbite extends CustomEffect {

    private static final NamespacedKey SLOW_KEY =
            new NamespacedKey("lumora", "effect_frostbite_slow");
    private static final double SLOW_FACTOR = -0.30; // -30% movement speed

    @Override protected void register() {
        setId("frostbite");
        setDisplayName("<aqua>❄ Frostbite");
        setPanelCarrier(PotionEffectType.UNLUCK);   // Inventar-Panel; im RP umskinnen
        defaultDuration(60);
    }

    @Override public void onApply(LivingEntity entity) {
        AttributeInstance speed = entity.getAttribute(Attribute.MOVEMENT_SPEED);
        if (speed == null) return;
        removeSlow(speed); // gegen stale Modifier absichern
        speed.addModifier(new AttributeModifier(
                SLOW_KEY, SLOW_FACTOR, AttributeModifier.Operation.MULTIPLY_SCALAR_1));
    }

    @Override public void onTick(LivingEntity entity) {
        entity.getWorld().spawnParticle(Particle.SNOWFLAKE,
                entity.getLocation().add(0, 1, 0), 3, 0.2, 0.3, 0.2, 0.01);
    }

    @Override public void onRemove(LivingEntity entity) {
        AttributeInstance speed = entity.getAttribute(Attribute.MOVEMENT_SPEED);
        if (speed != null) removeSlow(speed);
    }

    private static void removeSlow(AttributeInstance speed) {
        for (AttributeModifier m : new ArrayList<>(speed.getModifiers())) {
            if (m.getKey().equals(SLOW_KEY)) speed.removeModifier(m);
        }
    }
}

Anwenden lässt sich der Effekt von überall per id über die statische EffectEngine.apply(...) — aus einem Boss-/Mob-Hook, einer Ability, einem Item:

// entity, effekt-id, dauer in Ticks (<= 0 ⇒ Standarddauer des Effekts)
EffectEngine.apply(victim, "frostbite", 60);

Anzeige — zwei Wege

Ein Spieler sieht einen aktiven Effekt auf zwei Arten, die sich ergänzen:

  1. Actionbar (automatisch, ohne Resourcepack). Die Engine zeigt jeden aktiven Effekt über der Hotbar als displayName + " Ns" (Restsekunden), mehrere durch Abstand getrennt — z. B. ❄ Frostbite 3s. Dafür musst du nichts tun außer setDisplayName(...).
  2. Inventar-Panel (optional, via setPanelCarrier). Setzt du einen Carrier, legt die Engine für die Dauer diesen Vanilla-Effekt (still, mit Icon) auf den Spieler — so erscheint ein Eintrag mit Icon + Countdown im Effekt-Panel des Inventars. Das Resourcepack reskinnt den Carrier, damit er als dein Effekt liest.
🎨
Resourcepack-Reskin des Carriers (2 Dateien)

Für setPanelCarrier(PotionEffectType.UNLUCK) überschreibst du im RP:

  • Icon: assets/minecraft/textures/mob_effect/unluck.png (18×18)
  • Name (lang): "effect.minecraft.unluck": "❄ Frostbite"
🧩
Warum der Carrier-Umweg? (ehrlich)

Die mob_effect-Registry wird nicht zum Client gesynct (nur z. B. Enchantments/Biomes werden übertragen). Ein server-seitig registrierter „echter" Custom-Effekt kann auf einem Vanilla-Client also gar nicht gerendert werden. Deshalb: für die Logik dein CustomEffect, für die Panel-Darstellung ein vorhandener Vanilla-Effekt als Träger + Resourcepack-Reskin — kein Client-Mod nötig. Quelle: docs.papermc.io/paper/dev/registries.

♻️
Einmal schreiben, überall nutzen

Die Engine hält das Register aller CustomEffects und die aktiven Effekte pro Entity, tickt jeden, zeigt ihn an und entfernt ihn (inkl. Carrier), wenn die Dauer abläuft. Derselbe "frostbite"-Effekt dient dem Boss-On-Hit, dem Mob, einer Ability und einem Item — ohne Code zu kopieren.

Rezepte (Cookbook)

Kleine, fertige Schnipsel für häufige Wünsche. Jeder steckt in die BossBuilder.create(...)-Kette (bzw. in einen Hook).

Boss unsichtbar machen

Kein eigener Builder-Regler — per Selbst-Effekt INVISIBILITY:

.effect(new PotionEffect(PotionEffectType.INVISIBILITY, Integer.MAX_VALUE, 0, true, false))

Boss immun gegen Feuer

.immuneTo(DamageCause.FIRE, DamageCause.FIRE_TICK, DamageCause.LAVA)

Custom-Item in die Hand geben

.mainHand("flame_blade")                       // per id (Custom-Item)
.offHand(new ItemStack(Material.SHIELD))       // oder ein roher ItemStack

XP-Drop setzen & Vanilla-Drops abschalten

.xpDrop(50)
.clearVanillaDrops(true)                       // nur dein LootComponent zählt

Eigener Hurt-Sound

.hurtSound("minecraft:entity.stray.hurt")

Riesig & leuchtend, mit Umgebungs-Partikeln

.scale(2.5)
.glowing(true)
.ambientParticle(Particle.SNOWFLAKE, 8)

Bei halber HP zornig werden

.phase(0.5, rt -> {
    rt.entity().getWorld().strikeLightningEffect(rt.entity().getLocation());
    rt.entity().addPotionEffect(
        new PotionEffect(PotionEffectType.SPEED, Integer.MAX_VALUE, 1));
})

Beim Tod etwas tun (Hook)

public final class VictoryBroadcast extends BossComponent {
    @Override public void onDeath(BossRuntime runtime) {
        runtime.entity().getWorld().getPlayers().forEach(p ->
            p.sendMessage(Component.text("Der Boss ist besiegt!")));
    }
}

Effekt im Inventar-Panel anzeigen

In register() deines CustomEffect einen Vanilla-Carrier setzen — dann im Resourcepack dessen Icon + Namen überschreiben:

setPanelCarrier(PotionEffectType.UNLUCK);   // Träger fürs Inventar-Panel
assets/minecraft/textures/mob_effect/unluck.png      # 18x18 Icon
# + lang-Eintrag:  "effect.minecraft.unluck": "❄ Frostbite"

Vollbeispiel: Ice Titan

Der kanonische Referenz-Boss als ein zusammenhängender Block — er zeigt nahezu die gesamte Builder-Oberfläche, zwei Components (reagieren und eingreifen), eine Phase und das geteilte Effekt-System. Null Engine-Code.

public final class IceTitan implements Content {

    @Override
    public void register() {
        BossBuilder.create("ice_titan")
                .displayName("<aqua><bold>Ice Titan")
                .baseEntity(EntityType.STRAY)
                .glowing(true)
                .scale(1.6)
                .health(new IceTitanHealth())
                .loot(new IceTitanLoot())
                .component(new IceTitanFrostbite())              // on-hit freeze effect
                .component(new IceTitanWard())                   // -20% incoming damage (pipeline)
                .phase(0.5, rt -> {                              // enrage at half health
                    rt.entity().getWorld().strikeLightningEffect(rt.entity().getLocation());
                    rt.entity().addPotionEffect(new PotionEffect(PotionEffectType.SPEED, Integer.MAX_VALUE, 1));
                })
                .mainHand("flame_blade")                         // holds a CUSTOM item
                .helmet(new ItemStack(Material.DIAMOND_HELMET))
                .equipmentDropChance(0.15f)
                .movementSpeed(0.28).knockbackResistance(0.6).attackDamage(8).armor(8)
                .xpDrop(50).clearVanillaDrops(true)
                .immuneTo(DamageCause.FREEZE, DamageCause.DROWNING, DamageCause.FALL)
                .ambientParticle(Particle.SNOWFLAKE, 8)
                .spawnSound("minecraft:entity.wither.spawn")
                .deathSound("minecraft:entity.wither.death")
                .hurtSound("minecraft:entity.stray.hurt")
                .effect(new PotionEffect(PotionEffectType.RESISTANCE, Integer.MAX_VALUE, 0, true, false))
                .bossBarColor(BossBar.Color.BLUE)
                .register();
    }
}
⚠️
Niemals den Boss in die Engine schreiben

Kein if (id.equals("ice_titan")) { ... } irgendwo in der Engine. Die Engine darf keinen konkreten Inhalt kennen. Der Builder entscheidet — nicht die Engine.

Einen Mob bauen

Ein Mob ist der leichtere Zwilling eines Bosses: gebaut mit MobBuilder auf demselben Entity-Core, nur ohne Bossbar und ohne Phasen. Der ganze Aussehen-/Attribute-/Flags-/FX-/ Immunitäts-/Drops-Teil ist identisch zum Boss (er kommt aus EntityBuilder) — du nutzt also dieselben Methoden, die du oben schon kennst.

Was ein Mob zusätzlich hat

Gegenüber dem geteilten Entity-Core bringt MobBuilder nur diese Extras mit:

  • maxHealth(double) — die HP (setzt das MAX_HEALTH-Attribut; Standard 20). Kein separates HealthComponent nötig.
  • abilities(String... ids) — Abilities per id, die der Mob rotierend wirkt.
  • abilityInterval(int ticks) — Abstand zwischen zwei Ability-Casts (Standard 80 Ticks).
  • loot(LootComponent) — Drop-Tabelle (dieselbe LootComponent, die auch Bosse nutzen).
  • component(MobComponent... comps) — Hook-Components (gleiche Hooks wie Boss).

Alles andere — displayName, baseEntity, scale, movementSpeed, attackDamage, immuneTo, ambientParticle, hurtSound, xpDrop, effect, … — ist exakt die Boss-Oberfläche aus der Regler-Tabelle. Es gibt nur kein bossBar()/bossBarColor() und kein phase().

Eine Mob-Component

Eine MobComponent hat dieselben Hooks wie eine BossComponent (beide spezialisieren EntityComponent) — nur ist der Runtime-Typ MobRuntime. Authoring 1:1 wie beim Boss:

public final class FrostwolfChill extends MobComponent {
    @Override
    public void onHit(MobRuntime runtime, LivingEntity victim, double damage) {
        EffectEngine.apply(victim, "frostbite", 40);   // derselbe geteilte Effekt wie beim Boss
    }
}
🔁
Boss-Wissen = Mob-Wissen

Hooks, Loot, Effekte, Immunitäten, FX — alles, was du fürs Boss-Authoring gelernt hast, gilt 1:1 für Mobs. Der einzige Unterschied im Code: MobBuilder / MobComponent / MobRuntime statt der Boss-Pendants, plus maxHealth(...) statt eines HealthComponent.

Vollbeispiel: Frostwolf

Der Referenz-Mob als zusammenhängender Block — gebaut mit derselben Builder-Oberfläche wie ein Boss (minus Bossbar/Phasen), mit Loot und einer On-Hit-Component, die den geteilten "frostbite"-Effekt nutzt. Eigener Ordner content/mobs/frostwolf/.

public final class Frostwolf implements Content {

    @Override
    public void register() {
        MobBuilder.create("frostwolf")
                .displayName("<aqua>Frostwolf")
                .baseEntity(EntityType.WOLF)
                .maxHealth(40)
                .movementSpeed(0.35).attackDamage(5)
                .immuneTo(DamageCause.FREEZE)
                .ambientParticle(Particle.SNOWFLAKE, 4)
                .hurtSound("minecraft:entity.wolf.hurt")
                .deathSound("minecraft:entity.wolf.death")
                .xpDrop(10)
                .loot(new FrostwolfLoot())
                .component(new FrostwolfChill())   // on-hit frostbite — same shared effect a boss uses
                .register();
    }
}

Die Loot-Component ist dieselbe Klasse, die auch ein Boss benutzt:

public final class FrostwolfLoot extends LootComponent {
    @Override public void build(LootBuilder loot) {
        loot.dropVanilla(Material.BONE, 1.0, 1, 3)
            .dropVanilla(Material.PACKED_ICE, 0.5, 1, 2);
    }
}

Eine Ability hinzufügen

Abilities sind der Ort für vollständig freie Logik. Du erweiterst eine Ability-Basis und überschreibst execute(...) bzw. cast(...). Hier ist alles erlaubt: Partikel, Raytraces, Scheduler, eigene Mechaniken. Die Engine schränkt dich nicht ein.

public final class IceStorm extends Ability {
    @Override public void register() {
        setId("ice_storm").cooldown(8.0).manaCost(40);
    }

    @Override public void cast(AbilityContext ctx) {
        Player caster = ctx.caster();
        Location origin = caster.getEyeLocation();

        // Raytrace nach vorne
        RayTraceResult hit = caster.getWorld().rayTraceEntities(
                origin, origin.getDirection(), 25.0, e -> e != caster);

        // Partikel-Spirale + Schaden über einen Scheduler
        ctx.scheduler().repeat(0L, 2L, 30, tick -> {
            double radius = tick * 0.3;
            for (int i = 0; i < 8; i++) {
                double angle = (Math.PI / 4) * i + tick * 0.2;
                Location p = origin.clone().add(
                        Math.cos(angle) * radius, tick * 0.1, Math.sin(angle) * radius);
                caster.getWorld().spawnParticle(Particle.SNOWFLAKE, p, 3, 0, 0, 0, 0.01);
            }
        });

        if (hit != null && hit.getHitEntity() instanceof LivingEntity target) {
            target.damage(14.0, caster);
            target.setFreezeTicks(120);
        }
    }
}
Verknüpfung läuft über ids

Diese Ability heißt "ice_storm" — genau die id, die der Boss oben unter .abilities("ice_storm") nennt und ein Item unter .onUse(...) nutzen könnte. Inhalte verweisen aufeinander nur per id, niemals per direkter Klassenreferenz.

Troubleshooting & FAQ

🐉
Mein Boss spawnt nicht / wirft beim Start einen Fehler

Mit hoher Wahrscheinlichkeit fehlt die HealthComponentregister() wirft dann eine IllegalStateException („missing a HealthComponent — call .health(...)"). Setze .health(new DeinHealth()). Prüfe danach mit /boss list bzw. /lumora content verify, ob die id gelistet wird.

🎮
Wie spawne ich Boss/Mob zum Testen?

Direkt im Spiel: /boss spawn <id> (Permission lumora.boss.admin) bzw. /mob spawn <id> (Permission lumora.mob.admin) — beide mit Tab-Vervollständigung. /boss list / /mob list zeigen alle ids + die aktive Anzahl. Programmatisch: BossAPI.spawn(id, location) bzw. MobEngine.bound().spawn(id, location).

❄️
Mein Effekt tut nichts

Meist passt die id nicht: Der String in EffectEngine.apply(entity, "id", ticks) muss exakt der setId(...)-Wert im CustomEffect sein. Außerdem muss die Effekt-Klasse unter content/effects/<id>/ liegen und einen no-arg-Konstruktor haben, sonst wird sie nicht entdeckt. Check via /lumora content verify, ob der Effekt unter der effect-Engine auftaucht.

🔁
Meine Änderung greift nicht

Drei Klassiker, in dieser Reihenfolge: (1) Hast du ./gradlew deploy laufen lassen? (2) Hast du den Server neu gestartet (kein /reload)? (3) Bei yml-Änderungen: alte plugins/Lumora/*.yml löschen, damit sie frisch erzeugt werden.

📋
Wie weiß ich, ob mein Content erkannt wurde?

/lumora content verify (Alias /lum) listet jede entdeckte Content-id pro Engine. Taucht deine id dort auf, hat die Auto-Discovery sie gefunden und registriert.

🧱
Meine Hilfsklasse (z. B. ein zweiter Helper) taucht nicht auf — ist das ein Bug?

Nein. Der Bootstrap registriert nur Klassen, die eine Content-Kategorie erfüllen (implements Content, extends CustomEffect, …). Reine Hilfsklassen wie IceTitanHealth werden bewusst übersprungen — sie werden von deinem Content instanziiert, nicht vom Bootstrap.

NPCs & mehr in Arbeit

🚧
Weitere Entity-Typen auf demselben Core

Bosse und Mobs sind fertig und teilen sich den Entity-Core. Weitere Entity-Typen (z. B. NPCs, Pets) entstehen nach exakt demselben Muster — ein Content-Einstieg, ein fluenter Builder, der von EntityBuilder erbt, und offene Hook-Components über EntityComponent. Solange sie noch gebaut werden, dokumentieren wir bewusst keine NPC-/Pet-API, um nichts Falsches zu versprechen. Das Boss-/Mob-Authoring unten zeigt aber schon genau, wie es aussehen wird.

Layer B: Engine-Interna (nur zur Einordnung)

Damit du verstehst, was unter der Haube passiert — du musst das nicht anfassen: Jede Engine folgt demselben internen Stack. Dein Builder-Aufruf wandert da hindurch:

Builder Definition Registry Factory Runtime RuntimeManager SubEngine Component Event API
  • Definition = unveränderliche Daten (das „Rezept" deines Inhalts).
  • Runtime = lebendiger Zustand pro Instanz (z. B. der konkrete Boss in der Welt).
  • Kommunikation läuft über einen EventBus (publish / @Subscribe) — Engines bleiben entkoppelt.
🔒
Content-Autoren sehen davon nichts

Du rufst BossBuilder.create(...) auf und bist fertig. Definition, Registry, Factory und Runtime entstehen automatisch im Hintergrund.

Die goldenen Regeln

Halte dich an diese Prinzipien, dann bleibt dein Content sauber und die Engine stabil:

1️⃣
Die Engine kennt niemals konkreten Content

Kein if (id.equals("ice_titan")) in der Engine. Konkrete Inhalte tauchen ausschließlich in content/ auf.

2️⃣
Content beschreibt, die Engine führt aus

Du sagst was existiert. Das wie ist Sache der Engine.

3️⃣
Wiederverwendung statt Kopieren

Brauchst du dieselbe Logik zweimal, wird sie zur wiederverwendbaren Component — nicht zu kopiertem Code. Wenn du oft kopieren musst, fehlt der Engine vermutlich eine Funktion.

4️⃣
Eine Klasse = eine Verantwortung

Health, Loot, Spawn, AI — jede Component macht genau eine Sache.

5️⃣
Nichts ist eingeschränkt

Eigene ids, eigene Raritäten, eigene Texturen, vollständig eigenes Verhalten in Abilities. Die Engine vereinfacht — sie beschränkt deine Kreativität nicht.

Die wichtigste Frage bei jeder neuen Klasse

„Ist das Engine oder Content?" Ist die Antwort nicht sofort klar, überdenke den Code noch einmal. Im Zweifel: Es ist Content, und es gehört nach content/.