Zum Hauptinhalt springen

Lua-Skripting: Beispiele & Muster

Diese Seite enthält vollständige funktionierende Beispiele von FreeMinecraftModels-Prop-Skripten sowie praktische Muster und Best Practices. Jedes Beispiel enthält eine Erklärung, die beschreibt, was es tut und warum.

Wenn du neu im Prop-Skripting bist, beginne mit Erste Schritte. Für vollständige API-Details siehe die Prop-API.


Beispiel: Unverwundbares Prop

Was dieses Beispiel lehrt: Das einfachste nützliche Skript -- Schaden abbrechen, damit der Prop nicht zerstört werden kann.

Dies ist das mitgelieferte Skript, das mit FreeMinecraftModels ausgeliefert wird.

Vollständige Skriptdatei (zum Aufklappen klicken)
return {
api_version = 1,

on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}

Erklärung

  1. Hook-Wahl -- on_left_click wird ausgelöst, wenn ein Spieler den Prop schlägt (links klickt). Unter der Haube ist dies ein EntityDamageByEntityEvent auf dem darunterliegenden Armor Stand des Props.

  2. Event-Schutz -- context.event sollte in diesem Hook immer vorhanden sein, aber der Schutz ist eine gute Praxis.

  3. Abbruch -- context.event.cancel() bricht das Schadens-Event ab, was verhindert, dass der Armor Stand Schaden nimmt und zerstört wird.

Verwendung

Füge zu deiner Prop-.yml-Konfiguration hinzu:

isEnabled: true
scripts:
- invulnerable.lua

Beispiel: Interaktive Tür

Was dieses Beispiel lehrt: Zustand bei Rechtsklick umschalten, Animationen abspielen und stoppen, und context.state verwenden, um zu verfolgen, ob die Tür offen oder geschlossen ist.

Vollständige Skriptdatei (zum Aufklappen klicken)
local OPEN_ANIMATION = "open"
local CLOSE_ANIMATION = "close"

return {
api_version = 1,

on_spawn = function(context)
context.state.is_open = false
end,

on_left_click = function(context)
-- Tuer unverwundbar machen
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
local loc = context.prop.current_location

if context.state.is_open then
-- Tuer schliessen
context.prop:stop_animation()
context.prop:play_animation(CLOSE_ANIMATION, true, false)
context.state.is_open = false

if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_CLOSE",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
else
-- Tuer oeffnen
context.prop:stop_animation()
context.prop:play_animation(OPEN_ANIMATION, true, false)
context.state.is_open = true

if loc then
context.world:play_sound(
"BLOCK_WOODEN_DOOR_OPEN",
loc.x, loc.y, loc.z,
1.0, 1.0
)
end
end
end
}

Erklärung

  1. Konstanten auf Dateiebene -- OPEN_ANIMATION und CLOSE_ANIMATION werden oberhalb der Return-Tabelle definiert. Das macht sie leicht änderbar für verschiedene Modelldateien, die möglicherweise andere Animationsnamen verwenden.

  2. State-Initialisierung -- on_spawn setzt context.state.is_open = false. Der State bleibt über alle Hooks für diese Prop-Instanz bestehen.

  3. Unverwundbarkeit -- Der on_left_click-Hook bricht den Schaden ab, damit die Tür nicht versehentlich zerstört werden kann.

  4. Umschaltlogik -- on_right_click prüft context.state.is_open, stoppt jede aktuelle Animation, spielt die entsprechende Animation ab, wechselt den State und spielt einen Sound. Der stop_animation()-Aufruf vor play_animation() sorgt für saubere Übergänge.

  5. Sound-Feedback -- context.world:play_sound() spielt den Bukkit Sound Enum-Namen an der Position des Props ab. Sound-Namen müssen GROSSBUCHSTABEN Enum-Namen sein.

Animationsnamen

Animationsnamen wie "open" und "close" müssen mit dem übereinstimmen, was in der Modelldatei definiert ist. Wenn die Animation nicht gefunden wird, gibt play_animation() false zurück und nichts passiert. Überprüfe deine Modelldatei auf die exakten Animationsnamen.


Beispiel: Näherungsauslöser mit Partikeln

Was dieses Beispiel lehrt: Zonenerstellung, Zonenüberwachung für Betreten/Verlassen-Events, Partikeleffekte und Aufräumen.

Vollständige Skriptdatei (zum Aufklappen klicken)
local ZONE_RADIUS = 8
local PARTICLE_INTERVAL = 10 -- Ticks zwischen Partikel-Bursts

return {
api_version = 1,

on_spawn = function(context)
context.state.zone_handle = nil
context.state.particle_task = nil
context.state.players_in_zone = 0

local loc = context.prop.current_location
if loc == nil then return end

-- Kugelzone um den Prop erstellen
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, ZONE_RADIUS)
context.state.zone_handle = handle

-- Auf Betreten/Verlassen ueberwachen
context.zones:watch(
handle,
function(player)
-- on_enter
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,
function(player)
-- on_leave
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
end
)

-- Wiederholenden Partikeleffekt am Zonenrand starten
context.state.particle_task = context.scheduler:run_repeating(0, PARTICLE_INTERVAL, function(tick_context)
local prop_loc = tick_context.prop.current_location
if prop_loc == nil then return end

-- Partikel in einem Ring am Zonenrand spawnen
for angle = 0, 350, 30 do
local rad = math.rad(angle)
local px = prop_loc.x + math.cos(rad) * ZONE_RADIUS
local pz = prop_loc.z + math.sin(rad) * ZONE_RADIUS

if (tick_context.state.players_in_zone or 0) > 0 then
-- Rote Partikel wenn Spieler drinnen sind
tick_context.world:spawn_particle("DUST", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
else
-- Gruene Partikel wenn Zone leer ist
tick_context.world:spawn_particle("HAPPY_VILLAGER", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
end
end
end)
end,

on_left_click = function(context)
-- Unverwundbar machen
if context.event then
context.event.cancel()
end
end,

on_destroy = function(context)
-- Wiederholende Aufgabe aufraeumen
if context.state.particle_task then
context.scheduler:cancel(context.state.particle_task)
context.state.particle_task = nil
end
-- Zonenueberwachung aufraeumen
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}

Erklärung

  1. Konstanten -- ZONE_RADIUS und PARTICLE_INTERVAL sind auf Dateiebene für einfache Anpassung.

  2. State-Initialisierung -- on_spawn setzt alle State-Felder auf nil / 0, bevor irgendetwas anderes passiert.

  3. Zonenerstellung -- context.zones:create_sphere() erstellt eine Kugelzone zentriert auf dem Prop. Das zurückgegebene Handle ist eine numerische ID, die verwendet wird, um diese Zone später zu referenzieren.

  4. Zonenüberwachung -- context.zones:watch() registriert Callbacks für Spieler-Betreten und -Verlassen. Die Callbacks erhöhen und verringern einen Zähler, der in context.state gespeichert wird.

  5. Partikelschleife -- Eine wiederholende Aufgabe spawnt Partikel in einem Ring um den Prop jede halbe Sekunde. Der Partikeltyp ändert sich basierend darauf, ob Spieler in der Zone sind.

  6. Aufräumen -- on_destroy bricht die wiederholende Aufgabe ab und beendet die Zonenüberwachung. Obwohl beides automatisch aufgeräumt wird, wenn der Prop entfernt wird, ist explizites Aufräumen eine Best Practice.

Partikel-Performance

Das Spawnen vieler Partikel bei jedem Tick kann die Leistung beeinträchtigen. Verwende ein vernünftiges Intervall (10-20 Ticks) und halte die Partikelanzahl niedrig. Das obige Beispiel verwendet PARTICLE_INTERVAL = 10 (zweimal pro Sekunde) mit nur 12 Partikeln pro Ring.


Beispiel: Sound-erzeugendes Prop

Was dieses Beispiel lehrt: Sounds bei Interaktion abspielen, Cooldown-ähnliches Verhalten mit State und Scheduler, und schnelle aufeinanderfolgende Interaktionen verhindern.

Vollständige Skriptdatei (zum Aufklappen klicken)
local SOUND_NAME = "BLOCK_NOTE_BLOCK_HARP"
local COOLDOWN_TICKS = 40 -- 2 Sekunden zwischen Sounds

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
end,

on_left_click = function(context)
-- Unverwundbar machen
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
-- Spam verhindern
if context.state.on_cooldown then
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Sound abspielen
context.world:play_sound(SOUND_NAME, loc.x, loc.y, loc.z, 1.0, 1.0)

-- Partikel anzeigen
context.world:spawn_particle("NOTE", loc.x, loc.y + 1.5, loc.z, 5, 0.3, 0.3, 0.3, 0)

-- Cooldown setzen
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}

Erklärung

  1. Cooldown-Muster -- Da FMM-Prop-Skripte keine eingebaute context.cooldowns-API wie EliteMobs haben, implementiert das Beispiel einen einfachen Cooldown mit context.state.on_cooldown und scheduler:run_later(). Das Flag wird auf true gesetzt, wenn der Sound abgespielt wird, und eine verzögerte Aufgabe setzt es nach COOLDOWN_TICKS zurück.

  2. Sound-Wiedergabe -- context.world:play_sound() nimmt den Bukkit Sound Enum-Namen in GROSSBUCHSTABEN, Koordinaten, Lautstärke und Tonhöhe.

  3. Partikel-Feedback -- Noten-Partikel erscheinen über dem Prop, wenn der Sound abgespielt wird, und geben einen visuellen Hinweis.

  4. Unverwundbarkeit -- Der on_left_click-Hook bricht Schaden wie gewohnt ab.

  5. Scheduler-Callback-Context -- Der run_later-Callback erhält later_context, einen frischen Context. Wir verwenden later_context.state (nicht context.state), um das Cooldown-Flag zurückzusetzen. Da der State geteilt wird, zeigen beide auf dieselbe Tabelle -- aber den Context-Parameter des Callbacks zu verwenden ist die korrekte Gewohnheit.

Cooldown-Implementierung

FMM-Prop-Skripte haben nicht die context.cooldowns-API von EliteMobs. Verwende das hier gezeigte Muster: ein Boolean-Flag in context.state kombiniert mit scheduler:run_later(), um es zurückzusetzen. Das gibt dir volle Kontrolle über Cooldown-Dauer und -Verhalten.


Beispiel: Animiertes Umgebungs-Prop

Was dieses Beispiel lehrt: Eine wiederholende Animation beim Spawnen starten, mit einem tick-basierten Partikelemitter.

Vollständige Skriptdatei (zum Aufklappen klicken)
return {
api_version = 1,

on_spawn = function(context)
-- Idle-Animation sofort starten, wiederholend
context.prop:play_animation("idle", false, true)

-- Umgebungspartikel alle 40 Ticks (2 Sekunden) emittieren
context.state.ambient_task = context.scheduler:run_repeating(0, 40, function(tick_context)
local loc = tick_context.prop.current_location
if loc == nil then return end

tick_context.world:spawn_particle(
"ENCHANT",
loc.x, loc.y + 1, loc.z,
10, 0.5, 0.5, 0.5, 0.05
)
end)
end,

on_left_click = function(context)
if context.event then
context.event.cancel()
end
end,

on_destroy = function(context)
if context.state.ambient_task then
context.scheduler:cancel(context.state.ambient_task)
end
end
}

Erklärung

  1. Auto-Start-Animation -- on_spawn spielt sofort eine wiederholende "idle"-Animation ab. false für blend bedeutet, sie startet frisch ohne Überblendung von einer vorherigen Animation. true für loop bedeutet, sie wiederholt sich endlos.

  2. Umgebungspartikel -- Eine wiederholende Aufgabe spawnt Verzauberungstisch-Partikel über dem Prop alle 2 Sekunden und erzeugt einen magischen Umgebungseffekt.

  3. Aufräumen -- on_destroy bricht die Partikelaufgabe ab.


Beispiel: Sitzbarer Stuhl

Was dieses Beispiel lehrt: Einen Spieler per Rechtsklick auf ein Prop setzen, per Linksklick absteigen lassen, und context.event.player verwenden, um den interagierenden Spieler zu erhalten.

Vollständige Skriptdatei (zum Aufklappen klicken)
return {
api_version = 1,

on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end

-- Pruefen, ob der Spieler bereits auf diesem Prop sitzt
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
-- Spieler sitzt bereits, bei Rechtsklick nichts tun
return
end
end

-- Spieler auf den Stuhl setzen
context.prop:mount(player)

local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_PLACE", loc.x, loc.y, loc.z, 0.8, 1.2)
end
end,

on_left_click = function(context)
-- Schaden abbrechen, damit der Stuhl unverwundbar ist
if context.event then
context.event.cancel()
end

local player = context.event and context.event.player
if not player then return end

-- Pruefen, ob der Spieler sitzt und ihn absteigen lassen
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
context.prop:dismount(player)

local loc = context.prop.current_location
if loc then
context.world:play_sound("BLOCK_WOOD_BREAK", loc.x, loc.y, loc.z, 0.8, 1.0)
end
return
end
end
end
}

Erklärung

  1. Rechtsklick zum Hinsetzen -- on_right_click erhält den Spieler von context.event.player, prüft, ob er bereits Passagier ist (um doppeltes Aufsteigen zu vermeiden), und ruft context.prop:mount(player) auf, um ihn auf den Armor Stand des Props zu setzen.

  2. Linksklick zum Aufstehen -- on_left_click bricht das Schadens-Event ab (Unverwundbarkeit), prüft dann, ob der schlagende Spieler derzeit Passagier ist. Wenn ja, wirft context.prop:dismount(player) ihn ab.

  3. Passagier-Prüfung -- context.prop:get_passengers() gibt ein Array von Entity-Tabellen zurück. Wir vergleichen UUIDs, um den interagierenden Spieler in der Liste zu finden.

  4. Sound-Feedback -- Ein Holz-Platzier-Sound wird beim Hinsetzen abgespielt und ein Holz-Brech-Sound beim Aufstehen, was taktiles Feedback gibt.


Beispiel: Segens-Schrein

Was dieses Beispiel lehrt: Das gehaltene Item des Spielers prüfen, Items verbrauchen, zufällige Trankeffekte anwenden, Cooldown-Verwaltung und Partikel-/Sound-Feedback.

Vollständige Skriptdatei (zum Aufklappen klicken)
local COOLDOWN_TICKS = 600  -- 30 Sekunden zwischen Nutzungen

local BLESSINGS = {
{ effect = "speed", name = "Schnelligkeit" },
{ effect = "strength", name = "Staerke" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistenz" },
{ effect = "jump_boost", name = "Sprungkraft" },
{ effect = "haste", name = "Eile" },
}

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
end,

on_left_click = function(context)
-- Unverwundbar machen
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end

-- Cooldown pruefen
if context.state.on_cooldown then
player:send_message("&eDer Schrein lädt sich auf... Bitte warte.")
return
end

-- Pruefen, ob der Spieler einen Goldbarren haelt
local held = player:get_held_item()
if not held or held.type ~= "gold_ingot" then
player:send_message("&eDer Schrein verlangt eine Goldopfergabe...")
return
end

-- Einen Goldbarren verbrauchen
player:consume_held_item(1)

-- Segenseffekte abspielen
local loc = context.prop.current_location
if loc then
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 1.5)
end

-- Zufaelligen Segen anwenden
local chosen = BLESSINGS[math.random(#BLESSINGS)]
player:add_potion_effect(chosen.effect, 600, 1) -- 30 Sekunden, Stufe II
player:send_message("&aDer Schrein segnet dich mit " .. chosen.name .. "!")

-- Cooldown setzen
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}

Erklärung

  1. Item-Prüfung -- player:get_held_item() gibt eine Tabelle mit type, amount und display_name für das Haupthand-Item zurück (oder nil wenn leer). Wir vergleichen held.type mit "gold_ingot" (Materialname in Kleinbuchstaben).

  2. Item-Verbrauch -- player:consume_held_item(1) entfernt ein Item aus dem Haupthand-Stapel des Spielers.

  3. Zufälliger Buff -- Die BLESSINGS-Tabelle auf Dateiebene listet verfügbare positive Effekte. math.random(#BLESSINGS) wählt einen zufällig aus. player:add_potion_effect(effect, duration, amplifier) wendet ihn an -- 600 Ticks sind 30 Sekunden, Verstärker 1 ist Stufe II.

  4. Cooldown -- Dasselbe Boolean-Flag-plus-Scheduler-Muster aus dem Sound-erzeugenden Prop-Beispiel. Ein 30-Sekunden-Cooldown verhindert Schrein-Spam.

  5. Feedback -- Verzauberungs- und Glückliche-Dorfbewohner-Partikel plus ein Leuchtfeuer-Aktivierungssound erzeugen ein "göttlicher Segen"-Gefühl.


Beispiel: Verfluchter Schrein

Was dieses Beispiel lehrt: Negative Trankeffekte, Blitzschläge, Entity-Spawning und verzweigte Logik basierend auf Spieler-Opfergaben.

Vollständige Skriptdatei (zum Aufklappen klicken)
local COOLDOWN_TICKS = 600  -- 30 Sekunden zwischen Nutzungen

local BUFFS = {
{ effect = "speed", name = "Schnelligkeit" },
{ effect = "strength", name = "Staerke" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistenz" },
}

local CURSES = {
{ effect = "slowness", name = "Langsamkeit" },
{ effect = "weakness", name = "Schwaeche" },
{ effect = "poison", name = "Gift" },
{ effect = "mining_fatigue", name = "Abbaumuedigkeit" },
}

local ZOMBIE_COUNT = 4

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
end,

on_left_click = function(context)
-- Unverwundbar machen
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end

-- Cooldown pruefen
if context.state.on_cooldown then
player:send_message("&7Der dunkle Schrein pulsiert mit Restenergie...")
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Pruefen, ob der Spieler einen Goldbarren haelt
local held = player:get_held_item()
if not held or held.type ~= "gold_ingot" then
-- Keine Opfergabe -- den Spieler bestrafen!
player:send_message("&4Der Schrein verlangt Tribut! Du wagst es, mit leeren Haenden zu kommen?!")

-- Blitz auf den Spieler schlagen
local player_loc = player.current_location
if player_loc then
context.world:strike_lightning(player_loc.x, player_loc.y, player_loc.z)
end

-- Eine Horde Zombies um den Schrein spawnen
for i = 1, ZOMBIE_COUNT do
local angle = math.rad((360 / ZOMBIE_COUNT) * i)
local spawn_x = loc.x + math.cos(angle) * 3
local spawn_z = loc.z + math.sin(angle) * 3
context.world:spawn_entity("zombie", spawn_x, loc.y, spawn_z)
end

-- Zufaelligen Fluch anwenden
local chosen_curse = CURSES[math.random(#CURSES)]
player:add_potion_effect(chosen_curse.effect, 400, 1) -- 20 Sekunden, Stufe II
player:send_message("&cDer Schrein verflucht dich mit " .. chosen_curse.name .. "!")

-- Bedrohliche Effekte
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 30, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_WITHER_AMBIENT", loc.x, loc.y, loc.z, 1.0, 0.5)
else
-- Gold angeboten -- den Spieler belohnen
player:consume_held_item(1)

-- Zufaelligen Buff anwenden
local chosen_buff = BUFFS[math.random(#BUFFS)]
player:add_potion_effect(chosen_buff.effect, 600, 1) -- 30 Sekunden, Stufe II
player:send_message("&aDer dunkle Schrein akzeptiert deine Opfergabe. Du wirst mit " .. chosen_buff.name .. " gesegnet!")

-- Positives Feedback
context.world:spawn_particle("ENCHANT", loc.x, loc.y + 1.5, loc.z, 30, 0.5, 0.5, 0.5, 0.5)
context.world:play_sound("BLOCK_BEACON_ACTIVATE", loc.x, loc.y, loc.z, 1.0, 0.8)
end

-- Cooldown unabhaengig vom Pfad setzen
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}

Erklärung

  1. Verzweigung nach Opfergabe -- Das Skript prüft player:get_held_item() und nimmt einen von zwei Pfaden: Bestrafung, wenn der Spieler kein Gold hat, oder Belohnung, wenn er welches hat.

  2. Blitzschlag -- context.world:strike_lightning(x, y, z) schlägt echten (schadensverursachenden) Blitz an der Position des Spielers ein. Die Position des Spielers wird von player.current_location gelesen.

  3. Zombie-Spawning -- context.world:spawn_entity("zombie", x, y, z) spawnt Vanilla-Zombies. Die Schleife verteilt sie gleichmäßig in einem Kreis um den Schrein mit Trigonometrie.

  4. Negative Trankeffekte -- player:add_potion_effect("poison", 400, 1) wendet 20 Sekunden Gift II an. Effektnamen sind Kleinbuchstaben-Strings, die Bukkits PotionEffectType-Namen entsprechen.

  5. Belohnungspfad -- Wenn Gold angeboten wird, verbraucht der Schrein einen Barren und wendet einen zufälligen positiven Effekt an, analog zum Segens-Schrein-Verhalten.

  6. Cooldown -- Ein 30-Sekunden-Cooldown gilt unabhängig davon, welcher Zweig genommen wurde, und verhindert schnelle aufeinanderfolgende Bestrafung oder Belohnung.

Entity-Spawning-Performance

Das Spawnen mehrerer Entities auf einmal kann die Serverleistung beeinträchtigen. Halte die Anzahl niedrig (4-6) und erwäge einen pro-Spieler-Cooldown, wenn viele Spieler den Schrein gleichzeitig nutzen.


Beispiel: Drehender Globus

Was dieses Beispiel lehrt: Eine zeitgesteuerte Animation bei Interaktion abspielen, einen Animations-Stopp planen und mechanische Soundeffekte.

Vollständige Skriptdatei (zum Aufklappen klicken)
local SPIN_ANIMATION = "spin"
local SPIN_DURATION = 100 -- 5 Sekunden in Ticks

return {
api_version = 1,

on_spawn = function(context)
context.state.is_spinning = false
end,

on_left_click = function(context)
-- Unverwundbar machen
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
-- Verhindern, dass eine neue Drehung gestartet wird, waehrend bereits gedreht wird
if context.state.is_spinning then
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Dreh-Animation starten (nicht wiederholend)
context.prop:play_animation(SPIN_ANIMATION, true, false)
context.state.is_spinning = true

-- Mechanisches Klick-Geraeusch abspielen
context.world:play_sound("BLOCK_CHAIN_PLACE", loc.x, loc.y, loc.z, 1.0, 1.5)

-- Animation nach 5 Sekunden stoppen lassen
context.scheduler:run_later(SPIN_DURATION, function(later_context)
later_context.prop:stop_animation()
later_context.state.is_spinning = false

local stop_loc = later_context.prop.current_location
if stop_loc then
later_context.world:play_sound("BLOCK_CHAIN_FALL", stop_loc.x, stop_loc.y, stop_loc.z, 1.0, 0.8)
end
end)
end
}

Erklärung

  1. State-Schutz -- context.state.is_spinning verhindert mehrere überlappende Drehanfragen. Das Flag wird gesetzt, wenn die Drehung beginnt, und gelöscht, wenn der geplante Stopp ausgelöst wird.

  2. Zeitgesteuerte Animation -- play_animation(SPIN_ANIMATION, true, false) spielt die Animation einmal ab (kein Loop). Der scheduler:run_later(100, ...)-Aufruf stoppt die Animation nach genau 5 Sekunden, falls die Animation selbst länger ist oder wiederholt wird.

  3. Mechanische Sounds -- BLOCK_CHAIN_PLACE gibt ein klickendes/mechanisches Start-Geräusch; BLOCK_CHAIN_FALL gibt ein ablaufendes Stopp-Geräusch. Tonhöhe nach Geschmack anpassen.

  4. Callback-Context -- Der run_later-Callback verwendet later_context (nicht context) für allen State- und Weltzugriff. Dies ist das korrekte Muster für Scheduler-Callbacks.

Animationsnamen

Der Animationsname "spin" muss mit dem übereinstimmen, was in deiner Modelldatei definiert ist. Wenn dein Modell einen anderen Namen verwendet (z.B. "rotate", "turn"), aktualisiere die SPIN_ANIMATION-Konstante entsprechend.


Beispiel: Jumpscare-Prop

Was dieses Beispiel lehrt: Näherungszonen-Auslöser, einmalige Schreckeffekte mit langem Cooldown, und die Kombination von Sound/Partikeln/Animation für dramatische Wirkung.

Vollständige Skriptdatei (zum Aufklappen klicken)
local SCARE_RADIUS = 3
local COOLDOWN_TICKS = 1200 -- 60 Sekunden zwischen Schreckmomenten
local SCARE_ANIMATION = "jumpscare"

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
context.state.zone_handle = nil

local loc = context.prop.current_location
if loc == nil then return end

-- Kleine Kugelzone um den Prop erstellen
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, SCARE_RADIUS)
context.state.zone_handle = handle

-- Auf Spieler ueberwachen, die die Zone betreten
context.zones:watch(
handle,
function(player)
-- on_enter: den Schreck ausloesen
if context.state.on_cooldown then
return
end

local scare_loc = context.prop.current_location
if scare_loc == nil then return end

-- Jumpscare-Animation abspielen
context.prop:stop_animation()
context.prop:play_animation(SCARE_ANIMATION, false, false)

-- Gruseliger Sound
context.world:play_sound(
"ENTITY_GHAST_SCREAM",
scare_loc.x, scare_loc.y, scare_loc.z,
1.0, 0.7
)

-- Rauchpartikel-Burst
context.world:spawn_particle(
"CAMPFIRE_SIGNAL_SMOKE",
scare_loc.x, scare_loc.y + 1, scare_loc.z,
20, 0.5, 0.5, 0.5, 0.05
)

-- Cooldown setzen, damit es nicht sofort erneut ausloest
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end,
nil -- kein on_leave-Callback noetig
)
end,

on_left_click = function(context)
-- Unverwundbar machen
if context.event then
context.event.cancel()
end
end,

on_destroy = function(context)
-- Zonenueberwachung aufraeumen
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}

Erklärung

  1. Näherungszone -- context.zones:create_sphere() erstellt eine Zone mit 3-Block-Radius. context.zones:watch() registriert einen on-enter-Callback, der ausgelöst wird, wenn ein Spieler hineingeht.

  2. Schreckeffekte -- Der on-enter-Callback spielt eine "jumpscare"-Animation, einen Ghast-Schrei-Sound und spawnt Lagerfeuerrauch-Partikel. Die Kombination erzeugt einen plötzlichen, erschreckenden Effekt.

  3. 60-Sekunden-Cooldown -- Das Boolean-Flag-Muster verhindert, dass der Schreck wiederholt ausgelöst wird. Einmal ausgelöst, ist der Prop für 60 Sekunden still (COOLDOWN_TICKS = 1200), dann wird er wieder scharf.

  4. Kein Verlassen-Callback -- Das nil als zweites Argument von context.zones:watch() bedeutet, dass es uns nicht interessiert, wenn Spieler die Zone verlassen.

  5. Aufräumen -- on_destroy beendet die Zonenüberwachung. Obwohl Zonen automatisch aufgeräumt werden, wenn der Prop entfernt wird, ist explizites Aufräumen eine Best Practice.

Schreck-Design

Für den besten Jumpscare-Effekt verstecke den Prop um eine Ecke oder in einem dunklen Bereich. Der 3-Block-Radius stellt sicher, dass der Spieler nahe ist, bevor der Schreck ausgelöst wird. Passe SCARE_RADIUS und COOLDOWN_TICKS nach Geschmack an.


Beispiel: Goblin-Spawner-Prop

Was dieses Beispiel lehrt: prop:spawn_elitemobs_boss() verwenden, um einen Custom-Boss aus einer Prop-Interaktion zu spawnen, mit einem eleganten Fallback, wenn EliteMobs nicht installiert ist.

Vollständige Skriptdatei (zum Aufklappen klicken)
local BOSS_FILE = "goblin_warrior.yml"
local COOLDOWN_TICKS = 200 -- 10 Sekunden zwischen Spawns

return {
api_version = 1,

on_spawn = function(context)
context.state.on_cooldown = false
end,

on_left_click = function(context)
-- Unverwundbar machen
if context.event then
context.event.cancel()
end
end,

on_right_click = function(context)
local player = context.event and context.event.player
if not player then return end

-- Spawn-Spam verhindern
if context.state.on_cooldown then
player:send_message("&7Der Spawner laedt sich auf...")
return
end

local loc = context.prop.current_location
if loc == nil then return end

-- Versuchen, den EliteMobs-Boss zu spawnen
local boss = context.prop:spawn_elitemobs_boss(BOSS_FILE, loc.x, loc.y + 1, loc.z)

if boss then
-- Erfolg -- Spawn-Effekte abspielen
context.world:spawn_particle("FLAME", loc.x, loc.y + 1, loc.z, 20, 0.5, 0.5, 0.5, 0.05)
context.world:play_sound("ENTITY_EVOKER_PREPARE_SUMMON", loc.x, loc.y, loc.z, 1.0, 1.0)
player:send_message("&cEin Goblin-Krieger erscheint!")
else
-- EliteMobs ist nicht installiert oder die Boss-Datei wurde nicht gefunden
context.log:warn("Boss '" .. BOSS_FILE .. "' konnte nicht gespawnt werden -- ist EliteMobs installiert?")
player:send_message("&7Der Spawner verpufft... (EliteMobs nicht verfuegbar)")

-- Verpuff-Partikel als visuelles Feedback
context.world:spawn_particle("SMOKE", loc.x, loc.y + 1, loc.z, 10, 0.3, 0.3, 0.3, 0.02)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH", loc.x, loc.y, loc.z, 0.8, 1.2)
end

-- Cooldown setzen
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end
}

Erklärung

  1. Boss-Spawning -- context.prop:spawn_elitemobs_boss(filename, x, y, z) spawnt einen EliteMobs-Custom-Boss an den angegebenen Koordinaten. Der Dateiname muss einer .yml-Datei im custombosses-Ordner von EliteMobs entsprechen.

  2. Eleganter Fallback -- spawn_elitemobs_boss() gibt nil zurück, wenn EliteMobs nicht installiert ist oder die Boss-Datei nicht existiert. Das Skript behandelt dies mit einer Warn-Lognachricht, einem Verpuff-Partikeleffekt und einer Spielernachricht, die das Scheitern erklärt.

  3. Spawn-Versatz -- Der Boss wird bei loc.y + 1 (einen Block über dem Prop) gespawnt, um zu verhindern, dass der Boss in den Prop oder den Boden clippt.

  4. Cooldown -- Ein 10-Sekunden-Cooldown verhindert, dass Spieler das Gebiet mit Goblin-Kriegern überfluten. Passe COOLDOWN_TICKS basierend auf deinen Gameplay-Bedürfnissen an.

  5. Visuelle/Audio-Unterscheidung -- Erfolg verwendet Flammenpartikel und einen Evoker-Beschwörungssound für einen dramatischen Spawn. Misserfolg verwendet Rauch und Feuer-Lösch-Sound für einen deutlichen "Verpuffer"-Effekt, damit der Spieler weiß, dass etwas schiefging, ohne die Konsole zu überprüfen.

EliteMobs-Integration

Der Boss-Dateiname (z.B. "goblin_warrior.yml") muss einer vorhandenen Custom-Boss-Konfiguration in EliteMobs entsprechen. Wenn du eine Karte oder ein Dungeon verteilst, das dieses Skript verwendet, füge die Boss-Konfigurationsdatei bei und dokumentiere die EliteMobs-Abhängigkeit.


Bewährte Vorgehensweisen

  • Beginne mit einem kleinen Hook und überprüfe. Schreibe einen einzelnen on_spawn, der eine Log-Nachricht sendet. Bestätige, dass er feuert. Baue dann darauf auf.

  • Halte Hilfsfunktionen lokal. Deklariere Helfer wie local function toggle_door(context) oberhalb der Return-Tabelle. Das hält sie aus dem globalen Scope heraus.

  • Initialisiere allen State in on_spawn. Wenn du context.state.is_open in on_right_click liest, aber nie in on_spawn setzt, wird es nil sein und deine Vergleiche könnten sich unerwartet verhalten.

  • Breche wiederholende Aufgaben ab, wenn sie fertig sind. Jedes run_repeating sollte ein passendes cancel in on_destroy haben. Vergessene Aufgaben verschwenden CPU.

  • Verwende frische Scheduler-Callback-Contexts. Scheduler-Callbacks erhalten einen frischen Context-Parameter. Verwende immer diesen Parameter innerhalb des Callbacks, nicht den äußeren context.

  • Halte on_game_tick leichtgewichtig. Wenn du diesen Hook definierst, läuft er jeden Server-Tick (20 Mal pro Sekunde). Schütze aufwändige Arbeit hinter einer state-basierten Cooldown-Prüfung.

  • Mache Props standardmäßig unverwundbar. Sofern du nicht möchtest, dass der Prop zerstörbar ist, füge den on_left_click-Schadensabbruch in jedes Skript ein.

  • Verwende GROSSBUCHSTABEN für Bukkit-Enums. Sound-Namen und Partikel-Namen müssen das Bukkit-Enum-Konstantenformat verwenden (z.B. "FLAME", nicht "flame").


Häufige Anfängerfehler

  • Den äußeren context innerhalb eines Scheduler-Callbacks verwenden. Der äußere Context erfasst einen Snapshot zum Zeitpunkt, als der Hook ausgeführt wurde. Innerhalb von Callbacks immer den eigenen Parameter des Callbacks verwenden.

  • Vergessen, wiederholende Aufgaben abzubrechen. Wenn du ein run_repeating in on_spawn startest, aber es nie abbrichst, läuft die Aufgabe, bis der Prop entfernt wird.

  • State nicht in on_spawn initialisieren. Das Lesen von context.state.x vor dem Setzen gibt nil zurück, was deine Logik stillschweigend brechen kann.

  • Falsche Animationsnamen. Wenn play_animation("open") false zurückgibt, stimmt der Animationsname nicht mit dem in der Modelldatei überein. Überprüfe das Modell auf exakte Namen.

  • Kleinbuchstaben-Sound-/Partikel-Namen. "flame" funktioniert nicht -- verwende "FLAME". Die API konvertiert intern für Partikel zu GROSSBUCHSTABEN, aber Sound-Enum-Namen müssen exakt sein.

  • api_version = 1 vergessen. Die zurückgegebene Tabelle muss dieses Feld enthalten, sonst lädt FMM das Skript nicht.

  • Funktionen in die zurückgegebene Tabelle setzen, die keine Hooks sind. Hilfsfunktionen müssen oberhalb der return-Anweisung deklariert werden. Nur Hook-Namen (on_spawn, on_right_click, etc.) sind als Schlüssel in der zurückgegebenen Tabelle erlaubt.


QC-Checkliste

Verwende diese Checkliste, um ein Prop-Skript vor der Bereitstellung 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.
  3. context.event wird mit if context.event then geschützt, bevor cancel() aufgerufen wird.
  4. context.state-Felder werden in on_spawn initialisiert.
  5. Jeder scheduler:run_repeating(...)-Aufruf hat ein passendes scheduler:cancel(...) in on_destroy.
  6. Scheduler-Callbacks verwenden den eigenen Context-Parameter des Callbacks, nicht den äußeren context.
  7. on_game_tick-Hooks schützen aufwändige Arbeit hinter einer Prüfung.
  8. Alle Methodennamen existieren in der Prop-API-Referenz -- keine erfundenen Aliase.
  9. Sound- und Partikel-Namen verwenden GROSSBUCHSTABEN-Bukkit-Enum-Namen.
  10. Das Skript ruft keine blockierenden oder langwierigen Operationen innerhalb eines Hooks oder Callbacks auf.

Tipps zur KI-Generierung

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

  • Exakter Hook-Name -- z.B. on_right_click, nicht "wenn der Spieler den Prop anklickt".
  • Animationsnamen aus der Modelldatei -- die KI kann diese nicht erraten; gib sie an.
  • Sound-Enum-Namen -- z.B. "BLOCK_NOTE_BLOCK_HARP", nicht "Harfensound".
  • Partikel-Enum-Namen -- z.B. "FLAME", nicht "Feuerpartikel".
  • Ob der Prop unverwundbar sein soll -- wenn ja, füge on_left_click mit context.event.cancel() ein.
  • Nur dokumentierte Methodennamen verwenden -- wenn es nicht auf der Prop-API-Seite steht, existiert es nicht.

Gutes Prompt-Beispiel

Schreibe ein FMM-Prop-Skript, das die Animation "activate" bei Rechtsklick abspielt, das Prop unverwundbar macht, FLAME-Partikel an der Prop-Position beim Klick erzeugt, den BLOCK_LEVER_CLICK-Sound abspielt und einen 2-Sekunden-Cooldown zwischen Klicks mit context.state und scheduler:run_later hat.


Nächste Schritte