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
-
Hook-Wahl --
on_left_clickwird ausgelöst, wenn ein Spieler den Prop schlägt (links klickt). Unter der Haube ist dies einEntityDamageByEntityEventauf dem darunterliegenden Armor Stand des Props. -
Event-Schutz --
context.eventsollte in diesem Hook immer vorhanden sein, aber der Schutz ist eine gute Praxis. -
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
-
Konstanten auf Dateiebene --
OPEN_ANIMATIONundCLOSE_ANIMATIONwerden oberhalb der Return-Tabelle definiert. Das macht sie leicht änderbar für verschiedene Modelldateien, die möglicherweise andere Animationsnamen verwenden. -
State-Initialisierung --
on_spawnsetztcontext.state.is_open = false. Der State bleibt über alle Hooks für diese Prop-Instanz bestehen. -
Unverwundbarkeit -- Der
on_left_click-Hook bricht den Schaden ab, damit die Tür nicht versehentlich zerstört werden kann. -
Umschaltlogik --
on_right_clickprüftcontext.state.is_open, stoppt jede aktuelle Animation, spielt die entsprechende Animation ab, wechselt den State und spielt einen Sound. Derstop_animation()-Aufruf vorplay_animation()sorgt für saubere Übergänge. -
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 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
-
Konstanten --
ZONE_RADIUSundPARTICLE_INTERVALsind auf Dateiebene für einfache Anpassung. -
State-Initialisierung --
on_spawnsetzt alle State-Felder aufnil/0, bevor irgendetwas anderes passiert. -
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. -
Zonenüberwachung --
context.zones:watch()registriert Callbacks für Spieler-Betreten und -Verlassen. Die Callbacks erhöhen und verringern einen Zähler, der incontext.stategespeichert wird. -
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.
-
Aufräumen --
on_destroybricht 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.
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
-
Cooldown-Muster -- Da FMM-Prop-Skripte keine eingebaute
context.cooldowns-API wie EliteMobs haben, implementiert das Beispiel einen einfachen Cooldown mitcontext.state.on_cooldownundscheduler:run_later(). Das Flag wird auftruegesetzt, wenn der Sound abgespielt wird, und eine verzögerte Aufgabe setzt es nachCOOLDOWN_TICKSzurück. -
Sound-Wiedergabe --
context.world:play_sound()nimmt den Bukkit Sound Enum-Namen in GROSSBUCHSTABEN, Koordinaten, Lautstärke und Tonhöhe. -
Partikel-Feedback -- Noten-Partikel erscheinen über dem Prop, wenn der Sound abgespielt wird, und geben einen visuellen Hinweis.
-
Unverwundbarkeit -- Der
on_left_click-Hook bricht Schaden wie gewohnt ab. -
Scheduler-Callback-Context -- Der
run_later-Callback erhältlater_context, einen frischen Context. Wir verwendenlater_context.state(nichtcontext.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.
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
-
Auto-Start-Animation --
on_spawnspielt sofort eine wiederholende"idle"-Animation ab.falsefür blend bedeutet, sie startet frisch ohne Überblendung von einer vorherigen Animation.truefür loop bedeutet, sie wiederholt sich endlos. -
Umgebungspartikel -- Eine wiederholende Aufgabe spawnt Verzauberungstisch-Partikel über dem Prop alle 2 Sekunden und erzeugt einen magischen Umgebungseffekt.
-
Aufräumen --
on_destroybricht 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
-
Rechtsklick zum Hinsetzen --
on_right_clickerhält den Spieler voncontext.event.player, prüft, ob er bereits Passagier ist (um doppeltes Aufsteigen zu vermeiden), und ruftcontext.prop:mount(player)auf, um ihn auf den Armor Stand des Props zu setzen. -
Linksklick zum Aufstehen --
on_left_clickbricht das Schadens-Event ab (Unverwundbarkeit), prüft dann, ob der schlagende Spieler derzeit Passagier ist. Wenn ja, wirftcontext.prop:dismount(player)ihn ab. -
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. -
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
-
Item-Prüfung --
player:get_held_item()gibt eine Tabelle mittype,amountunddisplay_namefür das Haupthand-Item zurück (odernilwenn leer). Wir vergleichenheld.typemit"gold_ingot"(Materialname in Kleinbuchstaben). -
Item-Verbrauch --
player:consume_held_item(1)entfernt ein Item aus dem Haupthand-Stapel des Spielers. -
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 --600Ticks sind 30 Sekunden, Verstärker1ist Stufe II. -
Cooldown -- Dasselbe Boolean-Flag-plus-Scheduler-Muster aus dem Sound-erzeugenden Prop-Beispiel. Ein 30-Sekunden-Cooldown verhindert Schrein-Spam.
-
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
-
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. -
Blitzschlag --
context.world:strike_lightning(x, y, z)schlägt echten (schadensverursachenden) Blitz an der Position des Spielers ein. Die Position des Spielers wird vonplayer.current_locationgelesen. -
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. -
Negative Trankeffekte --
player:add_potion_effect("poison", 400, 1)wendet 20 Sekunden Gift II an. Effektnamen sind Kleinbuchstaben-Strings, die BukkitsPotionEffectType-Namen entsprechen. -
Belohnungspfad -- Wenn Gold angeboten wird, verbraucht der Schrein einen Barren und wendet einen zufälligen positiven Effekt an, analog zum Segens-Schrein-Verhalten.
-
Cooldown -- Ein 30-Sekunden-Cooldown gilt unabhängig davon, welcher Zweig genommen wurde, und verhindert schnelle aufeinanderfolgende Bestrafung oder Belohnung.
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
-
State-Schutz --
context.state.is_spinningverhindert mehrere überlappende Drehanfragen. Das Flag wird gesetzt, wenn die Drehung beginnt, und gelöscht, wenn der geplante Stopp ausgelöst wird. -
Zeitgesteuerte Animation --
play_animation(SPIN_ANIMATION, true, false)spielt die Animation einmal ab (kein Loop). Derscheduler:run_later(100, ...)-Aufruf stoppt die Animation nach genau 5 Sekunden, falls die Animation selbst länger ist oder wiederholt wird. -
Mechanische Sounds --
BLOCK_CHAIN_PLACEgibt ein klickendes/mechanisches Start-Geräusch;BLOCK_CHAIN_FALLgibt ein ablaufendes Stopp-Geräusch. Tonhöhe nach Geschmack anpassen. -
Callback-Context -- Der
run_later-Callback verwendetlater_context(nichtcontext) für allen State- und Weltzugriff. Dies ist das korrekte Muster für Scheduler-Callbacks.
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
-
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. -
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. -
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. -
Kein Verlassen-Callback -- Das
nilals zweites Argument voncontext.zones:watch()bedeutet, dass es uns nicht interessiert, wenn Spieler die Zone verlassen. -
Aufräumen --
on_destroybeendet die Zonenüberwachung. Obwohl Zonen automatisch aufgeräumt werden, wenn der Prop entfernt wird, ist explizites Aufräumen eine Best Practice.
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
-
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 imcustombosses-Ordner von EliteMobs entsprechen. -
Eleganter Fallback --
spawn_elitemobs_boss()gibtnilzurü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. -
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. -
Cooldown -- Ein 10-Sekunden-Cooldown verhindert, dass Spieler das Gebiet mit Goblin-Kriegern überfluten. Passe
COOLDOWN_TICKSbasierend auf deinen Gameplay-Bedürfnissen an. -
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.
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 ducontext.state.is_openinon_right_clickliest, aber nie inon_spawnsetzt, wird esnilsein und deine Vergleiche könnten sich unerwartet verhalten. -
Breche wiederholende Aufgaben ab, wenn sie fertig sind. Jedes
run_repeatingsollte ein passendescancelinon_destroyhaben. 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_tickleichtgewichtig. 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
contextinnerhalb 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_repeatinginon_spawnstartest, aber es nie abbrichst, läuft die Aufgabe, bis der Prop entfernt wird. -
State nicht in
on_spawninitialisieren. Das Lesen voncontext.state.xvor dem Setzen gibtnilzurück, was deine Logik stillschweigend brechen kann. -
Falsche Animationsnamen. Wenn
play_animation("open")falsezurü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 = 1vergessen. 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:
- Die Datei gibt genau eine Tabelle mit
api_version = 1zurück. - Jeder Hook-Name stimmt exakt mit einem Eintrag in der Hook-Liste überein.
context.eventwird mitif context.event thengeschützt, bevorcancel()aufgerufen wird.context.state-Felder werden inon_spawninitialisiert.- Jeder
scheduler:run_repeating(...)-Aufruf hat ein passendesscheduler:cancel(...)inon_destroy. - Scheduler-Callbacks verwenden den eigenen Context-Parameter des Callbacks, nicht den äußeren
context. on_game_tick-Hooks schützen aufwändige Arbeit hinter einer Prüfung.- Alle Methodennamen existieren in der Prop-API-Referenz -- keine erfundenen Aliase.
- Sound- und Partikel-Namen verwenden GROSSBUCHSTABEN-Bukkit-Enum-Namen.
- 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_clickmitcontext.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.