Lua-Skripting: Beispiele & Muster
Diese Seite enthält Beispiele für FreeMinecraftModels-Prop-Skripte mit Erklärungen. Stelle die referenzierten Modellanimationen und Inhalte bereit und prüfe jedes Beispiel auf deinem Server, bevor du es in einer Map verwendest.
Wenn du neu im Skripting bist, beginne mit Erste Schritte. Die vollständigen API-Details stehen in der Prop-API.
Beispiel: Unverwundbarer Prop
Was dieses Beispiel zeigt: Das einfachste nützliche Skript: Standardschaden durch Linksklicks abbrechen.
Speichere dieses Skript als invulnerable.lua im Skriptordner und lade neu, bevor du es einem Prop zuweist.
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 anklickt). Das Lua-Event ist das FMM-Abbruch-Flag für die standardmäßige Schadensbehandlung des Props. -
Event-Schutz --
context.eventsollte in diesem Hook immer vorhanden sein, aber der Schutz ist eine gute Praxis. -
Abbruch —
context.event.cancel()verhindert, dass FMM den Standardschaden dieses Klicks anwendet. Es macht den Prop nicht allgemein gegen jede Schadensquelle unverwundbar.
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)
-- Prevent default left-click damage
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
-- Close the door
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
-- Open the door
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 über der return-Tabelle definiert. Das macht sie für verschiedene Modelldateien, die unterschiedliche Animationsnamen verwenden könnten, leicht änderbar. -
Zustandsinitialisierung --
on_spawnsetztcontext.state.is_open = false. Der Zustand bleibt zwischen den Hooks dieser Skriptzuweisung an diesem Prop erhalten. Andere Skripte desselben Props haben einen eigenen Zustand. -
Klickschutz — Der Hook
on_left_clickbricht den Standardschaden dieses Klicks ab. -
Umschaltlogik --
on_right_clickfordert einen Stopp und die passende Animation an, schaltet das Flag um und spielt einen Sound. Das Flag erfasst den angeforderten Zustand, keine bestätigte Wiedergabe.blend = truestellt die Animation in die Warteschlange; Stoppen überschreibt keine aktive Todesanimation der gemeinsamen Engine. Siehe Animationen. Das Beispiel animiert das Modell, verändert aber keine Barrier-Kollisionen und garantiert keine passierbare Öffnung. -
Sounds akzeptieren Bukkit-Enum-Namen unabhängig von der Großschreibung oder gültige Schlüssel wie
minecraft:block.note_block.harpund eigene Ressourcenpaket-Schlüssel. Der Sound muss im Ressourcenpaket des Clients vorhanden sein.
Namen wie "open" und "close" müssen zum Modell passen. Eine fehlende Animation gibt false zurück; dieses Beispiel ändert trotzdem das Flag und spielt den Sound. Werte das Ergebnis aus, wenn diese Effekte von der Annahme abhängen sollen. Auch true beweist keine sichtbare Wiedergabe.
Beispiel: Annäherungs-Trigger mit Partikeln
Was dieses Beispiel lehrt: Zonenerstellung, Zonenüberwachung auf Eintritts-/Austritts-Events, Partikeleffekte und Aufräumen.
Vollständige Skriptdatei (zum Aufklappen klicken)
local ZONE_RADIUS = 8
local PARTICLE_INTERVAL = 10 -- ticks between particle 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
-- Create a sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, ZONE_RADIUS)
context.state.zone_handle = handle
-- Pass placeholder functions to enable on_zone_enter and on_zone_leave.
-- The actual logic lives in the hooks below.
context.zones:watch(handle, function() end, function() end)
-- Start a repeating particle effect at the zone boundary
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
-- Spawn particles in a ring at the zone boundary
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
-- Flame particles when players are inside
tick_context.world:spawn_particle("FLAME", px, prop_loc.y + 0.5, pz, 1, 0, 0, 0, 0)
else
-- Green particles when zone is empty
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)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_zone_enter = function(context)
context.state.players_in_zone = (context.state.players_in_zone or 0) + 1
end,
on_zone_leave = function(context)
context.state.players_in_zone = math.max(0, (context.state.players_in_zone or 0) - 1)
end,
on_destroy = function(context)
-- Clean up the repeating task
if context.state.particle_task then
context.scheduler:cancel(context.state.particle_task)
context.state.particle_task = nil
end
-- Clean up the zone watch
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_INTERVALbefinden sich auf Dateiebene zur einfachen Feinabstimmung. -
Zustandsinitialisierung --
on_spawnsetzt alle Zustandsfelder aufnil/0, bevor irgendetwas anderes getan wird. -
Zonenerstellung --
context.zones:create_sphere()erstellt eine Kugel an der Spawnposition des Props. Die numerische Kennung referenziert diese feste Zone. Der Partikelring liest dagegen die aktuelle Prop-Position. Verwende deshalb einen unbewegten Prop oder aktualisiere die Zone beim Verschieben. -
Zonenüberwachung --
context.zones:watch(handle, function() end, function() end)aktiviert die Hookson_zone_enterundon_zone_leave. Diese Hooks erhöhen und verringern einen incontext.stategespeicherten Zähler; sie erhalten außerdem den überquerenden Spieler alscontext.player/context.event.player, falls du spielerspezifisches Verhalten möchtest. -
Partikelschleife -- Eine wiederkehrende Aufgabe erzeugt den Ring alle 10 Server-Ticks, also jede halbe Sekunde bei 20 TPS. Der Partikeltyp hängt davon ab, ob Spieler in der Zone sind.
-
Aufräumen --
on_destroybricht die wiederkehrende Aufgabe ab und beendet die Zonenüberwachung. Obwohl beide automatisch aufgeräumt werden, wenn der Prop entfernt wird, ist explizites Aufräumen eine Best Practice.
Das Spawnen vieler Partikel in jedem Tick kann die Performance beeinträchtigen. Verwende ein angemessenes 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: Prop mit Soundeffekten
Was dieses Beispiel lehrt: Sounds bei Interaktion abspielen, context.cooldowns verwenden und schnell aufeinanderfolgende Interaktionen verhindern.
Vollständige Skriptdatei (zum Aufklappen klicken)
local SOUND_NAME = "BLOCK_NOTE_BLOCK_HARP"
local COOLDOWN_TICKS = 40 -- 2 seconds between sounds
return {
api_version = 1,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
-- Prevent spam
if not context.cooldowns:check_local("sound", COOLDOWN_TICKS) then
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Play the sound
context.world:play_sound(SOUND_NAME, loc.x, loc.y, loc.z, 1.0, 1.0)
-- Show some particles
context.world:spawn_particle("NOTE", loc.x, loc.y + 1.5, loc.z, 5, 0.3, 0.3, 0.3, 0)
end
}
Erklärung
-
Abklingzeit --
context.cooldowns:check_local("sound", COOLDOWN_TICKS)prüft die Abklingzeit und startet sie sofort, wenn sie bereit ist. Alle Spieler dieser Skriptzuweisung am Prop teilen sie. Diese Ticks stehen für je 50 ms vergangene Zeit; Scheduler-Verzögerungen zählen tatsächliche Server-Ticks. -
Sounds akzeptieren Bukkit-Enum-Namen unabhängig von der Großschreibung oder gültige Schlüssel wie
minecraft:block.note_block.harpund eigene Ressourcenpaket-Schlüssel. Der Sound muss im Ressourcenpaket des Clients vorhanden sein. -
Partikel-Feedback -- Noten-Partikel erscheinen über dem Prop, wenn der Sound abgespielt wird, und geben einen visuellen Hinweis.
-
Klickschutz — Der Hook
on_left_clickbricht den Standardschaden dieses Klicks ab.
Verwende context.cooldowns für normale Cooldowns. Reserviere context.state plus scheduler:run_later() für Fälle, in denen du benutzerdefinierte Zustandsänderungen am Ende der Verzögerung benötigst.
Beispiel: Animierter Umgebungs-Prop
Was dieses Beispiel lehrt: Eine sich wiederholende Animation beim Spawn starten, mit einem tick-basierten Partikel-Emitter.
Vollständige Skriptdatei (zum Aufklappen klicken)
return {
api_version = 1,
on_spawn = function(context)
-- Start the idle animation immediately, looping
context.prop:play_animation("idle", false, true)
-- Emit ambient particles every 40 ticks (2 seconds)
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
-
Animation automatisch starten —
on_spawnspielt sofort eine wiederholte"idle"-Animation ab.falsefür blend bewirkt einen sofortigen Wechsel. Der eingebaute Zustandidlewiederholt sich unabhängig vom übergebenen loop-Argument. -
Ambient-Partikel -- Eine wiederkehrende Aufgabe spawnt alle 2 Sekunden Verzauberungstisch-Partikel über dem Prop und erzeugt so einen magischen Ambient-Effekt.
-
Aufräumen --
on_destroybricht die Partikelaufgabe ab.
Beispiel: Stuhl zum Hinsetzen
Was dieses Beispiel lehrt: Einen Spieler bei Rechtsklick auf einen Prop setzen, bei 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)
-- Handle seating here instead of also using the default mount fallback
if context.event then
context.event.cancel()
end
local player = context.event and context.event.player
if not player then return end
-- Check if the player is already sitting on this prop
local passengers = context.prop:get_passengers()
for i = 1, #passengers do
if passengers[i].uuid == player.uuid then
-- Player is already seated, do nothing on right-click
return
end
end
-- Mount the player on the chair
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)
-- Cancel default left-click damage
if context.event then
context.event.cancel()
end
local player = context.event and context.event.player
if not player then return end
-- Check if the player is sitting and dismount them
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_clickholt den Spieler auscontext.event.player, prüft, ob er bereits Passagier ist (um doppeltes Aufsteigen zu vermeiden), und ruftcontext.prop:mount(player)auf, um den ersten freienm_-Sitzpunkt anzufordern. -
Linksklick zum Aufstehen —
on_left_clickbricht den Standardschaden dieses Klicks am Prop ab und prüft dann, ob der schlagende Spieler derzeit Passagier ist. Falls ja, lässtcontext.prop:dismount(player)ihn absteigen. -
Passagier-Prüfung --
context.prop:get_passengers()gibt ein Array von Entitäts-Tabellen zurück. Wir vergleichen UUIDs, um den interagierenden Spieler in der Liste zu finden. -
Sounds -- Sie begleiten die eingereihten Anfragen zum Auf- und Absteigen, keine bestätigten Sitzänderungen. Der Rechtsklick-Hook bricht FMMs standardmäßige Sitzalternative ab, damit diese nicht ebenfalls versucht, den Spieler zu setzen. Das Modell braucht einen freien
m_-Sitzpunkt.
Beispiel: Segnungsschrein
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 seconds between uses
local BLESSINGS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
{ effect = "jump_boost", name = "Leap" },
{ effect = "haste", name = "Haste" },
}
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
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
-- Check cooldown
if context.state.on_cooldown then
player:send_message("&eThe shrine is recharging... Please wait.")
return
end
-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
player:send_message("&eThe shrine demands a gold offering...")
return
end
-- Consume one gold ingot
player:consume_held_item(1)
-- Play blessing effects
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
-- Apply a random blessing
local chosen = BLESSINGS[math.random(#BLESSINGS)]
player:add_potion_effect(chosen.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe shrine blesses you with " .. chosen.name .. "!")
-- Set cooldown
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 Item in der Haupthand zurück (odernil, wenn leer).held.typeist der Bukkit-Materialname in Großbuchstaben, also vergleichen wir ihn mit"GOLD_INGOT". -
Item-Verbrauch —
player:consume_held_item(1)plant das Entfernen aus der Haupthand ein. Die Methode liest die Hand bei der Ausführung der Aufgabe und gibt kein Zahlungsergebnis zurück. Dieses Beispiel ist kein atomarer Tausch: Ein Wechsel des gehaltenen Items kann verändern, was verbraucht wird, während die Segnung sofort angewendet wird. -
Zufälliger Buff -- Die
BLESSINGS-Tabelle auf Dateiebene listet die verfügbaren positiven Effekte auf.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. -
Abklingzeit -- Das von dieser Skriptzuweisung geteilte Flag wird nach 600 Server-Ticks zurückgesetzt, also 30 Sekunden bei 20 TPS. Alle Spieler dieses Schreins teilen es. Für eine Abklingzeit nach vergangener Zeit verwende
context.cooldownswie im Sound-Beispiel. -
Feedback -- Verzauberungs- und Happy-Villager-Partikel sowie ein Beacon-Aktivierungs-Sound erzeugen ein „göttliches Segnungs"-Gefühl.
Beispiel: Verfluchter Schrein
Was dieses Beispiel lehrt: Negative Trankeffekte, Blitzeinschläge, Entitäten-Spawning und Verzweigungslogik basierend auf den Opfergaben des Spielers.
Vollständige Skriptdatei (zum Aufklappen klicken)
local COOLDOWN_TICKS = 600 -- 30 seconds between uses
local BUFFS = {
{ effect = "speed", name = "Swiftness" },
{ effect = "strength", name = "Strength" },
{ effect = "regeneration", name = "Regeneration" },
{ effect = "resistance", name = "Resistance" },
}
local CURSES = {
{ effect = "slowness", name = "Slowness" },
{ effect = "weakness", name = "Weakness" },
{ effect = "poison", name = "Poison" },
{ effect = "mining_fatigue", name = "Mining Fatigue" },
}
local ZOMBIE_COUNT = 4
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
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
-- Check cooldown
if context.state.on_cooldown then
player:send_message("&7The dark shrine pulses with residual energy...")
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Check if the player is holding a gold ingot
local held = player:get_held_item()
if not held or held.type ~= "GOLD_INGOT" then
-- No offering -- punish the player!
player:send_message("&4The shrine demands tribute! You dare approach empty-handed?!")
-- Strike lightning on the player
local player_loc = player.current_location
if player_loc then
context.world:strike_lightning(player_loc.x, player_loc.y, player_loc.z)
end
-- Spawn a horde of zombies around the shrine
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
-- Apply a random curse
local chosen_curse = CURSES[math.random(#CURSES)]
player:add_potion_effect(chosen_curse.effect, 400, 1) -- 20 seconds, level II
player:send_message("&cThe shrine curses you with " .. chosen_curse.name .. "!")
-- Ominous effects
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 offered -- reward the player
player:consume_held_item(1)
-- Apply a random buff
local chosen_buff = BUFFS[math.random(#BUFFS)]
player:add_potion_effect(chosen_buff.effect, 600, 1) -- 30 seconds, level II
player:send_message("&aThe dark shrine accepts your offering. You are blessed with " .. chosen_buff.name .. "!")
-- Positive 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
-- Set cooldown regardless of path
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 doch. -
Blitzeinschlag --
context.world:strike_lightning(x, y, z)schlägt echten (schädigenden) Blitz am Standort des Spielers ein. Die Position des Spielers wird ausplayer.current_locationgelesen. -
Zombies erzeugen --
context.world:spawn_entity("zombie", x, y, z)erzeugt gewöhnliche Zombies in einem Kreis um den Schrein. Das Entfernen des Props entfernt diese Mobs nicht. Das Beispiel prüft weder das Gelände noch begrenzt es die Gesamtzahl noch lebender Zombies. -
Negative Trankeffekte --
player:add_potion_effect("poison", 400, 1)wendet 20 Sekunden Gift II an. Effektnamen sind Kleinbuchstaben-Strings, die mit BukkitsPotionEffectType-Namen übereinstimmen. -
Belohnungspfad -- Der Schrein plant den Verbrauch aus der Haupthand ein und wendet einen zufälligen positiven Effekt an. Wie beim Segnungsschrein ist dies eine Demonstration, kein atomarer Austausch von Zahlung und Belohnung.
-
Abklingzeit -- Beide Zweige setzen dasselbe Flag für alle Spieler dieser Skriptzuweisung. Es wird nach 600 Server-Ticks zurückgesetzt, also 30 Sekunden bei 20 TPS.
Das gleichzeitige Spawnen mehrerer Entitäten kann die Server-Performance beeinträchtigen. Halte die Anzahl niedrig (4-6) und ziehe in Betracht, einen Cooldown pro Spieler hinzuzufügen, falls viele Spieler den Schrein gleichzeitig verwenden.
Beispiel: Sich drehender Globus
Was dieses Beispiel lehrt: Eine zeitgesteuerte Animation bei Interaktion abspielen, einen Animationsstopp planen und mechanische Soundeffekte.
Vollständige Skriptdatei (zum Aufklappen klicken)
local SPIN_ANIMATION = "spin"
local SPIN_DURATION = 100 -- 5 seconds in ticks
return {
api_version = 1,
on_spawn = function(context)
context.state.is_spinning = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_right_click = function(context)
-- Prevent starting a new spin while already spinning
if context.state.is_spinning then
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Start the spin animation (non-looping)
context.prop:play_animation(SPIN_ANIMATION, true, false)
context.state.is_spinning = true
-- Play a mechanical clicking sound
context.world:play_sound("BLOCK_CHAIN_PLACE", loc.x, loc.y, loc.z, 1.0, 1.5)
-- Schedule the animation to stop after 5 seconds
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
-
Zustands-Schutz --
context.state.is_spinningverhindert mehrere sich überschneidende Dreh-Anfragen. Das Flag wird gesetzt, wenn die Drehung startet, und gelöscht, wenn der geplante Stopp ausgelöst wird. -
Zeitgesteuerte Animation --
play_animation(SPIN_ANIMATION, true, false)fordert eine nicht wiederholte Animation über die Warteschlange an. Der Stopp wird 100 Server-Ticks nach dem Klick angefordert, nicht nach Wiedergabebeginn. Eine belegte Warteschlange kann die sichtbare Drehung verkürzen oder verhindern; die gemeinsame Engine stoppt keine aktive Todesanimation. Das Beispiel setzt sein Flag auch dann, wenn die Anfragefalsezurückgibt. -
Mechanische Sounds --
BLOCK_CHAIN_PLACEgibt einen klickenden/mechanischen Start-Sound;BLOCK_CHAIN_FALLgibt einen herunterfahrenden Stopp-Sound. Passe die Tonhöhe nach Geschmack an. -
Callback-Kontext -- Der
run_later-Callback verwendetlater_context(nichtcontext) für allen Zustands- und Welt-Zugriff. 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: Annäherungszonen-Trigger, einmalige Schreck-Effekte mit langem Cooldown und das Kombinieren von Sound/Partikeln/Animation für dramatische Wirkung.
Vollständige Skriptdatei (zum Aufklappen klicken)
local SCARE_RADIUS = 3
local COOLDOWN_TICKS = 1200 -- 60 seconds between scares
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
-- Create a small sphere zone around the prop
local handle = context.zones:create_sphere(loc.x, loc.y, loc.z, SCARE_RADIUS)
context.state.zone_handle = handle
-- Watch for players entering the zone. The actual scare logic
-- lives in on_zone_enter below.
context.zones:watch(handle, function() end, nil)
end,
on_zone_enter = function(context)
if context.state.on_cooldown then
return
end
local scare_loc = context.prop.current_location
if scare_loc == nil then return end
-- Play the jumpscare animation
context.prop:stop_animation()
context.prop:play_animation(SCARE_ANIMATION, false, false)
-- Scary sound
context.world:play_sound(
"ENTITY_GHAST_SCREAM",
scare_loc.x, scare_loc.y, scare_loc.z,
1.0, 0.7
)
-- Burst of smoke particles
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
)
-- Set cooldown so it does not trigger again immediately
context.state.on_cooldown = true
context.scheduler:run_later(COOLDOWN_TICKS, function(later_context)
later_context.state.on_cooldown = false
end)
end,
on_left_click = function(context)
-- Prevent default left-click damage
if context.event then
context.event.cancel()
end
end,
on_destroy = function(context)
-- Clean up the zone watch
if context.state.zone_handle then
context.zones:unwatch(context.state.zone_handle)
context.state.zone_handle = nil
end
end
}
Erklärung
-
Annäherungszone --
context.zones:create_sphere()erstellt eine feste Zone mit 3 Blöcken Radius an der Spawnposition.context.zones:watch(handle, function() end, nil)aktivierton_zone_enter. Verschiebe oder erstelle die Zone neu, wenn du den Prop bewegst. -
Schreck-Effekte -- Der
on_zone_enter-Hook spielt eine"jumpscare"-Animation, einen Ghast-Schrei-Sound ab und spawnt Lagerfeuer-Rauch-Partikel. Die Kombination erzeugt einen plötzlichen, erschreckenden Effekt. -
Abklingzeit -- Alle Spieler teilen das Flag dieser Skriptzuweisung. Es wird nach 1200 Server-Ticks zurückgesetzt, also 60 Sekunden bei 20 TPS. Danach kann der nächste Zoneneintritt einen weiteren Schreck auslösen.
-
Kein Austritts-Hook -- Das
nilals drittes Argument ancontext.zones:watch()bedeutet, dass es uns egal ist, wann 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 Radius von 3 Blöcken stellt sicher, dass der Spieler nah ist, bevor der Schreck ausgelöst wird. Passe SCARE_RADIUS und COOLDOWN_TICKS nach Geschmack an.
Beispiel: Goblin-Spawner-Prop
Was dieses Beispiel lehrt: Die Verwendung von prop:spawn_elitemobs_boss(), um einen benutzerdefinierten Boss aus einer Prop-Interaktion zu spawnen, mit einem eleganten Rückfall, falls EliteMobs nicht installiert ist.
Vollständige Skriptdatei (zum Aufklappen klicken)
local BOSS_FILE = "goblin_warrior.yml"
local COOLDOWN_TICKS = 200 -- 10 seconds between spawns
return {
api_version = 1,
on_spawn = function(context)
context.state.on_cooldown = false
end,
on_left_click = function(context)
-- Prevent default left-click damage
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
-- Prevent spawn spam
if context.state.on_cooldown then
player:send_message("&7The spawner is recharging...")
return
end
local loc = context.prop.current_location
if loc == nil then return end
-- Try to spawn the EliteMobs boss
local boss = context.prop:spawn_elitemobs_boss(BOSS_FILE, loc.x, loc.y + 1, loc.z)
if boss then
-- Success -- play spawn effects
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("&cA goblin warrior emerges!")
else
-- EliteMobs is not installed or the boss file was not found
context.log:warn("Could not spawn boss '" .. BOSS_FILE .. "' -- is EliteMobs installed?")
player:send_message("&7The spawner fizzles... (EliteMobs not available)")
-- Fizzle particles as visual 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
-- Set cooldown
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 benutzerdefinierten EliteMobs-Boss an den angegebenen Koordinaten. Der Dateiname muss mit einer.yml-Datei imcustombosses-Ordner von EliteMobs übereinstimmen. -
Fehlerhinweis --
spawn_elitemobs_boss()gibtnilzurück, wenn die Integration keinen lebenden Boss liefern kann, etwa wegen eines fehlenden Plugins, einer fehlenden Konfiguration oder eines gescheiterten Spawns. Die Beispielmeldung empfiehlt, EliteMobs zu prüfen; sie bestimmt nicht die genaue Ursache. -
Spawn-Versatz -- Die angeforderte Position liegt einen Block über dem Prop. Das ist keine Gelände- oder Kollisionsprüfung; wähle einen freien Bereich, der zum Boss passt.
-
Abklingzeit -- Die Skriptzuweisung erlaubt einen Versuch alle 200 Server-Ticks, also 10 Sekunden bei 20 TPS, gemeinsam für alle Spieler. Sie begrenzt keine lebenden Bosse und entfernt sie nicht, wenn der Prop verschwindet. Ergänze Begegnungslimits für wiederholbares Gameplay.
-
Visuelle/akustische Unterscheidung -- Erfolg verwendet Flammen-Partikel und einen Evoker-Beschwörungs-Sound für ein dramatisches Spawn. Fehlschlag verwendet Rauch und Feuer-Löschen für einen klaren „Fizzle"-Effekt, sodass der Spieler weiß, dass etwas schiefgelaufen ist, ohne die Konsole zu prüfen.
Der Boss-Dateiname (z. B. "goblin_warrior.yml") muss einer vorhandenen benutzerdefinierten Boss-Konfiguration in EliteMobs entsprechen. Wenn du eine Map oder einen Dungeon verteilst, der dieses Skript verwendet, füge die Boss-Konfigurationsdatei bei und dokumentiere die EliteMobs-Abhängigkeit.
Item-Effekte
Die Skriptbeispiele für ausgerüstete Items wurden eingestellt. Verwende benutzerdefinierte Item-Definitionen und eigene Verzauberungen für aktuelle Item-Effekte.
Bewährte Vorgehensweisen
-
Beginne mit einem winzigen Hook und überprüfe. Schreibe ein einzelnes
on_spawn, das eine Logmeldung sendet. Bestätige, dass es ausgelöst wird. Baue dann darauf auf. -
Halte Hilfsfunktionen lokal. Deklariere Helfer wie
local function toggle_door(context)über der return-Tabelle. Das hält sie aus dem globalen Scope heraus. -
Initialisiere allen Zustand in
on_spawn. Wenn ducontext.state.is_openinon_right_clickliest, es aber nie inon_spawnsetzt, ist esnilund deine Vergleiche verhalten sich möglicherweise unerwartet. -
Brich wiederkehrende Aufgaben ab, wenn fertig. Jedes
run_repeatingsollte ein passendescancelinon_destroyhaben. Übrig gebliebene Aufgaben verschwenden CPU. -
Verwende frische Scheduler-Callback-Kontexte. Scheduler-Callbacks erhalten einen frischen Kontext-Parameter. Verwende immer diesen Parameter innerhalb des Callbacks, nicht den äußeren
context. -
Halte
on_game_tickleichtgewichtig. Er läuft in jedem Server-Tick, also 20-mal pro Sekunde bei 20 TPS. Begrenze aufwendige Arbeit mitcontext.cooldownsoder einem Scheduler-Intervall. -
Steuere Linksklick-Schaden bewusst. Brich
on_left_clickab, wenn Schläge keinen Standardschaden am Prop auslösen sollen. Das blockiert nicht jede Schadensquelle. -
Partikelnamen akzeptieren
FLAMEundflame; die API vereinheitlicht die Großschreibung vor der Bukkit-Suche. Sounds akzeptieren Bukkit-Enum-Namen unabhängig von der Großschreibung oder gültige Schlüssel wieminecraft:block.note_block.harpund eigene Ressourcenpaket-Schlüssel. Der Sound muss im Ressourcenpaket des Clients vorhanden sein.
Häufige Anfängerfehler
-
Den äußeren
contextinnerhalb eines Scheduler-Callbacks verwenden. Der äußere Kontext erfasst eine Momentaufnahme zu dem Zeitpunkt, an dem der Hook lief. Verwende innerhalb von Callbacks immer den eigenen Parameter des Callbacks. -
Vergessen, wiederkehrende Aufgaben abzubrechen. Wenn du ein
run_repeatinginon_spawnstartest, es aber nie abbrichst, läuft die Aufgabe, bis der Prop entfernt wird. -
Zustand nicht in
on_spawninitialisieren. Das Lesen voncontext.state.x, bevor es gesetzt wird, gibtnilzurück, was deine Logik stillschweigend brechen kann. -
Falsche Animationsnamen. Wenn
play_animation("open")falsezurückgibt, stimmt der Animationsname nicht mit dem überein, was in der Modelldatei steht. Prüfe das Modell auf die genauen Namen. -
Partikelnamen akzeptieren
FLAMEundflame; die API vereinheitlicht die Großschreibung vor der Bukkit-Suche. Sounds akzeptieren Bukkit-Enum-Namen unabhängig von der Großschreibung oder gültige Schlüssel wieminecraft:block.note_block.harpund eigene Ressourcenpaket-Schlüssel. Der Sound muss im Ressourcenpaket des Clients vorhanden sein. -
api_version = 1vergessen. Die zurückgegebene Tabelle muss dieses Feld enthalten, sonst lädt FMM das Skript nicht. -
Hilfsfunktionen in die Rückgabetabelle setzen. Außer
api_versionund optionalempriorityakzeptiert die Tabelle nur unterstützte Hook-Namen mit Funktionswerten. Definiere Helfer lokal außerhalb der Tabelle. FMM führt Skripte in ihrer konfigurierten Listenreihenfolge aus;prioritysortiert sie nicht um.
QC-Checkliste
Verwende diese Checkliste, um ein Prop-Skript vor dem Bereitstellen zu überprüfen:
- Die Datei gibt genau eine Tabelle mit
api_version = 1zurück. - Jeder Hook-Name stimmt exakt mit einem Eintrag in der Prop-Hook-Liste überein. Verzauberungs-Hooks gehören zu einer separaten Laufzeit.
- Prüfe außerhalb eines bekannten Klick-Hooks sowohl
context.eventals auchcontext.event.cancel, bevor du abbrichst. context.state-Felder werden inon_spawninitialisiert.- Jeder
scheduler:run_repeating(...)-Aufruf hat ein passendesscheduler:cancel(...)inon_destroy. - Scheduler-Callbacks verwenden den eigenen Kontext-Parameter des Callbacks, nicht den äußeren
context. on_game_tick-Hooks schützen aufwendige Arbeit hinter einer Prüfung.- Alle Methodennamen existieren in der Prop-API-Referenz; verwende keine erfundenen Aliasse.
- Partikelnamen akzeptieren
FLAMEundflame; die API vereinheitlicht die Großschreibung vor der Bukkit-Suche. Sounds akzeptieren Bukkit-Enum-Namen unabhängig von der Großschreibung oder gültige Schlüssel wieminecraft:block.note_block.harpund eigene Ressourcenpaket-Schlüssel. Der Sound muss im Ressourcenpaket des Clients vorhanden sein. - Das Skript ruft keine blockierenden oder lang laufenden Operationen innerhalb eines Hooks oder Callbacks auf.
Tipps zur KI-Generierung
Wenn du möchtest, dass eine 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 auf den Prop klickt". - Animationsnamen aus der Modelldatei -- die KI kann diese nicht erraten; gib sie an.
- Sound-Enum-Namen -- z. B.
"BLOCK_NOTE_BLOCK_HARP", nicht „Harfen-Sound". - Partikel-Enum-Namen -- z. B.
"FLAME", nicht „Feuer-Partikel". - Ob Linksklicks Standardschaden verursachen sollen — brich diese Behandlung mit
on_left_clickundcontext.event.cancel()ab. - Verwende geprüfte Methodennamen -- nutze zuerst die Prop-API-Referenz. Fehlt eine benötigte Operation, prüfe den aktuellen Laufzeitcode, statt einen Alias zu erfinden.
Gutes Prompt-Beispiel
Schreibe ein FMM-Prop-Skript, das bei Rechtsklick die Animation "activate" abspielt, Standardschaden durch Linksklicks abbricht, beim Klick FLAME-Partikel am Prop-Standort erzeugt, den Sound BLOCK_LEVER_CLICK abspielt und mit context.cooldowns:check_local("activate", 40) eine Abklingzeit von 2 Sekunden zwischen Klicks verwendet.
Kompatibilität von Item-Inhalten
Ältere Item-Pakete mit scripts: benötigen aktualisierte Item-Definitionen und Verzauberungsinhalte. FMM überspringt diese eingestellten Definitionen und lässt die Dateien unverändert. Das Hinzufügen eines Felds material: aktiviert kein Item-Skript mehr.