Zum Hauptinhalt springen

Lua-Skripting: Beispiele & Muster

Diese Seite enthält vollständige funktionierende Beispiele von FreeMinecraftModels-Prop- und -Item-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 Skripting bist, beginne mit Erste Schritte. Für vollständige API-Details siehe die Prop- & Item-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)
-- Make the door invulnerable
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

  1. Konstanten auf Dateiebene -- OPEN_ANIMATION und CLOSE_ANIMATION werden über der return-Tabelle definiert. Das macht sie für verschiedene Modelldateien, die unterschiedliche Animationsnamen verwenden könnten, leicht änderbar.

  2. Zustandsinitialisierung -- on_spawn setzt context.state.is_open = false. Der Zustand bleibt über alle Hooks für diese Prop-Instanz erhalten.

  3. Unverwundbarkeit -- Der on_left_click-Hook bricht 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 passende Animation ab, kehrt den Zustand um 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 am Standort 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. Prüfe deine Modelldatei auf die genauen Animationsnamen.


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)
-- Make invulnerable
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

  1. Konstanten -- ZONE_RADIUS und PARTICLE_INTERVAL befinden sich auf Dateiebene zur einfachen Feinabstimmung.

  2. Zustandsinitialisierung -- on_spawn setzt alle Zustandsfelder auf nil / 0, bevor irgendetwas anderes getan wird.

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

  4. Zonenüberwachung -- context.zones:watch(handle, function() end, function() end) aktiviert die Hooks on_zone_enter und on_zone_leave. Diese Hooks erhöhen und verringern einen in context.state gespeicherten Zähler; sie erhalten außerdem den überquerenden Spieler als context.player / context.event.player, falls du spielerspezifisches Verhalten möchtest.

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

  6. Aufräumen -- on_destroy bricht 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.

Partikel-Performance

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: Sound-erzeugendes Prop

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)
-- Make invulnerable
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

  1. Cooldown-Muster -- context.cooldowns:check_local("sound", COOLDOWN_TICKS) prüft, ob der "sound"-Cooldown bereit ist, und startet ihn, falls ja, sofort. Das hält den Klick-Handler klein und vermeidet das Erstellen von Scheduler-Aufgaben, nur um ein Flag zurückzusetzen.

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

  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 üblich ab.

Cooldown-Implementierung

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: Animiertes Ambient-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

  1. Animation automatisch starten -- on_spawn spielt sofort eine sich wiederholende "idle"-Animation ab. Das false für blend bedeutet, dass sie von vorne beginnt, ohne von einer vorherigen Animation überzublenden. Das true für loop bedeutet, dass sie sich unbegrenzt wiederholt.

  2. Ambient-Partikel -- Eine wiederkehrende Aufgabe spawnt alle 2 Sekunden Verzauberungstisch-Partikel über dem Prop und erzeugt so einen magischen Ambient-Effekt.

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


Beispiel: Sitzbarer Stuhl

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)
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 damage so the chair is invulnerable
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

  1. Rechtsklick zum Hinsetzen -- on_right_click holt den Spieler aus 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. Falls ja, wirft context.prop:dismount(player) ihn ab.

  3. 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.

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


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)
-- Make invulnerable
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

  1. Item-Prüfung -- player:get_held_item() gibt eine Tabelle mit type, amount und display_name für das Item in der Haupthand zurück (oder nil, wenn leer). held.type ist der Bukkit-Materialname in Großbuchstaben, also vergleichen wir ihn mit "GOLD_INGOT".

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

  3. 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 -- 600 Ticks sind 30 Sekunden, Verstärker 1 ist Stufe II.

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

  5. 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)
-- Make invulnerable
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

  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 doch.

  2. Blitzeinschlag -- context.world:strike_lightning(x, y, z) schlägt echten (schädigenden) Blitz am Standort des Spielers ein. Die Position des Spielers wird aus player.current_location gelesen.

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

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

  5. Belohnungspfad -- Wenn Gold geopfert wird, verbraucht der Schrein einen Barren und wendet einen zufälligen positiven Effekt an, was das Verhalten des Segnungsschreins widerspiegelt.

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

Performance beim Entitäten-Spawning

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)
-- Make invulnerable
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

  1. Zustands-Schutz -- context.state.is_spinning verhindert mehrere sich überschneidende Dreh-Anfragen. Das Flag wird gesetzt, wenn die Drehung startet, und gelöscht, wenn der geplante Stopp ausgelöst wird.

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

  3. Mechanische Sounds -- BLOCK_CHAIN_PLACE gibt einen klickenden/mechanischen Start-Sound; BLOCK_CHAIN_FALL gibt einen herunterfahrenden Stopp-Sound. Passe die Tonhöhe nach Geschmack an.

  4. Callback-Kontext -- Der run_later-Callback verwendet later_context (nicht context) für allen Zustands- und Welt-Zugriff. 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: 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)
-- Make invulnerable
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

  1. Annäherungszone -- context.zones:create_sphere() erstellt eine Zone mit 3 Blöcken Radius. context.zones:watch(handle, function() end, nil) aktiviert on_zone_enter, das ausgelöst wird, wenn ein Spieler hineintritt.

  2. 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.

  3. 60-Sekunden-Cooldown -- Das Boolean-Flag-Muster verhindert, dass der Schreck wiederholt ausgelöst wird. Einmal ausgelöst, bleibt der Prop für 60 Sekunden stumm (COOLDOWN_TICKS = 1200) und schärft sich dann wieder.

  4. Kein Austritts-Hook -- Das nil als drittes Argument an context.zones:watch() bedeutet, dass es uns egal ist, wann 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 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)
-- Make invulnerable
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

  1. 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 im custombosses-Ordner von EliteMobs übereinstimmen.

  2. Eleganter Rückfall -- 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-Logmeldung, einem Fizzle-Partikeleffekt und einer Spielernachricht, die den Fehlschlag erklärt.

  3. Spawn-Offset -- Der Boss wird bei loc.y + 1 (ein 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 an deine Gameplay-Bedürfnisse an.

  5. 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.

EliteMobs-Integration

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.


Beispiel: Frost-Schockwellen-Schwert (Item-Skript)

Was dieses Beispiel lehrt: Ein vollständiges Item-Skript, das on_equip-Zustandsinitialisierung, on_shift_right_click zum Auslösen einer Fähigkeit, scheduler:run_repeating() für eine expandierende Welle, get_nearby_entities() zur Zielfindung, Entitäten-Kampfmethoden, Partikel-/Sound-Effekte, Geländemodifikation mit get_highest_block_y(), show_action_bar() für UI-Feedback und das Absichern gegen nicht-lebende Entitäten demonstriert.

Vollständige Skriptdatei (zum Aufklappen klicken)
-- Frost Shockwave Sword
-- Shift + Right-click to release a horizontal frost wave

local WAVE_MAX_RADIUS = 12
local WAVE_SPEED = 2
local WAVE_TICK_INTERVAL = 2
local WAVE_DAMAGE = 4.0
local WAVE_KNOCKBACK = 1.2
local WAVE_CONE_ANGLE = 70

return {
api_version = 1,

on_equip = function(context)
context.state.wave_active = false
end,

on_shift_right_click = function(context)
if context.state.wave_active then return end
local loc = context.player.current_location
local yaw_rad = math.rad(loc.yaw)
local dir_x = -math.sin(yaw_rad)
local dir_z = math.cos(yaw_rad)

context.event:cancel()
context.state.wave_active = true
context.state.hit_entities = {}
context.state.destroyed_blocks = {}

local current_radius = 1
local origin_x, origin_y, origin_z = loc.x, loc.y, loc.z
local player_uuid = context.player.uuid

context.player:show_action_bar("&b&lFrost Shockwave!", 40)
context.world:play_sound("ENTITY_PLAYER_ATTACK_SWEEP", origin_x, origin_y, origin_z, 1.5, 0.5)

local task_id = context.scheduler:run_repeating(0, WAVE_TICK_INTERVAL, function(tick_context)
if current_radius > WAVE_MAX_RADIUS then
tick_context.state.wave_active = false
tick_context.scheduler:cancel(tick_context.state.wave_task)
return
end

local half_angle = math.rad(WAVE_CONE_ANGLE / 2)
local steps = math.floor(current_radius * 8)
for i = 0, steps do
local angle = (i / steps) * 2 * math.pi
local px = origin_x + math.cos(angle) * current_radius
local pz = origin_z + math.sin(angle) * current_radius
local to_x, to_z = px - origin_x, pz - origin_z
local to_len = math.sqrt(to_x * to_x + to_z * to_z)
if to_len > 0 then
local dot = (to_x * dir_x + to_z * dir_z) / to_len
if dot >= math.cos(half_angle) then
local ground_y = tick_context.world:get_highest_block_y(math.floor(px), math.floor(pz))
tick_context.world:spawn_particle("SNOWFLAKE", px, ground_y + 1.0, pz, 3, 0.3, 0.2, 0.3, 0.02)
local bx, bz = math.floor(px), math.floor(pz)
local block_key = bx .. "," .. bz
if not tick_context.state.destroyed_blocks[block_key] then
tick_context.state.destroyed_blocks[block_key] = true
local block_type = tick_context.world:get_block_at(bx, ground_y, bz)
if block_type ~= "bedrock" and block_type ~= "obsidian" and block_type ~= "barrier" then
tick_context.world:set_block_at(bx, ground_y, bz, "AIR")
end
end
end
end
end

local entities = tick_context.world:get_nearby_entities(origin_x, origin_y, origin_z, current_radius + 1)
for _, entity in ipairs(entities) do
if entity.uuid ~= player_uuid and not tick_context.state.hit_entities[entity.uuid] then
local eloc = entity.current_location
local dx, dz = eloc.x - origin_x, eloc.z - origin_z
local dist = math.sqrt(dx * dx + dz * dz)
if dist >= current_radius - 2 and dist <= current_radius + 1 and dist > 0 then
local dot = (dx * dir_x + dz * dir_z) / dist
if dot >= math.cos(half_angle) and entity.damage then
tick_context.state.hit_entities[entity.uuid] = true
entity:damage(WAVE_DAMAGE)
entity:push((dx/dist)*WAVE_KNOCKBACK, 0.3, (dz/dist)*WAVE_KNOCKBACK)
if entity.is_alive then entity:add_potion_effect("SLOWNESS", 60, 1) end
tick_context.world:spawn_particle("SNOWFLAKE", eloc.x, eloc.y+1, eloc.z, 10, 0.5, 0.5, 0.5, 0.1)
end
end
end
end

tick_context.world:play_sound("BLOCK_GLASS_BREAK",
origin_x + dir_x * current_radius, origin_y,
origin_z + dir_z * current_radius, 0.8, 1.5)
current_radius = current_radius + WAVE_SPEED
end)

context.state.wave_task = task_id
end
}

Erklärung

  1. Zustandsinitialisierung -- on_equip setzt wave_active = false, um mehrere sich überschneidende Schockwellen zu verhindern. Dieser Hook wird ausgelöst, wenn der Spieler das Schwert ausrüstet.

  2. Gerichteter Kegel -- Der Gier-Winkel (yaw) des Spielers wird in einen Richtungsvektor (dir_x, dir_z) umgewandelt. Ein 70-Grad-Kegel vor dem Spieler bestimmt die Ausbreitung der Welle.

  3. Expandierende Welle -- scheduler:run_repeating() läuft alle 2 Ticks. In jedem Tick wächst der Wellenradius um WAVE_SPEED. Partikel werden in einem Ring gespawnt, aber nur innerhalb des Kegelwinkels.

  4. Geländezerstörung -- get_highest_block_y() findet die Bodenhöhe an jedem Punkt. Oberflächenblöcke werden zerstört (auf AIR gesetzt), wobei Bedrock/Obsidian/Barrier ausgeschlossen sind. Eine destroyed_blocks-Tabelle verhindert doppelte Verarbeitung.

  5. Entitäten-Zielerfassung -- get_nearby_entities() findet alle Entitäten in der Nähe des Ursprungs. Das Skript prüft, dass jede Entität sich innerhalb des aktuellen Wellenbands, innerhalb des Kegels befindet und noch nicht getroffen wurde. Der if entity.damage then-Schutz ist entscheidend -- get_nearby_entities() gibt ALLE Entitäten zurück (Armor Stands, Items usw.), nicht nur lebende.

  6. Kampf-Effekte -- Getroffene Entitäten nehmen Schaden, werden in Richtung der Welle zurückgestoßen und erhalten 3 Sekunden Langsamkeit II.

  7. Spieler-Feedback -- show_action_bar() zeigt 40 Ticks lang „Frost Shockwave!" an. Glas-Brech-Sounds bewegen sich mit der Wellenfront vor.

  8. Aufräumen -- Wenn der Radius WAVE_MAX_RADIUS überschreitet, bricht die wiederkehrende Aufgabe sich selbst ab und setzt das wave_active-Flag zurück.

Sicherheit bei Entitätstypen

Prüfe immer if entity.damage then, bevor du entity:damage(), entity:push() oder entity:add_potion_effect() aufrufst. Die Methode get_nearby_entities() gibt alle Entitäten in Reichweite zurück, einschließlich nicht-lebender wie Armor Stands, fallengelassener Items und Erfahrungskugeln, die diese Methoden nicht haben.


Beispiel: Heilstab (Item-Skript)

Was dieses Beispiel lehrt: Ein einfacheres Item-Skript, das Rechtsklick-Aktivierung, Cooldown-Verwaltung, die item:consume()-Methode für begrenzte Verwendungen und Heilung mit Trankeffekten zeigt.

Vollständige Skriptdatei (zum Aufklappen klicken)
-- Healing Wand
-- Right-click to heal yourself. Consumes one charge per use.

local HEAL_AMOUNT = 6.0 -- 3 hearts
local COOLDOWN_TICKS = 60 -- 3 seconds
local REGEN_DURATION = 40 -- 2 seconds of regen
local REGEN_AMPLIFIER = 1 -- Regeneration II

return {
api_version = 1,

on_right_click = function(context)
-- Check remaining charges
local uses = context.item:get_uses()
if uses <= 0 then
context.player:show_action_bar("&c&lOut of charges!", 40)
context.world:play_sound("BLOCK_FIRE_EXTINGUISH",
context.player.current_location.x,
context.player.current_location.y,
context.player.current_location.z,
0.8, 1.5)
return
end

-- Check cooldown
if not context.cooldowns:check_local("heal", COOLDOWN_TICKS) then
context.player:show_action_bar("&cWand recharging...", 20)
return
end

-- Cancel the event so we don't interact with blocks
if context.event then
context.event:cancel()
end

-- Consume one charge
context.item:set_uses(uses - 1)

-- Heal the player
local loc = context.player.current_location
context.player:add_potion_effect("REGENERATION", REGEN_DURATION, REGEN_AMPLIFIER)

-- Visual and audio feedback
context.world:spawn_particle("HEART", loc.x, loc.y + 1.5, loc.z, 8, 0.4, 0.3, 0.4, 0)
context.world:spawn_particle("HAPPY_VILLAGER", loc.x, loc.y + 1, loc.z, 15, 0.5, 0.5, 0.5, 0)
context.world:play_sound("ENTITY_PLAYER_LEVELUP", loc.x, loc.y, loc.z, 0.7, 1.8)

-- UI feedback
context.player:show_action_bar("&a&lHealed! &7(" .. (uses - 1) .. " charges left)", 40)
end
}

Erklärung

  1. Cooldown-Muster -- context.cooldowns:check_local("heal", COOLDOWN_TICKS) prüft und startet den Aufladungs-Cooldown in einem Aufruf. Item-lokale Cooldowns bleiben über Ausrüst- und Ablege-Zyklen hinweg erhalten.

  2. Ladungssystem -- context.item:get_uses() liest einen benutzerdefinierten Verwendungszähler aus dem PDC des Items. Jede Verwendung verringert ihn mit set_uses(). Wenn die Ladungen 0 erreichen, spielt der Stab einen Fizzle-Sound ab und verweigert die Aktivierung.

  3. Heilung -- Anstatt die Gesundheit direkt zu setzen, wendet das Skript 2 Sekunden Regeneration II an. Das funktioniert natürlicher mit Minecrafts Gesundheitssystem und stapelt sich korrekt mit anderen Effekten.

  4. Event-Abbruch -- context.event:cancel() verhindert, dass der Rechtsklick mit nahegelegenen Blöcken interagiert (Blöcke platzieren, Türen öffnen usw.).

  5. UI-Feedback -- show_action_bar() teilt dem Spieler das Ergebnis seiner Aktion und die verbleibenden Ladungen mit. Unterschiedliche Nachrichten für Cooldown, leere Ladungen und erfolgreiche Heilung lassen das Item reaktionsfreudig wirken.

Benutzerdefinierte Items erhalten

Verwende /fmm giveitem <id>, um ein korrekt getaggtes benutzerdefiniertes Item zu erhalten. Items, die auf andere Weise erstellt wurden, fehlt möglicherweise der fmm_item_id-PDC-Schlüssel, was bedeutet, dass Skripte für sie nicht aktiviert werden.


Best Practices

  • 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 du context.state.is_open in on_right_click liest, es aber nie in on_spawn setzt, ist es nil und deine Vergleiche verhalten sich möglicherweise unerwartet.

  • Brich wiederkehrende Aufgaben ab, wenn fertig. Jedes run_repeating sollte ein passendes cancel in on_destroy haben. Ü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_tick leichtgewichtig. Wenn du diesen Hook definierst, läuft er in jedem Server-Tick (20-mal pro Sekunde). Schütze aufwendige Arbeit hinter context.cooldowns oder einem Scheduler-Intervall.

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

  • 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 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_repeating in on_spawn startest, es aber nie abbrichst, läuft die Aufgabe, bis der Prop entfernt wird.

  • Zustand nicht in on_spawn initialisieren. Das Lesen von context.state.x, bevor es gesetzt wird, 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 überein, was in der Modelldatei steht. Prüfe das Modell auf die genauen Namen.

  • Kleinbuchstaben-Sound-/Partikel-Namen. "flame" funktioniert nicht -- verwende "FLAME". Die API konvertiert Partikel intern in 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 einfügen, die keine Hooks sind. Hilfsfunktionen müssen über der return-Anweisung deklariert werden. Nur Hook-Namen (on_spawn, on_right_click usw.) sind als Schlüssel in der zurückgegebenen Tabelle erlaubt.


QC-Checkliste

Verwende diese Checkliste, um ein Prop-Skript vor dem Bereitstellen 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 Prop-Hook-Liste oder Item-Hook-Liste überein.
  3. context.event ist mit if context.event then abgesichert, 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 Kontext-Parameter des Callbacks, nicht den äußeren context.
  7. on_game_tick-Hooks schützen aufwendige Arbeit hinter einer Prüfung.
  8. Alle Methodennamen existieren in der Prop- & Item-API-Referenz -- keine erfundenen Aliase.
  9. Sound- und Partikel-Namen verwenden GROSSBUCHSTABEN-Bukkit-Enum-Namen.
  10. 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 der Prop unverwundbar sein soll -- falls ja, füge on_left_click mit context.event.cancel() hinzu.
  • Verwende nur dokumentierte Methodennamen -- wenn es nicht auf der Prop-API-Seite steht, existiert es nicht.

Gutes Prompt-Beispiel

Schreibe ein FMM-Prop-Skript, das die „activate"-Animation abspielt, wenn rechtsgeklickt wird, den Prop unverwundbar macht, FLAME-Partikel beim Klick am Prop-Standort spawnt, den BLOCK_LEVER_CLICK-Sound abspielt und einen 2-Sekunden-Cooldown zwischen den Klicks mit context.cooldowns:check_local("activate", 40) hat.


Goblin-Item-Sammlung

Ein Satz von 10 Beispiel-Item-Skripten rund um das Thema Goblin-Waffen ist als herunterladbarer Beispiel-Inhalt verfügbar. Sie sind nicht im Basis-Plugin-Jar gebündelt — die einzigen Skripte, die das Plugin selbst beim ersten Start nach scripts/ schreibt, sind die vier mitgelieferten Prop-Skripte: invulnerable.lua, pickupable.lua, storage_single.lua und storage_double.lua. Jedes Goblin-Skript unten demonstriert unterschiedliche Kampfmechaniken, Partikeleffekte und API-Verwendungsmuster.

SkriptItemEffekt
goblin_golden_sword.luaGoldenes Schwert25 % Chance auf Sweep -- schädigt und stößt alle nahegelegenen Gegner zurück
goblin_iron_axe.luaEisenaxt20 % Chance auf Spike -- schleudert das Ziel mit Tropfstein-Visual nach oben
goblin_bow.luaBogenZieht den getroffenen Mob zum Spieler
goblin_crossbow.luaArmbrust33 % Chance auf Feuerwerks-Sperrfeuer
goblin_trident.luaDreizackEiskuppel umschließt das Ziel für 3 Sek.
goblin_iron_hoe.luaEisenhacke15 % Chance auf Giftwolken
goblin_mace.luaStreitkolbenMeteor-Einschläge vom Himmel (Rechtsklick)
goblin_spear.luaSpeerFeuerstrahl entlang der Zielrichtung (Shift+Rechtsklick)
goblin_shield.luaSchildGlaskuppel beim Blocken (reaktiv)
goblin_iron_sword.luaEisenschwertAufladender Dunkelheitsring (Shift+Rechtsklick)
Kein Modell erforderlich

Diese Skripte funktionieren mit vanilla-aussehenden Items. Jedes benötigt nur eine YML-Konfiguration mit einem material:-Feld -- keine .bbmodel-Datei ist erforderlich. Um zum Beispiel das Golden-Sword-Skript zu aktivieren, erstelle eine YML-Konfiguration wie:

isEnabled: true
material: GOLDEN_SWORD
scripts:
- goblin_golden_sword.lua

Nächste Schritte