Zum Hauptinhalt springen

Lua-Scripting: Beispiele & Muster

Diese Seite enthält vollständige Lua-Fähigkeiten für EliteMobs mit Erklärungen. Speichere jedes Beispiel in einer eigenen .lua-Datei, hänge es an einen Testboss, führe /em reload aus und spawne nach Änderungen einen neuen Boss. Animationsnamen wie slam, roar, frenzy und transform müssen auf dem eigenen Modell des Bosses vorhanden sein; sie sind nicht eingebaut. Ohne eigenes Modell überspringt der Boss diese Animationsaufrufe. Die Zeitangaben setzen 20 TPS voraus.

Wenn du neu bei Lua-Fähigkeiten bist, beginne mit Erste Schritte. Für NPC-Skripte siehe NPC-Skripte. Die vollständige API für Boss-Fähigkeiten steht unter API-Referenz, Boss & Entitäten, Welt & Umgebung, Zonen & Zielerfassung und Enums.

webapp_banner.jpg


Beispiel: Zonenbasierte Zielerfassung mit Script-Utilities​

Was dieses Beispiel lehrt: Wie man context.script verwendet, um EliteScript-artige Zonengeometrie aus Lua zu erstellen, Partikel zu spawnen und Entitäten in der Zone Schaden zuzufügen.

Vollständige Power-Datei (klicken zum Aufklappen)
return {
api_version = 1,

on_boss_damaged_by_player = function(context)
local cone = context.script:zone({
shape = "CONE",
Target = {
targetType = "SELF",
offset = "0,1,0"
},
Target2 = {
targetType = "NEARBY_PLAYERS",
range = 20
},
radius = 5
})

context.script:spawn_particles(
cone:full_target(0.4),
{
particle = "FLAME",
amount = 1,
speed = 0.05
}
)

context.script:damage(cone:full_target(), 1.0, 1.5)
end
}

Erklärung​

  1. Zonenerstellung -- context.script:zone(...) verwendet die Felder der EliteScript-Zonen. Target setzt die Kegelspitze einen Block über den Boss. Die erste aufgelöste Target2-Position bestimmt die Basismitte und damit die Kegellänge; radius = 5 setzt den Basisradius. NEARBY_PLAYERS sucht in einer Box mit 20 Blöcken Ausdehnung je Achsenrichtung und garantiert nicht den nächsten Spieler. Verwende in diesem Schadenshook DIRECT_TARGET, um auf den Angreifer zu zielen. Ohne aufgelösten Spieler entsteht kein Kegel; die Endpunkte müssen verschieden sein und in derselben Welt liegen.

  2. Partikel-Spawning -- cone:full_target(0.4) liefert ein Ziel-Handle, das bei jedem Aufruf Zonenpunkte mit 40 % Wahrscheinlichkeit auswählt. Das garantiert keine genaue Anzahl und filtert keine Schadensempfänger. Die Spezifikation verwendet die EliteScript-Felder particle, amount und speed. Der Partikelhelfer verschiebt Zonenpositionen um einen halben Block auf X und Z; Partikel bilden keine exakten Treffergrenzen für Entitäten ab.

  3. Schaden -- context.script:damage(cone:full_target(), 1.0, 1.5) trifft aufgelöste Spieler innerhalb der Kegel, weil der Standardfilter der Zone PLAYER ist. Die erste Zahl (1.0) ist der übermittelte Basisschaden, die zweite (1.5) der EliteMobs-Schadensmultiplikator für Spieler. Die Kampfverarbeitung bestimmt den endgültigen Schaden.

Dieses Beispiel läuft bei jedem zugestellten Treffer: Es besitzt weder lokalen noch globalen Cooldown und keine Warnwartezeit zwischen Partikeln und Schaden. Ergänze ausdrückliche Cooldown-Prüfungen, wenn du es mit anderen Angriffen kombinierst. Jede Abfrage einer statischen Zone löst ihre Ziele erneut auf; Lua-Zonen-Handles wenden Bedingungen oder Ziel-Caches von YAML-Aktionen nicht automatisch an.

EliteScript-Feldnamen

Die an context.script übergebenen Zonen- und Zieltabellen verwenden EliteScript-Feldnamen (targetType, shape, Target, Target2, range, offset, coverage). Die vollständige Liste findest du unter EliteScript-Zonen und EliteScript-Ziele.


Beispiel: Zustand + Scheduler-Angriffsschleife​

Was dieses Beispiel lehrt: Verwendung von context.state zur Verfolgung des Laufzeitzustands, context.scheduler für wiederholende Aufgaben und ordnungsgemäßen Kampfstart/Ende-Lebenszyklus.

Vollständige Power-Datei (klicken zum Aufklappen)
local function pick_action(context)
local roll = math.random(1, 2)

if roll == 1 then
context.boss:play_model_animation("slam")
else
context.boss:play_model_animation("roar")
end
end

return {
api_version = 1,

on_spawn = function(context)
context.state.started = false
context.state.loop_task_id = nil
end,

on_enter_combat = function(context)
if context.state.started then
return
end

context.state.started = true

context.state.loop_task_id = context.scheduler:run_every(100, function(loop_context)
if loop_context.boss.exists then
pick_action(loop_context)
end
end)
end,

on_exit_combat = function(context)
if context.state.loop_task_id ~= nil then
context.scheduler:cancel_task(context.state.loop_task_id)
context.state.loop_task_id = nil
end
context.state.started = false
end
}

Erklärung​

  1. Zustandsinitialisierung -- on_spawn setzt context.state.started auf false und context.state.loop_task_id auf nil. Die state-Tabelle bleibt zwischen Hooks dieser Fähigkeitslaufzeit auf diesem Boss erhalten. Andere Fähigkeiten haben eigene Zustandstabellen.

  2. Kampfschutz -- on_enter_combat prüft context.state.started, bevor die Schleife gestartet wird. Dies verhindert mehrere überlappende Schleifen, wenn das Ereignis mehr als einmal ausgelöst wird.

  3. Scheduler-Muster -- context.scheduler:run_every(100, callback) plant den ersten Aufruf ohne Verzögerung und wiederholt dann alle 100 Ticks (5 Sekunden). Es wartet vor dem Start keine fünf Sekunden. Verwende dafür run_repeating(100, 100, callback). Der Callback erhält einen neuen Kontext ohne auslösendes Ereignis oder Spieler; loop_context.boss ist ein neuer Snapshot. Speichere die zurückgegebene numerische Aufgaben-ID im Zustand. Dieses Beispiel wählt nur eine Animation und verursacht selbst keinen Schaden.

  4. Aufräumen beim Verlassen -- on_exit_combat bricht die wiederholende Aufgabe mit der gespeicherten Task-ID ab und setzt den State zurück. Dies ist kritisch: Ohne Aufräumen läuft der Scheduler auch nach Kampfende weiter.

Scheduler-Aufgaben immer abbrechen

Beende kampfgebundene Wiederholungen in on_exit_combat. Ohne diese Bereinigung bleiben sie nach Kampfende geplant, bis sie abgebrochen werden oder die Fähigkeitslaufzeit schließt. Pausierte Laufzeiten überspringen Callbacks, ohne die Aufgabe zu verlieren. Ein nicht abgefangener Callback-Fehler schließt die Laufzeit und beendet ihre Aufgaben.


Beispiel: Feuereffekt bei Treffer​

Was dieses Beispiel lehrt: Ein einfacher „bei Treffer Effekt anwenden"-Power -- das häufigste Muster für Kampffähigkeiten.

Vollständige Power-Datei (klicken zum Aufklappen)
return {
api_version = 1,

on_player_damaged_by_boss = function(context)
-- Guard: the player may be nil in edge cases
if context.player == nil then
return
end

if not context.cooldowns:global_ready() then
return
end

-- Check and set a 60-tick (3 second) local cooldown in one call
if not context.cooldowns:check_local("fire_touch", 60) then
return
end

-- Coordinate with other powers that check the shared cooldown
context.cooldowns:set_global(40)

-- Set the player on fire for 60 ticks (3 seconds)
context.player:set_fire_ticks(60)

-- Visual feedback: spawn flame particles at the player's location
context.world:spawn_particle_at_location(
context.player.current_location,
{ particle = "FLAME", amount = 20, speed = 0.1 }
)

-- Tell the player what happened
context.player:send_message("&cThe boss's touch burns!")
end
}

Erklärung​

  1. Nil-Schutz -- context.player wird aus dem Spieler als Ereignisakteur oder direktem Ziel aufgelöst. Prüfe ihn vor der Verwendung, besonders beim Übertragen des Codes auf andere Hooks. Geplante Callbacks erben diesen Spieler nicht.

  2. Lokaler Cooldown -- Nach global_ready() prüft context.cooldowns:check_local("fire_touch", 60) seinen Schlüssel und setzt bei Bereitschaft sofort 60 Ticks. Andernfalls gibt es false zurück und der Hook endet. Fähigkeiten dieses Bosses teilen den Schlüssel; andere Bosse sind unabhängig. Stelle Schlüsseln beim Kombinieren von Beispielen den Fähigkeitsnamen voran.

  3. Feuer-Ticks -- context.player:set_fire_ticks(60) setzt die Bukkit-Feuerdauer auf 60 Ticks (3 Sekunden), nachdem geprüft wurde, dass der Spieler gültig, lebendig, online, in der Quellwelt und innerhalb einer Dungeon-Instanz ein aktueller Teilnehmer ist. Es ersetzt die Dauer, statt 60 Ticks hinzuzufügen. Andere Spielmechaniken können das Feuer am Spieler löschen; Nachricht und Partikel beweisen nicht, dass der Effekt angenommen wurde.

  4. Partikel -- context.world:spawn_particle_at_location(location, spec) spawnt Partikel an einer bestimmten Position. Die Spec-Tabelle akzeptiert particle (Bukkit-Partikel-Enum-Name), amount und speed.

  5. Nachricht -- context.player:send_message(text) sendet eine farbcodierte Chat-Nachricht. Standard-Minecraft-Farbcodes wie &c (rot) funktionieren automatisch.

  6. Globaler Cooldown -- context.cooldowns:set_global(40) setzt den gemeinsamen Cooldown des Bosses vor den Effekten auf 40 Ticks (2 Sekunden). Dieses Beispiel prüft ihn, bevor es den lokalen Cooldown verbraucht. Andere Lua-Hooks und Callbacks müssen ebenfalls global_ready() aufrufen, wenn sie warten sollen; der globale Cooldown hält nicht alle Lua-Callbacks an.


Beispiel: Zonenbasierte AoE-Fähigkeit mit nativen Lua-Zonen​

Was dieses Beispiel lehrt: Erstellen und Abfragen nativer Lua-Zonen, um Spielern in einem Bereich Schaden zuzufügen.

Vollständige Power-Datei (klicken zum Aufklappen)
return {
api_version = 1,

on_spawn = function(context)
context.state.aoe_task_id = nil
end,

on_enter_combat = function(context)
-- Prevent duplicate loops
if context.state.aoe_task_id ~= nil then
return
end

context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
-- Make sure the boss is still alive
if not tick_context.boss.exists then
return
end

if not tick_context.cooldowns:global_ready() then
return
end

-- Check and set this boss's named cooldown before applying effects
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then
return
end
tick_context.cooldowns:set_global(60)

-- Build a sphere zone centered on the boss's current position
local zone_def = {
kind = "sphere",
radius = 8,
origin = tick_context.boss:get_location()
}

-- Find all players inside the sphere
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })

-- Damage and show particles on each player found
for i = 1, #victims do
local victim = victims[i]
victim:deal_custom_damage(4.0)

tick_context.world:spawn_particle_at_location(
victim.current_location,
{ particle = "DUST", amount = 15, speed = 0, red = 128, green = 0, blue = 255 }
)
end

-- Spawn a particle burst at the boss; this does not draw the sphere
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "WITCH", amount = 40, speed = 0.1 }
)
end)
end,

on_exit_combat = function(context)
if context.state.aoe_task_id ~= nil then
context.scheduler:cancel_task(context.state.aoe_task_id)
context.state.aoe_task_id = nil
end
end
}

Erklärung​

  1. Zustandseinrichtung -- on_spawn initialisiert aoe_task_id mit nil. Diese Task-ID wird die Referenz auf die wiederholende Scheduler-Aufgabe halten.

  2. Wiederholender Angriff -- on_enter_combat startet ohne Anfangsverzögerung und wiederholt alle 60 Ticks (3 Sekunden). Die Prüfung verhindert doppelte Schleifen. Jeder Callback prüft den globalen Cooldown, prüft und setzt dann pulse_aoe und setzt den globalen Cooldown vor den Effekten auf 60 Ticks. Das koordiniert nur Fähigkeiten, die diese Schlüssel prüfen; es blockiert nicht automatisch alle Skripte.

  3. Zonendefinition -- Die zone_def-Tabelle verwendet die native Lua-Zonensyntax. Das kind-Feld gibt die Form an ("sphere"), radius setzt die Größe, und origin wird auf die aktuelle Position des Bosses zum Zeitpunkt des Callback-Aufrufs gesetzt. Das bedeutet, die Zone folgt dem Boss, wenn er sich bewegt.

  4. Entitätsabfrage -- tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" }) liefert ein bei eins indiziertes Lua-Array anhand des Entitäts-Überschneidungstests der Form, nicht einer exakten Distanz zu den Füßen. Abfragen suchen in der aktuellen Boss-Welt und filtern Spieler weder nach Spielmodus noch Teilnahme am Kampf. Filter akzeptieren "players", "elites", "mobs" oder "living" (Standard); der Boss selbst ist ausgeschlossen.

  5. Schaden verursachen -- victim:deal_custom_damage(4.0) übermittelt über BossCustomAttackDamage 4 Basisschaden mit dem Boss als Angreifer. Unverwundbare oder tote Ziele sowie Spieler außerhalb von Überleben und Abenteuer werden übersprungen. Auf dem Elite-zu-Spieler-Pfad umgeht es die normale Formel und Blockreduktion, während Vanilla-Rüstungsreduktionen entfernt werden. Listener, Abbruch und Absorption können den Gesundheitsverlust weiter verändern. Nach der Übermittlung setzt der Helfer den nativen Schadenscooldown des Ziels zurück; der Ablauf unterscheidet sich von context.script:damage.

  6. Partikel-Feedback -- Lila Staub erscheint an der gespeicherten Position jedes gefundenen Spielers, auch wenn dessen Schaden abgelehnt wurde. Ein WITCH-Partikelausbruch erscheint am Boss; er zeichnet weder einen Ring noch die Kugelgrenze. Diese kompakten Tabellen verwenden standardmäßig keine Positionsstreuung; die Staubgröße ist fest 1.

  7. Aufräumen -- on_exit_combat bricht die wiederholende Aufgabe ab und bereinigt den State, dem gleichen Muster wie im vorherigen Beispiel folgend.

Native Zonen vs. Script-Utility-Zonen

Dieses Beispiel verwendet native Lua-Zonen (context.zones:get_entities_in_zone()), die eine einfache Tabelle mit kind, radius, origin usw. nehmen. Die Script-Utilities (context.script:zone(...)) verwenden EliteScript-Feldnamen wie shape, Target und Target2. Beide funktionieren -- verwende native Zonen für einfache Formen und context.script, wenn du EliteScripts erweiterte Zielauflösung benötigst.


Beispiel: Mehrstufige Boss-Mechanik​

Was dieses Beispiel lehrt: Verwendung von State zur Verfolgung von Boss-Phasen und Verhaltensänderung bei Lebensschwellenwerten.

Dies sind Phasen im Lua-Zustand, keine YAML-phases: von EliteMobs, die eine Boss-Definition ersetzen. Hier wird kein Boss ersetzt oder geheilt. Spätere Heilung führt nicht zu Phase 1 zurück; nach Kampfende bleibt Phase 2 erhalten, bis die Fähigkeitslaufzeit neu erstellt wird.

Vollständige Power-Datei (klicken zum Aufklappen)
local function phase_one_attack(context)
-- One damage pulse per callback; the animation does not delay it
context.boss:play_model_animation("slam")

local zone_def = {
kind = "sphere",
radius = 5,
origin = context.boss:get_location()
}

local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do
targets[i]:deal_custom_damage(3.0)
end

context.world:spawn_particle_at_location(
context.boss:get_location(),
{ particle = "EXPLOSION", amount = 3, speed = 0 }
)
end

local function phase_two_attack(context)
-- One pulse per callback, repeated more frequently in phase 2
context.boss:play_model_animation("frenzy")

local zone_def = {
kind = "sphere",
radius = 8,
origin = context.boss:get_location()
}

local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do
targets[i]:deal_custom_damage(2.0)
targets[i]:apply_potion_effect("SLOWNESS", 40, 1)
end

context.world:spawn_particle_at_location(
context.boss:get_location(),
{ particle = "DUST", amount = 30, speed = 0.2, red = 255, green = 0, blue = 0 }
)

context.world:play_sound_at_location(
context.boss:get_location(),
"entity.wither.ambient",
1.0, 1.5
)
end

local function start_attack_loop(context)
if context.state.attack_task_id ~= nil then return end
local phase = context.state.phase or 1
local interval = phase == 2 and 40 or 100
local attack = phase == 2 and phase_two_attack or phase_one_attack
context.state.attack_task_id = context.scheduler:run_every(interval, function(tick_context)
if not tick_context.boss.exists or not tick_context.boss.is_in_combat then return end
if tick_context.cooldowns:check_local("phase_attack", interval) then
attack(tick_context)
end
end)
end

return {
api_version = 1,

on_spawn = function(context)
context.state.phase = 1
context.state.attack_task_id = nil
end,

on_enter_combat = function(context)
start_attack_loop(context)
end,

on_game_tick = function(context)
-- Only check phase transition every 20 ticks (1 second) to stay lightweight
if not context.boss.is_in_combat then return end
if not context.cooldowns:check_local("phase_check", 20) then
return
end

-- Skip if already in phase 2
if context.state.phase ~= 1 then
return
end

-- Check health ratio
local health_ratio = context.boss.health / context.boss.maximum_health

if health_ratio <= 0.5 then
-- Transition to phase 2
context.state.phase = 2

context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")

-- Cancel the old attack loop
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end

-- Play transition effects
context.boss:play_model_animation("transform")

-- Announce the phase change to nearby players
local nearby = context.players.nearby_players(40)
for i = 1, #nearby do
nearby[i]:send_message("&4&lThe boss enters a frenzy!")
nearby[i]:show_title("&4Phase 2", "&cThe boss is enraged!", 10, 40, 10)
end

start_attack_loop(context)
end
end,

on_exit_combat = function(context)
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
end
}

Erklärung​

  1. Zustandsinitialisierung -- on_spawn setzt phase auf 1 und attack_task_id auf nil. Diese Werte bleiben zwischen Hooks dieser Fähigkeitslaufzeit erhalten, nicht nach einem Neuladen oder in anderen Fähigkeiten.

  2. Angriffsschleife -- on_enter_combat setzt mit start_attack_loop die aktuelle Phase fort: Phase 1 verwendet 100 Ticks Intervall, Phase 2 verwendet 40. Beide planen den ersten Callback ohne Verzögerung. Jeder Angriff benötigt weiterhin einen bereiten phase_attack-Cooldown; ein Phasenwechsel löscht den bisherigen Cooldown nicht, sodass ein Callback den Angriff überspringen kann. Dieses Beispiel prüft den globalen Cooldown nicht. Phase 1 übermittelt 3 benutzerdefinierte Schadenspunkte an Spieler in einer Kugel mit Radius 5 und spielt Animation sowie Partikeleffekte, ohne die Animationslänge abzuwarten.

  3. Phasenprüfung in on_game_tick -- on_game_tick kehrt außerhalb des Kampfes sofort zurück und prüft danach einen Cooldown von 20 Ticks ("phase_check"). Ist der Boss bereits in Phase 2, kehrt es ebenfalls frühzeitig zurück.

  4. Lebensschwellenwert -- context.boss.health / context.boss.maximum_health gibt die aktuelle Gesundheit als Bruch an. Wenn sie auf 50% oder darunter fällt, beginnt der Übergang.

  5. Phasenübergang -- Die alte Angriffsschleife wird abgebrochen, eine Transformationsanimation wird abgespielt, nahegelegene Spieler erhalten eine Nachricht und einen Titel, und eine neue schnellere Angriffsschleife startet (alle 40 Ticks statt 100).

  6. Phase-2-Angriffe -- phase_two_attack verwendet eine Kugel mit Radius 8 und übermittelt pro gefundenem Spieler und erfolgreichem Callback einen Treffer mit 2 Schaden. Es versucht, Langsamkeit II für 40 Ticks (amplifier = 1) anzuwenden, unter den oben beschriebenen Teilnahmeprüfungen für Statuseffekte. Danach folgen roter Staub und ein Wither-Sound. Es werden keine mehreren Treffer innerhalb eines Callbacks geplant.

  7. Protokollierung -- context.log:info(...) schreibt in die Serverkonsole, was für das Debugging von Phasenübergängen während der Entwicklung unschätzbar wertvoll ist.

  8. Aufräumen -- on_exit_combat bricht die aktuell aktive Angriffsschleife ab, unabhängig davon, in welcher Phase sich der Boss befindet.

on_game_tick leichtgewichtig halten

Eine aktive, nicht pausierte Laufzeit erhält on_game_tick jeden Server-Tick (20-mal pro Sekunde bei 20 TPS). Begrenze teure Arbeit durch einen Cooldown wie check_local("phase_check", 20). Die Gesundheit wird periodisch abgefragt; ein tödlicher Treffer kann den Boss vor der nächsten Phasenprüfung entfernen. Überschreiten des Ausführungsbudgets schließt diese Fähigkeitslaufzeit und beendet ihre Aufgaben.


KI-Generierungstipps​

Wenn du möchtest, dass KI Lua-Fähigkeiten zuverlässig generiert, stelle sicher, dass der Prompt Folgendes enthält:

  • Exakter Hook-Name -- z.B. on_player_damaged_by_boss, nicht „wenn der Boss einen Spieler trifft".
  • Native Lua-Zonen oder Script-Utilities -- gib an, welche. Native Zonen verwenden context.zones:get_entities_in_zone(zone_def, opts) mit kind, radius, origin. Script-Utilities verwenden context.script:zone(...) mit EliteScript-Feldnamen wie shape, Target, Target2.
  • Lokale und globale Cooldowns -- gib einen Schlüssel mit Präfix und die Dauer in Ticks an. context.cooldowns:check_local(key, ticks) prüft und setzt einen benannten Cooldown pro Boss. Zur globalen Koordination prüfe ausdrücklich global_ready(), bevor der lokale Schlüssel verbraucht wird, und setze die globale Dauer vor den Effekten.
  • Benutzerdefinierte Modellanimationsnamen -- z.B. context.boss:play_model_animation("slam"). Die KI kann keine Animationsnamen erraten; stelle sie bereit.
  • Zielauswahl -- gib an, ob die Fähigkeit context.player (den Ereignisspieler), context.players.nearby_players(range) oder eine Zonenabfrage anvisiert.
  • Effekttyp -- Schaden (deal_custom_damage), Trank (apply_potion_effect), Feuer (set_fire_ticks), Geschwindigkeit (set_velocity_vector, apply_push_vector), etc.
  • Methoden des richtigen Wrappers verwenden -- gib die passende Boss- oder gemeinsame API-Referenz an und erfinde keine Aliase.
  • Script-Utility-Specs verwenden EliteScript-Feldnamen -- targetType, shape, Target, Target2, range, offset, coverage, nicht Lua-artige Namen.
  • Scheduler-Zeitverhalten und Kontext -- gib an, ob der erste Callback sofort oder verzögert starten soll. run_every hat keine Startverzögerung; run_repeating nimmt sie separat entgegen. Verwende den neuen Callback-Kontext ohne auslösenden Spieler oder Ereignis. Speichere benötigte Spieler-UUIDs oder Positionen ausdrücklich für eine spätere Verwendung.

Gutes Prompt-Beispiel​

Schreibe eine Lua-Fähigkeit, die in on_enter_combat eine Aufgabe mit 80 Ticks Startverzögerung und 80 Ticks Intervall startet. Erstelle pro Callback eine native Lua-Kugel mit Radius 6 um den Boss, frage Spieler ab und übermittle jeweils 2.0 benutzerdefinierten Schaden. Prüfe den globalen Cooldown, bevor der lokale Schlüssel "my_power:pulse" für 80 Ticks geprüft und gesetzt wird; setze dann den globalen Cooldown vor den Effekten auf 80. Beende die Aufgabe in on_exit_combat. Erzeuge DUST-Partikel (red=0, green=255, blue=100) an jedem gefundenen Spieler.

Zusätzliche Einschränkungen zum Einbeziehen​

  • „Gib eine einzelne Tabelle mit api_version = 1 zurück."
  • „Initialisiere alle State-Felder in on_spawn."
  • „Schütze Scheduler-Callbacks mit if not tick_context.boss.exists then return end."
  • „Breche alle wiederholenden Aufgaben in on_exit_combat ab."
  • „Erfinde keine Methodennamen -- verwende nur Methoden von der API-Referenzseite."
  • „Verwende context.cooldowns:check_local(key, ticks) für kombiniertes Prüfen-und-Setzen."

QC-Checkliste für manuelle oder KI-Überprüfung​

Verwende diese Checkliste, um eine Lua-Fähigkeit vor dem Einsatz zu überprüfen:

  1. Die Datei gibt genau eine Tabelle mit api_version = 1 zurück.
  2. Jeder Hook-Name stimmt exakt mit einem Eintrag in der Hook-Liste überein (z.B. on_player_damaged_by_boss, nicht on_player_hit).
  3. context.player wird mit == nil geschützt, bevor es in Hooks verwendet wird, wo es nil sein kann.
  4. context.state-Felder werden in on_spawn initialisiert.
  5. Jede kampfgebundene Wiederholung aus run_every oder run_repeating wird in on_exit_combat beendet; das Verhalten der Startverzögerung ist bewusst gewählt.
  6. Scheduler-Callbacks verwenden ihren eigenen Kontext und setzen weder einen auslösenden Spieler noch ein Ereignis voraus.
  7. Cooldown-Schlüssel tragen den Fähigkeitsnamen als Präfix; Dauern sind in Ticks. Globale Koordination enthält eine ausdrückliche Bereitschaftsprüfung.
  8. on_game_tick-Hooks schützen aufwendige Arbeit hinter einer Cooldown-Prüfung.
  9. Alle Methodennamen existieren in der API-Referenz -- keine erfundenen Aliase.
  10. Script-Utility-Tabellen verwenden EliteScript-Feldnamen (targetType, shape, Target, etc.), nicht Lua-artige Namen.
  11. Native Zonendefinitionen verwenden kind, radius, origin, destination, etc.
  12. Die Fähigkeit ruft keine blockierenden oder lang laufenden Operationen innerhalb eines Hooks oder Callbacks auf.
  13. Partikelspezifikationen verwenden gültige Bukkit-Partikel-Enum-Namen in UPPER_CASE (z.B. "FLAME", "DUST", "EXPLOSION").
  14. Lade geänderte Dateien neu, spawne einen neuen Boss und prüfe sichtbares Verhalten und Konsole auf einem Staging-Server. Eine erfolgreich gelesene Datei beweist noch keinen funktionierenden Angriff.

Best Practices​

  • Beginne mit einem kleinen Hook und verifiziere. Schreibe einen einzelnen on_spawn, der eine Log-Nachricht sendet. Bestätige, dass er auslöst. Dann baue von dort aus weiter.

  • Halte Hilfsfunktionen lokal. Deklariere Helfer wie local function pick_action(context) vor der Rückgabetabelle, um Abhängigkeiten ausdrücklich zu halten. Jede Fähigkeitsinstanz hat bereits ihre eigene Lua-Laufzeit.

  • Lege Geometrie in Script-Utilities. Verwende für Kegel, rotierende oder verschobene Strahlen und animierte Zonen context.script:zone(...) mit EliteScript-Feldnamen. Die Script-Utilities verwenden die EliteScript-Zonenengine.

  • Verwende context.state für Laufzeitzustand. Er gehört zu einer Fähigkeitslaufzeit auf einem Boss und bleibt zwischen deren Hooks erhalten. Er wird weder mit anderen Fähigkeiten geteilt noch über Neuladen hinweg gespeichert.

  • Verwende benannte lokale Cooldown-Schlüssel. Wähle beschreibende Schlüssel wie "fire_touch" oder "aoe_pulse". Explizite Schlüssel werden zwischen allen Fähigkeiten desselben Bosses geteilt. Stelle ihnen den Fähigkeitsnamen voran, etwa "my_power:fire_touch", um Kollisionen zu vermeiden.

  • Halte on_game_tick leichtgewichtig. Begrenze aufwendige Arbeit durch einen Cooldown und bleibe deutlich unter dem Ausführungsbudget.

  • Beende Wiederholungen, wenn sie nicht mehr benötigt werden. Kampfgebundene Schleifen sollten in on_exit_combat beendet werden. Eigene Scheduler-Aufgaben werden beim Schließen der Laufzeit automatisch beendet, laufen aber nach Kampfende ohne ausdrücklichen Abbruch weiter.

  • Verwende neue Scheduler-Callback-Kontexte. Callbacks von run_every, run_repeating und run_after erhalten einen neuen Kontext ohne das planende Ereignis oder dessen Spieler. Verwende dieses Argument und speichere benötigte UUIDs oder Positionen ausdrücklich; ein äußerer Kontext kann veraltete Snapshots enthalten.

  • Protokolliere Zustandsübergänge mit context.log:info(). Erfasse während der Entwicklung Phasenwechsel, Cooldown-Starts und Scheduler-Starts oder -Stopps. Entferne übermäßige Ausgaben vor dem Einsatz: Boss-context.log:debug() schreibt ebenfalls auf Info-Ebene, mit anderem Präfix.

  • Verwende die passenden EliteScript-Referenzen. Zonen, Ziele und Relative Vektoren beschreiben die gemeinsamen Parser. Eigenständige Lua-Helfer werten YAML-Bedingungen, Wartezeiten und Aktions-Caches nicht automatisch aus; formuliere Prüfungen in Lua. Vererbte Ziele und Landepositionen benötigen einen YAML-Aktionskontext, der diesen Handles fehlt.


Häufige Anfängerfehler​

  • Verwendung des äußeren context innerhalb eines Scheduler-Callbacks. Der äußere Kontext erfasst einen Snapshot zum Zeitpunkt der Hook-Ausführung. Verwende innerhalb eines run_every- oder run_after-Callbacks immer den eigenen Parameter des Callbacks (z.B. tick_context), der dir einen frischen Snapshot gibt.

  • Vergessen, wiederholende Aufgaben abzubrechen. Wenn du ein run_every in on_enter_combat startest, aber nie abbrichst, läuft die Aufgabe bis der Boss vom Server entfernt wird, auch nach Kampfende.

  • State nicht in on_spawn initialisieren. Wenn du context.state.phase in on_game_tick liest, aber nie in on_spawn setzt, wird es nil sein und deine Vergleiche werden sich unerwartet verhalten.

  • context.player ohne Nil-Prüfung verwenden. Prüfe, welche Hooks einen Spieler bereitstellen. Geplante Callbacks besitzen standardmäßig keinen; eine fehlende Prüfung kann die Fähigkeitslaufzeit schließen.

  • Lua-artige Feldnamen in Script-Utilities verwenden. Verwende die dokumentierten Felder targetType, Target, Target2 und shape. Unterstrichvarianten wie target_type und zone_shape werden nicht unterstützt. Der Zonenparser akzeptiert kleingeschriebenes target und target2, aber deshalb keine beliebigen Lua-artigen Aliase.

  • Teure Arbeit in on_game_tick ohne Cooldown ausführen. Weltweite Entitätsabfragen und dichte Partikelgeometrie für viele Bosse sind teuer. Begrenze diese Arbeit und halte jeden Callback innerhalb seines Ausführungsbudgets.

  • Wrapper-APIs mischen. Boss-spezifische EliteMobs-Wrapper verwenden entity:teleport_to_location(loc) und player:set_velocity_vector(vec). Gemeinsame MagmaCore-Wrapper bieten zusätzliche Aliase. Prüfe die Referenz deines Kontexts, bevor du Methoden aus Beispielen anderer Plugins übernimmst.

  • context.boss.health zum Setzen der Gesundheit verwenden. context.boss.health ist ein schreibgeschützter Snapshot. Um den Boss zu heilen, verwende context.boss:restore_health(amount).

  • api_version = 1 vergessen. Die zurückgegebene Tabelle muss dieses Feld enthalten, oder EliteMobs wird die Fähigkeit nicht laden.


Nächste Schritte​