Scripts Lua : Exemples et patterns
Cette page contient des exemples complets fonctionnels de scripts de props et d'objets FreeMinecraftModels, ainsi que des patterns pratiques et bonnes pratiques. Chaque exemple inclut une explication détaillée de ce qu'il fait et pourquoi.
Si vous êtes nouveau dans les scripts, commencez par Pour commencer. Pour les détails complets de l'API, consultez l'API Prop et Objet.
Exemple : Prop invulnérable
Ce que cet exemple enseigne : Le script utile le plus simple -- annuler les dégâts pour que le prop ne puisse pas être cassé.
C'est le script pré-configuré livré avec FreeMinecraftModels.
Fichier de script complet (cliquez pour développer)
return {
api_version = 1,
on_left_click = function(context)
if context.event then
context.event.cancel()
end
end
}
Explication détaillée
-
Choix du hook --
on_left_clickse déclenche lorsqu'un joueur frappe (clic gauche) le prop. Sous le capot, c'est unEntityDamageByEntityEventsur le support d'armure du prop. -
Garde d'événement --
context.eventdevrait toujours être présent dans ce hook, mais la garde est une bonne pratique. -
Annulation --
context.event.cancel()annule l'événement de dégâts, ce qui empêche le support d'armure de prendre des dégâts et d'être détruit.
Utilisation
Ajoutez à la configuration .yml de votre prop :
isEnabled: true
scripts:
- invulnerable.lua
Exemple : Porte interactive
Ce que cet exemple enseigne : Basculer un état au clic droit, jouer et arrêter des animations, et utiliser context.state pour suivre si la porte est ouverte ou fermée.
Fichier de script complet (cliquez pour développer)
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
}
Explication détaillée
-
Constantes à la portée du fichier --
OPEN_ANIMATIONetCLOSE_ANIMATIONsont définies au-dessus de la table de retour. Cela les rend faciles à modifier pour différents fichiers de modèle qui pourraient utiliser des noms d'animation différents. -
Initialisation de l'état --
on_spawndéfinitcontext.state.is_open = false. L'état persiste à travers tous les hooks pour cette instance de prop. -
Invulnérabilité -- Le hook
on_left_clickannule les dégâts afin que la porte ne puisse pas être cassée par accident. -
Logique de bascule --
on_right_clickvérifiecontext.state.is_open, arrête toute animation en cours, joue l'animation appropriée, inverse l'état et joue un son. L'appelstop_animation()avantplay_animation()assure des transitions propres. -
Retour sonore --
context.world:play_sound()joue le nom de l'enum Sound de Bukkit à l'emplacement du prop. Les noms de son doivent être des noms d'enum en MAJUSCULES.
Les noms d'animation comme "open" et "close" doivent correspondre à ce qui est défini dans le fichier de modèle. Si l'animation n'est pas trouvée, play_animation() renvoie false et rien ne se passe. Vérifiez votre fichier de modèle pour les noms d'animation exacts.
Exemple : Déclencheur de proximité avec particules
Ce que cet exemple enseigne : Création de zone, surveillance de zone pour les événements d'entrée/sortie, effets de particules et nettoyage.
Fichier de script complet (cliquez pour développer)
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
}
Explication détaillée
-
Constantes --
ZONE_RADIUSetPARTICLE_INTERVALsont à la portée du fichier pour un ajustement facile. -
Initialisation de l'état --
on_spawninitialise tous les champs d'état ànil/0avant de faire quoi que ce soit d'autre. -
Création de zone --
context.zones:create_sphere()crée une zone sphérique centrée sur le prop. Le handle renvoyé est un ID numérique utilisé pour référencer cette zone plus tard. -
Surveillance de zone --
context.zones:watch(handle, function() end, function() end)active les hookson_zone_entereton_zone_leave. Ces hooks incrémentent et décrémentent un compteur stocké danscontext.state; ils reçoivent aussi le joueur qui traverse viacontext.player/context.event.playersi vous voulez un comportement spécifique au joueur. -
Boucle de particules -- Une tâche répétée fait apparaître des particules en anneau autour du prop toutes les demi-secondes. Le type de particule change selon que des joueurs sont dans la zone ou non.
-
Nettoyage --
on_destroyannule la tâche répétée et arrête la surveillance de la zone. Bien que les deux soient nettoyés automatiquement lorsque le prop est retiré, le nettoyage explicite est une bonne pratique.
Faire apparaître de nombreuses particules à chaque tick peut impacter les performances. Utilisez un intervalle raisonnable (10-20 ticks) et gardez le nombre de particules bas. L'exemple ci-dessus utilise PARTICLE_INTERVAL = 10 (deux fois par seconde) avec seulement 12 particules par anneau.
Exemple : Prop émetteur de son
Ce que cet exemple enseigne : Jouer des sons à l'interaction, utiliser context.cooldowns et empêcher les interactions en rafale.
Fichier de script complet (cliquez pour développer)
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
}
Explication détaillée
-
Pattern de cooldown --
context.cooldowns:check_local("sound", COOLDOWN_TICKS)vérifie si le cooldown"sound"est prêt et, si c'est le cas, le démarre immédiatement. Cela garde le gestionnaire de clic petit et évite de créer des tâches de scheduler juste pour réinitialiser un flag. -
Lecture du son --
context.world:play_sound()prend le nom de l'enum Sound de Bukkit en MAJUSCULES, les coordonnées, le volume et la hauteur. -
Retour par particules -- Des particules de note apparaissent au-dessus du prop lorsque le son est joué, donnant un indice visuel.
-
Invulnérabilité -- Le hook
on_left_clickannule les dégâts comme d'habitude.
Utilisez context.cooldowns pour les cooldowns normaux. Réservez context.state plus scheduler:run_later() pour les cas où vous avez besoin de changements d'état personnalisés à la fin du délai.
Exemple : Prop ambiant animé
Ce que cet exemple enseigne : Démarrer une animation en boucle au spawn, avec un émetteur de particules basé sur les ticks.
Fichier de script complet (cliquez pour développer)
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
}
Explication détaillée
-
Démarrage automatique de l'animation --
on_spawnjoue immédiatement une animation"idle"en boucle. Lefalsepour blend signifie qu'elle démarre à neuf sans mélange depuis une animation précédente. Letruepour loop signifie qu'elle se répète indéfiniment. -
Particules ambiantes -- Une tâche répétée fait apparaître des particules de table d'enchantement au-dessus du prop toutes les 2 secondes, créant un effet ambiant magique.
-
Nettoyage --
on_destroyannule la tâche de particules.
Exemple : Chaise pour s'asseoir
Ce que cet exemple enseigne : Monter un joueur sur un prop au clic droit, le démonter au clic gauche, et utiliser context.event.player pour obtenir le joueur qui interagit.
Fichier de script complet (cliquez pour développer)
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
}
Explication détaillée
-
Clic droit pour s'asseoir --
on_right_clickobtient le joueur depuiscontext.event.player, vérifie s'il est déjà passager (pour éviter un double montage), et appellecontext.prop:mount(player)pour l'asseoir sur le support d'armure du prop. -
Clic gauche pour se lever --
on_left_clickannule l'événement de dégâts (invulnérabilité), puis vérifie si le joueur qui frappe est actuellement passager. Si c'est le cas,context.prop:dismount(player)l'éjecte. -
Vérification des passagers --
context.prop:get_passengers()renvoie un tableau de tables d'entités. Nous comparons les UUID pour trouver le joueur qui interagit dans la liste. -
Retour sonore -- Un son de pose de bois est joué en s'asseyant et un son de casse de bois en se levant, donnant un retour tactile.
Exemple : Sanctuaire de bénédiction
Ce que cet exemple enseigne : Vérifier l'objet tenu par le joueur, consommer des objets, appliquer des effets de potion aléatoires, gestion du cooldown, et retour par particules/son.
Fichier de script complet (cliquez pour développer)
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
}
Explication détaillée
-
Vérification de l'objet --
player:get_held_item()renvoie une table avectype,amountetdisplay_namepour l'objet en main principale (ounilsi vide).held.typeest le nom de matériau Bukkit en majuscules, donc nous le comparons à"GOLD_INGOT". -
Consommation de l'objet --
player:consume_held_item(1)retire un objet de la pile en main principale du joueur. -
Buff aléatoire -- La table
BLESSINGSà la portée du fichier liste les effets positifs disponibles.math.random(#BLESSINGS)en choisit un au hasard.player:add_potion_effect(effect, duration, amplifier)l'applique --600ticks correspond à 30 secondes, l'amplificateur1est le niveau II. -
Cooldown -- Le même pattern flag-booléen-plus-scheduler que dans l'exemple du Prop émetteur de son. Un cooldown de 30 secondes empêche le spam du sanctuaire.
-
Retour -- Des particules d'enchantement et de villageois content plus un son d'activation de balise créent une sensation de « bénédiction divine ».
Exemple : Sanctuaire maudit
Ce que cet exemple enseigne : Effets de potion négatifs, frappes de foudre, apparition d'entités, et logique de branchement basée sur les offrandes du joueur.
Fichier de script complet (cliquez pour développer)
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
}
Explication détaillée
-
Branchement sur l'offrande -- Le script vérifie
player:get_held_item()et emprunte l'un de deux chemins : punition si le joueur n'a pas d'or, ou récompense s'il en a. -
Frappe de foudre --
context.world:strike_lightning(x, y, z)frappe une vraie foudre (qui inflige des dégâts) à la position du joueur. La position du joueur est lue depuisplayer.current_location. -
Apparition de zombies --
context.world:spawn_entity("zombie", x, y, z)fait apparaître des zombies vanilla. La boucle les répartit uniformément en cercle autour du sanctuaire à l'aide de la trigonométrie. -
Effets de potion négatifs --
player:add_potion_effect("poison", 400, 1)applique 20 secondes de Poison II. Les noms d'effet sont des chaînes en minuscules correspondant aux nomsPotionEffectTypede Bukkit. -
Chemin de récompense -- Lorsque de l'or est offert, le sanctuaire consomme un lingot et applique un effet positif aléatoire, reflétant le comportement du Sanctuaire de bénédiction.
-
Cooldown -- Un cooldown de 30 secondes s'applique quel que soit le chemin emprunté, empêchant la punition ou la récompense en rafale.
Faire apparaître plusieurs entités à la fois peut impacter les performances du serveur. Gardez le nombre bas (4-6) et envisagez d'ajouter un cooldown par joueur si de nombreux joueurs utilisent le sanctuaire simultanément.
Exemple : Globe tournant
Ce que cet exemple enseigne : Jouer une animation chronométrée à l'interaction, programmer l'arrêt d'une animation, et effets sonores mécaniques.
Fichier de script complet (cliquez pour développer)
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
}
Explication détaillée
-
Garde d'état --
context.state.is_spinningempêche plusieurs demandes de rotation qui se chevauchent. Le flag est défini lorsque la rotation démarre et effacé lorsque l'arrêt programmé se déclenche. -
Animation chronométrée --
play_animation(SPIN_ANIMATION, true, false)joue l'animation une fois (sans boucle). L'appelscheduler:run_later(100, ...)arrête l'animation après exactement 5 secondes, au cas où l'animation elle-même serait plus longue ou en boucle. -
Sons mécaniques --
BLOCK_CHAIN_PLACEdonne un son de démarrage cliquetant/mécanique ;BLOCK_CHAIN_FALLdonne un son d'arrêt en fin de course. Ajustez la hauteur selon vos goûts. -
Contexte du callback -- Le callback
run_laterutiliselater_context(et noncontext) pour tous les accès à l'état et au monde. C'est le pattern correct pour les callbacks de scheduler.
Le nom d'animation "spin" doit correspondre à ce qui est défini dans votre fichier de modèle. Si votre modèle utilise un nom différent (par ex. "rotate", "turn"), mettez à jour la constante SPIN_ANIMATION en conséquence.
Exemple : Prop jumpscare
Ce que cet exemple enseigne : Déclencheurs de zone de proximité, effets de frayeur one-shot avec un long cooldown, et combinaison de son/particules/animation pour un impact dramatique.
Fichier de script complet (cliquez pour développer)
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
}
Explication détaillée
-
Zone de proximité --
context.zones:create_sphere()crée une zone d'un rayon de 3 blocs.context.zones:watch(handle, function() end, nil)activeon_zone_enter, qui se déclenche lorsqu'un joueur y pénètre. -
Effets de frayeur -- Le hook
on_zone_enterjoue une animation"jumpscare", un son de cri de ghast, et fait apparaître des particules de fumée de feu de camp. La combinaison crée un effet soudain et effrayant. -
Cooldown de 60 secondes -- Le pattern de flag booléen empêche la frayeur de se déclencher de façon répétée. Une fois déclenché, le prop reste silencieux pendant 60 secondes (
COOLDOWN_TICKS = 1200), puis se réarme. -
Pas de hook de sortie -- Le troisième argument
nildecontext.zones:watch()signifie que nous ne nous soucions pas du moment où les joueurs quittent la zone. -
Nettoyage --
on_destroyarrête la surveillance de la zone. Bien que les zones soient nettoyées automatiquement lorsque le prop est retiré, le nettoyage explicite est une bonne pratique.
Pour un meilleur effet de jumpscare, cachez le prop au coin d'un mur ou dans une zone sombre. Le rayon de 3 blocs garantit que le joueur est proche avant que la frayeur ne se déclenche. Ajustez SCARE_RADIUS et COOLDOWN_TICKS selon vos goûts.
Exemple : Prop invocateur de gobelin
Ce que cet exemple enseigne : Utiliser prop:spawn_elitemobs_boss() pour faire apparaître un boss personnalisé depuis une interaction de prop, avec un repli gracieux si EliteMobs n'est pas installé.
Fichier de script complet (cliquez pour développer)
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
}
Explication détaillée
-
Apparition de boss --
context.prop:spawn_elitemobs_boss(filename, x, y, z)fait apparaître un boss personnalisé EliteMobs aux coordonnées données. Le nom de fichier doit correspondre à un fichier.ymldans le dossiercustombossesd'EliteMobs. -
Repli gracieux --
spawn_elitemobs_boss()renvoienilsi EliteMobs n'est pas installé ou si le fichier de boss n'existe pas. Le script gère cela avec un message de log d'avertissement, un effet de particules de raté, et un message au joueur expliquant l'échec. -
Décalage d'apparition -- Le boss apparaît à
loc.y + 1(un bloc au-dessus du prop) pour éviter qu'il ne s'enfonce dans le prop ou le sol. -
Cooldown -- Un cooldown de 10 secondes empêche les joueurs d'inonder la zone de guerriers gobelins. Ajustez
COOLDOWN_TICKSselon vos besoins de gameplay. -
Distinction visuelle/audio -- Le succès utilise des particules de flamme et un son d'invocation d'evoker pour une apparition dramatique. L'échec utilise de la fumée et un son d'extinction de feu pour un effet de « raté » clair, afin que le joueur sache que quelque chose a mal tourné sans vérifier la console.
Le nom de fichier du boss (par ex. "goblin_warrior.yml") doit correspondre à une configuration de boss personnalisé existante dans EliteMobs. Si vous distribuez une carte ou un donjon qui utilise ce script, incluez le fichier de configuration du boss et documentez la dépendance EliteMobs.
Exemple : Épée de l'onde de choc glaciale (script d'objet)
Ce que cet exemple enseigne : Un script d'objet complet démontrant l'initialisation de l'état dans on_equip, on_shift_right_click pour déclencher une capacité, scheduler:run_repeating() pour une onde en expansion, get_nearby_entities() pour trouver des cibles, les méthodes de combat d'entité, les effets de particules/son, la modification du terrain avec get_highest_block_y(), show_action_bar() pour le retour d'UI, et la protection contre les entités non vivantes.
Fichier de script complet (cliquez pour développer)
-- 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
}
Explication détaillée
-
Initialisation de l'état --
on_equipdéfinitwave_active = falsepour empêcher plusieurs ondes de choc qui se chevauchent. Ce hook se déclenche lorsque le joueur équipe l'épée. -
Cône directionnel -- Le yaw du joueur est converti en vecteur de direction (
dir_x,dir_z). Un cône de 70 degrés devant le joueur détermine l'étalement de l'onde. -
Onde en expansion --
scheduler:run_repeating()s'exécute tous les 2 ticks. À chaque tick, le rayon de l'onde croît deWAVE_SPEED. Des particules apparaissent en anneau, mais uniquement dans l'angle du cône. -
Destruction du terrain --
get_highest_block_y()trouve le niveau du sol à chaque point. Les blocs de surface sont détruits (mis à AIR), avec bedrock/obsidienne/barrière exclus. Une tabledestroyed_blocksempêche le double traitement. -
Ciblage des entités --
get_nearby_entities()trouve toutes les entités près de l'origine. Le script vérifie que chaque entité est dans la bande actuelle de l'onde, à l'intérieur du cône, et n'a pas déjà été touchée. La gardeif entity.damage thenest cruciale --get_nearby_entities()renvoie TOUTES les entités (supports d'armure, objets, etc.), pas seulement les vivantes. -
Effets de combat -- Les entités touchées prennent des dégâts, sont repoussées dans la direction de l'onde, et reçoivent 3 secondes de Lenteur II.
-
Retour au joueur --
show_action_bar()affiche « Frost Shockwave! » pendant 40 ticks. Des sons de bris de verre avancent avec le front de l'onde. -
Nettoyage -- Lorsque le rayon dépasse
WAVE_MAX_RADIUS, la tâche répétée s'annule elle-même et réinitialise le flagwave_active.
Vérifiez toujours if entity.damage then avant d'appeler entity:damage(), entity:push(), ou entity:add_potion_effect(). La méthode get_nearby_entities() renvoie toutes les entités à portée, y compris les non vivantes comme les supports d'armure, les objets lâchés et les orbes d'expérience qui n'ont pas ces méthodes.
Exemple : Baguette de soin (script d'objet)
Ce que cet exemple enseigne : Un script d'objet plus simple montrant l'activation au clic droit, la gestion du cooldown, la méthode item:consume() pour un nombre limité d'utilisations, et le soin avec des effets de potion.
Fichier de script complet (cliquez pour développer)
-- 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
}
Explication détaillée
-
Pattern de cooldown --
context.cooldowns:check_local("heal", COOLDOWN_TICKS)vérifie et démarre le cooldown de recharge en un seul appel. Les cooldowns locaux d'objet persistent à travers les cycles d'équipement et de déséquipement. -
Système de charges --
context.item:get_uses()lit un compteur d'utilisations personnalisé depuis le PDC de l'objet. Chaque utilisation le décrémente avecset_uses(). Lorsque les charges atteignent 0, la baguette joue un son de raté et refuse de s'activer. -
Soin -- Plutôt que de définir directement la santé, le script applique Régénération II pendant 2 secondes. Cela fonctionne plus naturellement avec le système de santé de Minecraft et se cumule correctement avec d'autres effets.
-
Annulation de l'événement --
context.event:cancel()empêche le clic droit d'interagir avec les blocs proches (poser des blocs, ouvrir des portes, etc.). -
Retour d'UI --
show_action_bar()indique au joueur le résultat de son action et les charges restantes. Des messages différents pour le cooldown, le manque de charges et le soin réussi rendent l'objet réactif.
Utilisez /fmm giveitem <id> pour obtenir un objet personnalisé correctement tagué. Les objets créés par d'autres moyens peuvent manquer de la clé PDC fmm_item_id, ce qui signifie que les scripts ne s'activeront pas pour eux.
Bonnes pratiques
-
Commencez par un hook minuscule et vérifiez. Écrivez un seul
on_spawnqui envoie un message de log. Confirmez qu'il se déclenche. Puis construisez à partir de là. -
Gardez les fonctions d'aide locales. Déclarez les helpers comme
local function toggle_door(context)au-dessus de la table de retour. Cela les garde hors de la portée globale. -
Initialisez tout l'état dans
on_spawn. Si vous lisezcontext.state.is_opendanson_right_clickmais ne le définissez jamais danson_spawn, il seranilet vos comparaisons peuvent se comporter de manière inattendue. -
Annulez les tâches répétées une fois terminées. Chaque
run_repeatingdevrait avoir uncancelcorrespondant danson_destroy. Les tâches non nettoyées gaspillent du CPU. -
Utilisez des contextes de callback de scheduler frais. Les callbacks de scheduler reçoivent un paramètre de contexte frais. Utilisez toujours ce paramètre à l'intérieur du callback, et non le
contextextérieur. -
Gardez
on_game_tickléger. Si vous définissez ce hook, il s'exécute à chaque tick du serveur (20 fois par seconde). Encadrez le travail coûteux derrièrecontext.cooldownsou un intervalle de scheduler. -
Rendez les props invulnérables par défaut. À moins que vous ne vouliez que le prop soit cassable, incluez l'annulation des dégâts dans
on_left_clickdans chaque script. -
Utilisez les MAJUSCULES pour les enums Bukkit. Les noms de son et de particule doivent utiliser le format constant d'enum Bukkit (par ex.
"FLAME", et non"flame").
Erreurs courantes de débutant
-
Utiliser le
contextextérieur à l'intérieur d'un callback de scheduler. Le contexte extérieur capture un instantané au moment où le hook s'est exécuté. À l'intérieur des callbacks, utilisez toujours le propre paramètre du callback. -
Oublier d'annuler les tâches répétées. Si vous démarrez un
run_repeatingdanson_spawnmais ne l'annulez jamais, la tâche s'exécute jusqu'à ce que le prop soit retiré. -
Ne pas initialiser l'état dans
on_spawn. Lirecontext.state.xavant de le définir renvoienil, ce qui peut casser votre logique silencieusement. -
Mauvais noms d'animation. Si
play_animation("open")renvoiefalse, le nom d'animation ne correspond pas à ce qui est dans le fichier de modèle. Vérifiez le modèle pour les noms exacts. -
Noms de son/particule en minuscules.
"flame"ne fonctionne pas -- utilisez"FLAME". L'API convertit en MAJUSCULES en interne pour les particules, mais les noms d'enum Sound doivent être exacts. -
Oublier
api_version = 1. La table renvoyée doit inclure ce champ, sinon FMM ne chargera pas le script. -
Mettre dans la table renvoyée des fonctions qui ne sont pas des hooks. Les fonctions d'aide doivent être déclarées au-dessus de l'instruction
return. Seuls les noms de hook (on_spawn,on_right_click, etc.) sont autorisés comme clés dans la table renvoyée.
Liste de vérification QC
Utilisez cette liste de vérification pour valider un script de prop avant de le déployer :
- Le fichier renvoie exactement une table avec
api_version = 1. - Chaque nom de hook correspond exactement à une entrée dans la liste des hooks de prop ou la liste des hooks d'objet.
context.eventest protégé parif context.event thenavant d'appelercancel().- Les champs
context.statesont initialisés danson_spawn. - Chaque appel
scheduler:run_repeating(...)a unscheduler:cancel(...)correspondant danson_destroy. - Les callbacks de scheduler utilisent le propre paramètre de contexte du callback, et non le
contextextérieur. - Les hooks
on_game_tickencadrent le travail coûteux derrière une vérification. - Tous les noms de méthode existent dans la référence de l'API Prop et Objet -- pas d'alias inventés.
- Les noms de son et de particule utilisent les noms d'enum Bukkit en MAJUSCULES.
- Le script n'appelle aucune opération bloquante ou de longue durée à l'intérieur d'un hook ou d'un callback.
Conseils pour la génération par IA
Si vous voulez que l'IA génère des scripts de prop de manière fiable, assurez-vous que le prompt inclut :
- Le nom exact du hook -- par ex.
on_right_click, et non « quand le joueur clique sur le prop ». - Les noms d'animation du fichier de modèle -- l'IA ne peut pas les deviner ; fournissez-les.
- Les noms d'enum de son -- par ex.
"BLOCK_NOTE_BLOCK_HARP", et non « son de harpe ». - Les noms d'enum de particule -- par ex.
"FLAME", et non « particules de feu ». - Si le prop doit être invulnérable -- si oui, incluez
on_left_clickaveccontext.event.cancel(). - N'utilisez que des noms de méthode documentés -- s'il n'est pas sur la page de l'API Prop, il n'existe pas.
Bon exemple de prompt
Écris un script de prop FMM qui joue l'animation « activate » au clic droit, rend le prop invulnérable, fait apparaître des particules FLAME à l'emplacement du prop au clic, joue le son BLOCK_LEVER_CLICK, et a un cooldown de 2 secondes entre les clics en utilisant context.cooldowns:check_local("activate", 40).
Collection d'objets gobelins
Un ensemble de 10 scripts d'objets exemple sur le thème des armes de gobelin est disponible en tant que contenu exemple téléchargeable. Ils ne sont pas inclus dans le jar de base du plugin — les seuls scripts que le plugin lui-même écrit dans scripts/ à la première exécution sont les quatre scripts de prop pré-configurés : invulnerable.lua, pickupable.lua, storage_single.lua et storage_double.lua. Chaque script de gobelin ci-dessous démontre différentes mécaniques de combat, effets de particules et patterns d'utilisation de l'API.
| Script | Objet | Effet |
|---|---|---|
goblin_golden_sword.lua | Golden Sword | 25 % de chance de balayage -- inflige des dégâts et repousse tous les ennemis proches |
goblin_iron_axe.lua | Iron Axe | 20 % de chance d'empalement -- propulse la cible vers le haut avec un visuel de dripstone |
goblin_bow.lua | Bow | Attire le mob touché vers le joueur |
goblin_crossbow.lua | Crossbow | 33 % de chance de barrage de feux d'artifice |
goblin_trident.lua | Trident | Un dôme de glace enferme la cible pendant 3 s |
goblin_iron_hoe.lua | Iron Hoe | 15 % de chance de nuages de poison |
goblin_mace.lua | Mace | Frappes de météores depuis le ciel (clic droit) |
goblin_spear.lua | Spear | Rayon de feu le long de la visée (shift+clic droit) |
goblin_shield.lua | Shield | Dôme de verre lors d'un blocage (réactif) |
goblin_iron_sword.lua | Iron Sword | Anneau de ténèbres en chargement (shift+clic droit) |
Ces scripts fonctionnent avec des objets à l'apparence vanilla. Chacun n'a besoin que d'une config YML avec un champ material: -- aucun fichier .bbmodel n'est requis. Par exemple, pour activer le script de l'épée dorée, créez une config YML comme :
isEnabled: true
material: GOLDEN_SWORD
scripts:
- goblin_golden_sword.lua