Docs / API

API

Animorph expone dos APIs: una de servidor (IMorphAPI) para plugins Bukkit/Paper y mods Fabric server, y una de cliente (ClientMorphAPI) para mods Fabric client. Ambas incluyen un sistema de eventos para reaccionar a cambios de modelo, emotes y layers.

Agregar la dependencia

Agrega el repositorio de JitPack y la dependencia correspondiente:

JitPack version
groovy build.gradle — repositorios
repositories {
    mavenCentral()
    maven { url 'https://jitpack.io' }
}

Plugin Bukkit/Paper

groovy build.gradle
dependencies {
    compileOnly 'com.github.feeldev12.Animorph-API:server:{version}'
}

Mod de servidor (Fabric)

groovy build.gradle
dependencies {
    implementation 'com.github.feeldev12.Animorph-API:server:{version}'
}

Cliente (mod Fabric)

groovy build.gradle
dependencies {
    modImplementation 'com.github.feeldev12.Animorph-API:client:{version}'
}

API de Servidor (IMorphAPI)

IMorphAPI<P> es la interfaz principal para controlar Animorph desde el servidor. Funciona tanto en plugins Bukkit/Paper como en mods Fabric server.

java
import me.feeldev.animorph.api.AnimorphProvider;
import me.feeldev.animorph.api.IMorphAPI;

// Verificar disponibilidad
if (AnimorphProvider.isAvailable()) {
    IMorphAPI<Player> api = AnimorphProvider.getApi();
    // usar la API...
}
Nota
El tipo genérico P es Player en Bukkit/Paper o ServerPlayerEntity en Fabric server. Todos los métodos que envían datos aceptan viewers opcionales al final: si se omiten, se envía al jugador y a todos sus trackers.

Modelos

java
IMorphAPI<Player> api = AnimorphProvider.getApi();

// Aplicar un modelo al jugador
api.updateModel(player, "skeleton", false);

// Aplicar modelo forzando re-envío de datos
api.updateModel(player, "skeleton", true);

// Quitar el morph
api.clearModel(player);

// Re-enviar el modelo actual (sincronización con nuevos viewers)
api.updateModelPersistent(player);

// Re-enviar forzando datos completos
api.updateModelPersistent(player, true);

// Consultar el modelo activo
Optional<? extends IModelData> model = api.getModel(player);

// Obtener un modelo registrado por ID
Optional<? extends IModelData> modelo = api.getModel("skeleton");

// Listar todos los modelos registrados
Map<String, ? extends IModelData> modelos = api.registeredModels();

Registrar modelos por código

Si tu mod o plugin trae sus propios modelos (geometría, animación, textura) empaquetados dentro de su propio JAR, puedes registrarlos programáticamente en vez de pedirle al administrador del servidor que copie archivos sueltos a plugins/Animorph/models/. Extiende AnimorphModelDefinition — el mismo patrón que usa GeckoLib para sus GeoModel — y registra una instancia.

java Caso simple: apuntar a tus assets empaquetados
public class FoxModel extends AnimorphModelDefinition {

    @Override
    public String getModelId() { return "fox"; }

    @Override
    public String getDisplayName() { return "&6Fox"; }

    @Override
    protected String modelResourcePath() { return "/assets/mymod/fox.geo.json"; }

    @Override
    protected String animationResourcePath() { return "/assets/mymod/fox.animation.json"; }

    @Override
    protected String textureResourcePath() { return "/assets/mymod/fox.png"; }
}

boolean registered = api.registerModel(new FoxModel());
if (!registered) {
    // Ya existe un modelo en disco con ese ID — el registro fue rechazado
}
Consejo
No hace falta pasarle tu clase a Animorph para que sepa de dónde leer los archivos — como getModelId() y compañía corren dentro de tu propia clase, Animorph ya usa el classloader correcto solo. Las rutas son relativas al classpath de tu JAR, igual que getClass().getResourceAsStream(...).

Contenido generado en runtime

Si no quieres leer un archivo — por ejemplo, generas la textura o la geometría por código — sobrescribe los métodos get*Content() / getTextureBytes() directamente en vez de los *ResourcePath():

java Modelo con contenido generado
public class ProceduralModel extends AnimorphModelDefinition {

    @Override
    public String getModelId() { return "procedural"; }

    @Override
    public byte[] getTextureBytes() { return generateTexture(); }

    @Override
    public String getModelContent() { return generateGeometry(); }
}

Layers

Sobrescribe getLayers(). Para leer la textura de un layer, usa el método heredado readBytes(path):

java Modelo con un layer
@Override
public List<AnimorphLayerDefinition> getLayers() {
    return List.of(
        AnimorphLayerDefinition.texture("scarf", readBytes("/assets/mymod/scarf.png"))
            .defaultEnabled(true)
    );
}

Propiedades (properties:)

Sobrescribe getAnimationControllers(), isLayer(), getRenderType(), isHideNametag(), getFirstPersonProperty(), getTextPlaceholders(), getHitboxContents(), getEquipmentContents() y getItemEquipmentContents() — cada una refleja el mismo campo del bloque properties: de un model.yml normal (ver Modelos → Referencia de campos para el detalle completo de cada campo). Ninguna es obligatoria: si no sobrescribes nada, el modelo se comporta igual que un model.yml sin bloque properties:.

java Controlador de animación nativo + render type
@Override
public List<AnimorphControllerDefinition> getAnimationControllers() {
    return List.of(
        AnimorphControllerDefinition.nativeController("idle").transitionTime(5)
    );
}

@Override
public String getRenderType() { return "entity_translucent"; }
java Controlador por contenido JSON + primera persona
@Override
public List<AnimorphControllerDefinition> getAnimationControllers() {
    return List.of(
        AnimorphControllerDefinition.content("walk", readText("/assets/mymod/walk.controller.json"))
            .animationTransition("walk_anim", 3)
    );
}

@Override
public IFirstPersonProperty getFirstPersonProperty() {
    return IFirstPersonProperty.builder().showModel(true).build();
}

Quitar un modelo registrado

java
api.unregisterModel("fox");
Los modelos en disco siempre ganan
Si el ID que eliges ya lo usa un modelo cargado desde plugins/Animorph/models/ (un archivo .yml normal), registerModel devuelve false y no hace nada — el modelo en disco tiene prioridad. Si dos mods/plugins distintos registran el mismo ID por código, el último en registrarse gana.
Nota
Los modelos registrados por código sobreviven a /animorph reload — no hace falta volver a llamar registerModel después de un reload.

Bundles automáticos (sin código)

Si no quieres escribir ningún código, Animorph puede detectar y registrar modelos automáticamente desde dentro del JAR de tu propio mod o plugin, con solo seguir una estructura de carpetas específica. Funciona igual en Bukkit/Paper, Fabric, Forge y NeoForge — Animorph escanea todos los mods/plugins cargados al iniciar y busca esta estructura dentro de cada uno. Está separada por tipo de asset, igual que la convención de disco (carpetas hermanas models/, animations/, textures/):

plaintext Estructura esperada dentro de tu JAR
animorph/
  models/
    <id>.yml              (obligatorio — mismo esquema que un model.yml normal en disco)
    <id>.geo.json          (geometría por defecto; auto-detectada por nombre, o vía model.default en el yml)
    <id>_slim.geo.json     (opcional; geometría slim, o vía model.slim en el yml)
  animations/
    <id>.animation.json    (referenciado por la clave `animation:` del yml)
  textures/
    <id>.png               (referenciado por la clave `texture:` del yml)
    <id>.png.mcmeta         (opcional, textura animada)

No existe una sub-carpeta anidada layers/<layer_id>/. Un layer type: model (referenciado desde la sección layers: de un modelo) es arquitectónicamente idéntico a cualquier otro modelo — es simplemente otro .yml hermano en models/ con properties.is_layer: true. Todos los animorph/models/*.yml de una misma fuente se descubren en un único escaneo plano, se ordenan para que las entradas is_layer: true se registren primero (igual que el orden de carga en disco), y luego se parsean en ese orden. Un layer type: texture no es un bundle aparte — es solo una referencia texture: resuelta contra la carpeta compartida animorph/textures/, igual que la textura propia de un modelo de nivel superior.

<id>.yml usa el mismo formato que un YAML de modelo normal (ver Modelos) — display_name, properties, etc. — pero vive empaquetado en tu JAR en vez de en la carpeta de Animorph.

Los modelos en disco siempre ganan
Igual que con el registro por código: si el <id> de tu bundle coincide con un modelo YAML normal en plugins/Animorph/models/, el modelo en disco gana y tu bundle se ignora (queda un warning en el log). Esto le da al administrador del servidor la última palabra para sobrescribir cualquier modelo que traiga un mod/plugin.
Consejo
Esta es la opción más simple si solo quieres distribuir contenido — no necesitas tocar la API de Java para nada. Usa registro por código en cambio si necesitas generar el modelo dinámicamente, cargarlo condicionalmente, o quitarlo en tiempo de ejecución.

Emotes

Los emotes pueden reproducirse a nivel de modelo o de layers específicos.

java
// Reproducir un emote en el modelo
api.playEmote(player, "greet:wave", null);

// Reproducir un emote en layers específicos
api.playEmote(player, "dance:spin", Set.of("pompompurin"));

// Reproducir emote desde un tick específico (sincronización)
api.playEmote(player, "dance:spin", 40.0, null);

// Reproducir emote en layers desde un tick específico
api.playEmote(player, "dance:spin", 40.0, Set.of("pompompurin"));

// Detener todos los emotes (modelo + layers)
api.clearEmote(player);

// Detener emotes solo en layers específicos
api.clearEmote(player, Set.of("pompompurin"));

// Re-enviar emote de modelo (sincronización)
api.playEmote(player);

// Re-enviar emote de un tipo específico
api.playEmote(player, EmoteType.MODEL);
api.playEmote(player, EmoteType.LAYER);

// Consultar emote activo en un layer específico
String layerEmote = api.getLayerEmoteId(player, "pompompurin");

// Consultar animaciones registradas
Optional<? extends IEmoteData> emote = api.getAnimation("greet");
Map<String, ? extends IEmoteData> emotes = api.registeredAnimations();

Layers

java
// Activar un layer
api.applyLayer(player, "pompompurin", true);

// Desactivar un layer
api.applyLayer(player, "pompompurin", false);

// Activar layer en un modelo específico
api.applyLayer(player, "player", "pompompurin", true);

// Re-enviar todos los layers (sincronización)
api.updateLayers(player);

applyLayerColor

Sobrescribe el tinte ARGB de un layer activo en runtime. color es un int ARGB (por ejemplo 0xFFFF5500 para naranja). null revierte al color del YAML del modelo.

java
// Poner layer con tinte rojo semitransparente
api.applyLayerColor(player, "mi_accesorio", 0x88FF0000);

// Revertir al color del YAML
api.applyLayerColor(player, "mi_accesorio", null);

Texto

java
// Actualizar un placeholder
api.updateTextPlaceholder(player, "{name}", "Animorph Pro");

// Actualizar un placeholder solo para viewers específicos
api.updateTextPlaceholder(player, "{name}", "Animorph Pro", viewer1, viewer2);

// Re-enviar todos los placeholders
api.updateTextPlaceholder(player);

Primera persona

Controla la configuración de primera persona de un jugador en tiempo real, sin modificar el YAML del modelo. Las propiedades se aplican por jugador y sobrescriben la configuración del modelo.

java
import me.feeldev.animorph.api.IFirstPersonProperty;

// Crear propiedades de primera persona con el Builder
IFirstPersonProperty property = IFirstPersonProperty.builder()
    .showModel(true)
    .modelOffset(0, 1.5, 0)
    .customArmsShow(true)
    .customArmsBothHands(true)
    .customArmsRenderItems(true)
    .showEquipment(false)
    .build();

// Aplicar al jugador
api.updateFirstPersonProperty(player, property);

// Consultar las propiedades actuales
Optional<IFirstPersonProperty> fp = api.getFirstPersonProperty(player);

// Restaurar al valor por defecto del modelo
api.clearFirstPersonProperty(player);
Nota
Las propiedades de primera persona se envían solo al jugador afectado, ya que solo impactan su propia vista.

Referencia del Builder

MétodoTipoDescripción
showModel(bool)booleanMuestra el cuerpo completo del modelo en primera persona.
modelOffset(x, y, z)doubleDesplazamiento del modelo respecto a la cámara.
customArmsShow(bool)booleanHabilita los brazos del modelo en lugar de los vanilla.
customArmsBothHands(bool)booleanRenderiza ambas manos (derecha e izquierda).
customArmsRenderItems(bool)booleanRenderiza ítems a través de las manos del modelo.
showEquipment(bool)booleanMuestra la armadura en primera persona.

Estado del jugador (servidor)

java
// ID del modelo actual ("empty" si no tiene)
String modelId = api.getModelId(player);

// ¿Tiene un modelo aplicado?
boolean hasMorph = api.hasModel(player);

// ID del emote actual ("empty" si no tiene)
String emoteId = api.getEmoteId(player);

// ¿Está reproduciendo un emote?
boolean isEmoting = api.isEmoting(player);

// Emote en un layer específico
String layerEmote = api.getLayerEmoteId(player, "pompompurin");

// ¿Un layer está activo?
boolean active = api.isLayerActive(player, "pompompurin");

// Todos los layers y su estado
Map<String, Boolean> layers = api.getLayerStates(player);

// Todos los placeholders y sus valores
Map<String, String> texts = api.getTextPlaceholders(player);

// Propiedades de primera persona
Optional<IFirstPersonProperty> fp = api.getFirstPersonProperty(player);

setHideNametag

Sobrescribe en runtime la visibilidad del nametag del jugador, independientemente del campo hide_nametag del YAML. Se limpia automáticamente al aplicar un nuevo modelo.

setModelDisplayName

Sobrescribe el nombre visible del modelo en runtime: nametag 3D, tab list, mensajes de muerte. null revierte al display_name del YAML (o al nombre real del jugador si no tiene).

java
// Ocultar nametag
api.setHideNametag(player, true);

// Cambiar nombre visible
api.setModelDisplayName(player, "§6Héroe");

// Revertir
api.setModelDisplayName(player, null);

Sistema de Eventos

Animorph incluye un AnimorphEventBus ligero y thread-safe. Los eventos de servidor se disparan antes de aplicar el cambio y son cancelables. Los eventos de cliente se disparan después de aplicar el cambio y no son cancelables.

java
AnimorphEventBus bus = api.getEventBus();

// Registrar un listener
bus.register(ModelUpdateEvent.class, event -> {
    Player player = event.getPlayer();
    String newModel = event.getModelId();
    String oldModel = event.getPreviousModelId();

    // Cancelar si es un modelo prohibido
    if (newModel.equals("banned_model")) {
        event.setCancelled(true);
    }
});

// Desregistrar un listener
bus.unregister(ModelUpdateEvent.class, myListener);

Eventos de servidor

Todos los eventos de servidor extienden AnimorphEvent<P> y los cancelables implementan Cancellable.

EventoCancelableDescripciónMétodos
ModelUpdateEvent Se aplica o quita un modelo. getModelId(), getPreviousModelId(), isClearing()
EmotePlayEvent Se reproduce un emote. getEmoteId(), getPreviousEmoteId(), getLayerIds(), getStartTick(), isLayerEmote()
EmoteStopEvent Se detiene un emote. getEmoteId(), getLayerIds(), isLayerEmote()
LayerUpdateEvent Se activa o desactiva un layer. getLayerId(), getState()
FirstPersonPropertyUpdateEvent Se cambian las propiedades de primera persona. getProperty()
AnimorphReloadEvent No Se ejecuta /animorph reload.
java Ejemplo: impedir emotes en combate
api.getEventBus().register(EmotePlayEvent.class, event -> {
    Player player = event.getPlayer();
    if (isInCombat(player)) {
        event.setCancelled(true);
        player.sendMessage("No puedes usar emotes en combate.");
    }
});

Eventos de cliente

Los eventos de cliente se registran en ClientMorphAPI.getEventBus(). Se disparan después de que el cambio ya se aplicó.

EventoDescripción
ClientModelUpdateEventEl modelo de un jugador cambió.
ClientEmotePlayEventSe inició un emote.
ClientEmoteStopEventSe detuvo un emote.
ClientLayerUpdateEventUn layer cambió de estado.
ClientFirstPersonPropertyUpdateEventCambiaron las propiedades de primera persona.
java Ejemplo: reaccionar a cambio de modelo en cliente
import me.feeldev.animorph.client.api.ClientMorphAPI;
import me.feeldev.animorph.client.api.event.ClientModelUpdateEvent;

ClientMorphAPI.getEventBus().register(ClientModelUpdateEvent.class, event -> {
    UUID playerId = event.getPlayerId();
    String modelId = event.getModelId();
    // Actualizar UI, efectos visuales, etc.
});

Eventos de render

Los eventos de render se disparan cada frame, dentro del render pass del morph. Se registran en ClientMorphAPI.getEventBus() igual que el resto de eventos de cliente, pero están diseñados para integrarse con el pipeline gráfico en lugar de reaccionar a cambios de estado.

Son útiles para mods que normalmente modifican el renderer del jugador vanilla mediante mixins — con Animorph esos mixins no tienen efecto porque el renderer está reemplazado, y estos eventos son la alternativa sin mixins.

EventoCuándo se disparaModificableCancelable
PlayerMorphPreRenderEvent Antes de actuallyRender — el modelo aún no se dibujó. PoseStack, bones Sí — cancela todo el render del frame
PlayerMorphPostRenderEvent Después de todas las render layers — geometría ya dibujada. Renderizar encima No
PlayerMorphRenderColorEvent Al resolver el color ARGB del modelo (una vez por frame por jugador). setColor(Color) No
PlayerMorphNametagRenderEvent Justo antes de dibujar el nametag del jugador morfeado. PoseStack, setLabel(Component) No
Nota
Cuando PlayerMorphPreRenderEvent se cancela, se omiten actuallyRender, todas las render layers y PlayerMorphPostRenderEvent. El renderer vanilla no se restaura: el jugador quedará invisible ese frame.
java Ejemplo: pre-render — aplicar transformación al modelo
import me.feeldev.animorph.client.api.ClientMorphAPI;
import me.feeldev.animorph.client.api.event.PlayerMorphPreRenderEvent;

ClientMorphAPI.getEventBus().register(PlayerMorphPreRenderEvent.class, event -> {
    event.getPoseStack().pushPose();
    event.getPoseStack().scale(1.2f, 1.2f, 1.2f);
    // Renderizar geometría detrás del modelo si es necesario
    event.getPoseStack().popPose();
});
java Ejemplo: pre-render — ocultar el modelo completo
ClientMorphAPI.getEventBus().register(PlayerMorphPreRenderEvent.class, event -> {
    if (shouldHideForThisPlayer(event.getPlayer())) {
        event.setCancelled(true);
    }
});
java Ejemplo: pre-render — ocultar un hueso específico
ClientMorphAPI.getEventBus().register(PlayerMorphPreRenderEvent.class, event -> {
    // Ocultar un hueso antes de que se renderice el modelo
    event.getModel().getBone("left_wing").ifPresent(bone -> bone.setHidden(true));
});
java Ejemplo: post-render — renderizar algo encima del morph
import me.feeldev.animorph.client.api.event.PlayerMorphPostRenderEvent;
import net.minecraft.client.renderer.RenderType;

ClientMorphAPI.getEventBus().register(PlayerMorphPostRenderEvent.class, event -> {
    VertexConsumer buffer = event.getBufferSource().getBuffer(RenderType.lines());
    // Dibujar un bounding box, outline, indicador de partículas, etc.
});
java Ejemplo: color — hacer el morph semitransparente
import me.feeldev.animorph.client.api.event.PlayerMorphRenderColorEvent;
import software.bernie.geckolib.util.Color;

ClientMorphAPI.getEventBus().register(PlayerMorphRenderColorEvent.class, event -> {
    Color c = event.getColor();
    // Reducir el alpha al 50%
    event.setColor(Color.ofARGB(c.getAlpha() / 2, c.getRed(), c.getGreen(), c.getBlue()));
});
java Ejemplo: color — tinte de daño custom
ClientMorphAPI.getEventBus().register(PlayerMorphRenderColorEvent.class, event -> {
    if (event.getPlayer().hurtTime > 0) {
        // Flash rojo cuando recibe daño
        event.setColor(Color.ofARGB(255, 255, 80, 80));
    }
});
java Ejemplo: nametag — agregar un ícono sobre el nombre
import me.feeldev.animorph.client.api.event.PlayerMorphNametagRenderEvent;

ClientMorphAPI.getEventBus().register(PlayerMorphNametagRenderEvent.class, event -> {
    event.getPoseStack().pushPose();
    event.getPoseStack().translate(0.0, -0.25, 0.0);
    // Renderizar un ícono/badge usando event.getBufferSource()
    event.getPoseStack().popPose();
});
java Ejemplo: nametag — reemplazar el texto
ClientMorphAPI.getEventBus().register(PlayerMorphNametagRenderEvent.class, event -> {
    if (isVip(event.getPlayer())) {
        event.setLabel(Component.literal("★ ").append(event.getLabel()));
    }
});

Campos disponibles en los eventos de render

MétodoDisponible enDescripción
getPlayer()TodosEl jugador cuyo morph se está renderizando.
getPoseStack()Pre / Post / NametagPoseStack activo del frame, en coordenadas del jugador (pies) o del anchor del nametag.
getModel()Pre / PostModelo GeckoLib ya horneado — acceso a huesos vía getBone(name).
getBufferSource()Pre / Post / NametagBuffer source para dibujar geometría adicional.
getPartialTick()TodosFracción del tick para interpolación.
getPackedLight()TodosValor de luz empaquetado del bloque.
getPackedOverlay()Pre / PostOverlay empaquetado (daño, flash, etc.).
isCancelled() / setCancelled(bool)PreCancela todo el render del morph para este frame.
getColor() / setColor(Color)ColorColor ARGB del modelo. Se acumula en orden de registro.
getLabel() / setLabel(Component)NametagTexto del nametag ya resuelto (prefijo/sufijo de equipo, etc). Reemplazable.

API de Cliente (ClientMorphAPI)

ClientMorphAPI permite extender Animorph desde el lado del cliente: registrar controladores de animación custom, agregar queries Molang personalizadas, consultar el estado de los jugadores y escuchar eventos.

Solo disponible en mods Fabric client.

java
import me.feeldev.animorph.client.api.ClientMorphAPI;

// Verificar disponibilidad
if (ClientMorphAPI.isAvailable()) {
    // usar la API...
}

Controladores de animación custom

Extiende AnimorphController para crear controladores de animación propios. Una vez registrados, los modelos pueden referenciarlos por su ID en animation_controllers.

java Crear un controlador
import me.feeldev.animorph.client.api.AnimorphController;
import me.feeldev.animorph.client.interfaces.IPlayerData;
import software.bernie.geckolib.animation.AnimationController;
import software.bernie.geckolib.animation.PlayState;
import software.bernie.geckolib.animation.RawAnimation;

public class WingsController extends AnimorphController {

    public WingsController() {
        // nombre del controlador, tiempo de transición en ticks
        super("wings", 5);
    }

    @Override
    protected AnimationController.AnimationStateHandler<IPlayerData>
    createAnimationStateHandler(IPlayerData entity) {
        return animationState -> {
            var player = entity.animorph$getPlayer();
            if (player.isFallFlying()) {
                return animationState.setAndContinue(
                    RawAnimation.begin().thenLoop("animation.wings_flap")
                );
            }
            return PlayState.STOP;
        };
    }

    @Override
    protected boolean interrupt(AnimationState<IPlayerData> animationState) {
        // Interrumpir si hay un emote activo, por ejemplo
        return false;
    }
}
java Registrar el controlador
import me.feeldev.animorph.client.api.ClientMorphAPI;

// En la inicialización de tu mod
ClientMorphAPI.registerController("wings", new WingsController());

// Verificar si un controlador existe
boolean exists = ClientMorphAPI.hasController("wings");

Después, referéncialo en el YAML del modelo:

yaml
properties:
  animation_controllers:
    idle:
    simple_pose:
    wings:     # tu controlador custom

Controladores de primera persona

Los controladores custom también pueden usarse para los brazos en primera persona. Se registran de la misma forma con ClientMorphAPI.registerController() y se referencian en fp_animation_controllers en el YAML del modelo.

Si no se especifica fp_animation_controllers, se usan los controladores por defecto: fp_arm_right, fp_arm_left y fp_arms.

yaml
properties:
  fp_animation_controllers:
    - fp_arm_right
    - fp_arm_left
    - fp_arms
    - my_custom_fp_controller   # tu controlador custom de primera persona

Variables y queries Molang custom

Puedes registrar variables Molang personalizadas y actualizarlas cada tick de animación. Esto permite crear condiciones custom en animation controllers de Blockbench.

java Registrar una variable Molang
import me.feeldev.animorph.client.api.ClientMorphAPI;
import software.bernie.geckolib.loading.math.MathParser;

// 1. Registrar la variable (una vez, durante la inicialización del mod)
ClientMorphAPI.registerMolangVariable("query.my_stamina");

// 2. Agregar un query que actualice la variable cada tick
ClientMorphAPI.addMolangQuery((animationState, animTime) -> {
    var playerData = animationState.getAnimatable();
    var player = playerData.animorph$getPlayer();

    // Calcular el valor que quieres exponer
    double stamina = getStamina(player); // tu lógica

    // Actualizar la variable Molang
    MathParser.setVariable("query.my_stamina", () -> stamina);
});

Después puedes usar query.my_stamina en las condiciones de transición de tus animation controllers de Blockbench.

Estado del jugador (cliente)

Consulta el estado de cualquier jugador visible desde el cliente:

java
import me.feeldev.animorph.client.api.ClientMorphAPI;
import java.util.UUID;

UUID playerId = player.getUuid();

// Obtener el modelo activo
Optional<String> modelId = ClientMorphAPI.getPlayerModelId(playerId);

// ¿Tiene un modelo aplicado?
boolean hasModel = ClientMorphAPI.hasModel(playerId);

// Obtener el emote activo
Optional<String> emoteId = ClientMorphAPI.getEmoteId(playerId);

// ¿Está reproduciendo un emote?
boolean isEmoting = ClientMorphAPI.isEmoting(playerId);

Huesos de modelo y layers (cliente)

Para integraciones de gameplay (por ejemplo, raycasts o partículas desde un punto del modelo), puedes consultar huesos del modelo principal o de un layer de tipo MODEL.

java
import me.feeldev.animorph.client.api.ClientMorphAPI;
import me.feeldev.animorph.client.api.BoneTransformSnapshot;
import software.bernie.geckolib.cache.object.GeoBone;

UUID playerId = player.getUuid();

// Hueso del modelo principal
Optional<GeoBone> headBone = ClientMorphAPI.getPlayerModelBone(playerId, "head");

// Hueso del modelo principal en contexto de primera persona
Optional<GeoBone> headBoneFp = ClientMorphAPI.getPlayerModelBone(playerId, "head", true);

// Hueso de un layer de modelo activo
Optional<GeoBone> muzzleBone = ClientMorphAPI.getPlayerLayerBone(playerId, "rifle_layer", "muzzle");

// Hueso de layer en contexto explícito (true = primera persona, false = tercera persona)
Optional<GeoBone> muzzleBoneFp = ClientMorphAPI.getPlayerLayerBone(playerId, "rifle_layer", "muzzle", true);

// Snapshot inmutable recomendado para gameplay (estable fuera del render pass)
Optional<BoneTransformSnapshot> muzzleSnapshot = ClientMorphAPI.getPlayerLayerBoneSnapshot(playerId, "rifle_layer", "muzzle", true);

if (muzzleBone.isPresent()) {
    GeoBone bone = muzzleBone.get();
    float x = bone.getPosX();
    float y = bone.getPosY();
    float z = bone.getPosZ();
    // Usa también rotación/escala si tu lógica lo necesita
}
Nota
Importante: getPlayerLayerBone solo funciona para layers de tipo MODEL. Los layers de textura no tienen esqueleto/huesos.
Nota
Para integraciones de gameplay en primera persona (por ejemplo, raycasts desde muzzle), usa la sobrecarga con firstPerson=true y preferentemente getPlayerLayerBoneSnapshot para obtener datos inmutables con menor riesgo de estado stale.

Primera persona (cliente)

Consulta las propiedades de primera persona desde el cliente:

java
import me.feeldev.animorph.client.api.ClientMorphAPI;
import me.feeldev.animorph.api.IFirstPersonProperty;

UUID playerId = player.getUuid();

// Obtener las propiedades de primera persona
Optional<IFirstPersonProperty> fp = ClientMorphAPI.getFirstPersonProperty(playerId);

fp.ifPresent(property -> {
    boolean showsModel = property.showModel();
    boolean showsCustomArms = property.customArms().show();
    boolean showsEquipment = property.showEquipment();

    // Acceder a las opciones del modelo
    double offsetY = property.modelOptions().offsetY();

    // Acceder a la configuración de brazos custom
    boolean bothHands = property.customArms().bothHands();
});

Armaduras en morphs (AnimorphArmorRenderer)

La API de Fabric ArmorRenderer recibe un BipedEntityModel contextModel que no existe en modelos GeckoLib. AnimorphArmorRenderer es una interfaz de extensión que puedes agregar junto a tu ArmorRenderer existente. Cuando Animorph la detecta, llama a renderOnMorph en lugar del método original, dándote acceso directo al IPlayerData del morph.

java Implementar AnimorphArmorRenderer
import me.feeldev.animorph.client.api.AnimorphArmorRenderer;
import me.feeldev.animorph.client.interfaces.IPlayerData;
import net.fabricmc.fabric.api.client.rendering.v1.ArmorRenderer;

public class MyCoolArmorRenderer implements ArmorRenderer, AnimorphArmorRenderer {

    // Fabric lo llama cuando el jugador NO tiene morph activo (fallback vanilla)
    @Override
    public void render(MatrixStack matrices, VertexConsumerProvider bufferSource,
                       ItemStack stack, LivingEntity entity, EquipmentSlot slot,
                       int light, BipedEntityModel<LivingEntity> contextModel) {
        // ... tu render vanilla/GeoArmorRenderer
    }

    // Animorph lo llama cuando el jugador SÍ tiene morph activo
    @Override
    public void renderOnMorph(MatrixStack matrices, VertexConsumerProvider bufferSource,
                               ItemStack stack, LivingEntity entity, EquipmentSlot slot,
                               int light, IPlayerData morphContext) {
        // IPlayerData da acceso al modelo GeckoLib, bones, animaciones, etc.
        BakedGeoModel bakedModel = morphContext.animorph$getModel().getBakedModel(morphContext);
        GeoBone rightArmBone = bakedModel.getBone("right_arm").orElse(null);
        // ... lógica de render usando los huesos del modelo morph
    }
}
java Registrar (una sola vez, en la inicialización del mod)
MyCoolArmorRenderer renderer = new MyCoolArmorRenderer();

// Se registra una sola vez en la API de Fabric — Animorph detecta la interfaz automáticamente
ArmorRenderer.register(renderer, Items.NETHERITE_HELMET, Items.NETHERITE_CHESTPLATE);
Nota
Solo necesitas registrar el renderer una vez con Fabric. Animorph detecta instanceof AnimorphArmorRenderer en tiempo de render y llama al método correcto según si el jugador tiene morph activo o no. No se necesita registro adicional con la API de Animorph.
Consejo
Si tu mod ya usa GeoArmorRenderer de GeckoLib, Animorph lo detecta directamente y no necesitas implementar AnimorphArmorRenderer. Esta interfaz es para renderers custom que necesitan contexto del morph.

Render layers custom (AnimorphRenderLayer)

Puedes registrar render layers que se ejecutan dentro del contexto del render pass del modelo morph. Son equivalentes a los GeoRenderLayer de GeckoLib pero registrables desde cualquier mod cliente, sin necesidad de modificar el renderer de Animorph.

java Registrar render layers
import me.feeldev.animorph.client.api.ClientMorphAPI;
import me.feeldev.animorph.client.api.AnimorphRenderLayer;
import me.feeldev.animorph.client.api.AnimorphRenderContext;

// En la inicialización del mod cliente

// Layer global — se ejecuta para cualquier jugador con morph
ClientMorphAPI.registerRenderLayer(context -> {
    MatrixStack matrices = context.matrices();
    IPlayerData animatable = context.animatable();
    VertexConsumerProvider bufferSource = context.bufferSource();
    int packedLight = context.packedLight();
    // Renderizar algo sobre todos los jugadores con morph
});

// Layer específico de modelo — solo se ejecuta cuando el jugador usa "mi_modelo"
ClientMorphAPI.registerRenderLayer("mi_modelo", context -> {
    // Renderizar algo exclusivo para el modelo "mi_modelo"
});

AnimorphRenderContext

MétodoTipoDescripción
matrices()MatrixStackPila de transformaciones del render pass actual.
animatable()IPlayerDataDatos del jugador con morph (acceso al modelo GeckoLib, player, bones, etc.).
bakedModel()BakedGeoModelEl modelo GeckoLib ya horneado del jugador.
renderType()RenderLayer (nullable)Tipo de render activo en este pass.
bufferSource()VertexConsumerProviderProveedor de vertex buffers para dibujar geometría.
partialTick()floatFracción del tick para interpolar animaciones.
packedLight()intValor de luz empaquetado del bloque.
packedOverlay()intValor de overlay (daño, flash, etc.).
Nota
Los render layers se registran una sola vez al inicio del mod. Una vez registrados, se ejecutan automáticamente en cada render pass del jugador con morph. El layer global corre primero, luego los específicos del modelo.
java Ejemplo: partículas desde un hueso del modelo
ClientMorphAPI.registerRenderLayer("mi_modelo", context -> {
    BakedGeoModel bakedModel = context.bakedModel();
    bakedModel.getBone("particle_origin").ifPresent(bone -> {
        float x = bone.getPosX();
        float y = bone.getPosY();
        float z = bone.getPosZ();
        // Spawnear partículas en la posición del hueso
    });
});

Eventos de render (resumen de uso)

A diferencia de los render layers, los eventos de render permiten modificar el estado del render (transformaciones, color, visibilidad de huesos) además de agregar geometría. Son la alternativa directa a mixins en PlayerRenderer para mods que necesitan integrarse con el pipeline sin parchear Animorph.

Ver la referencia completa en Eventos › Eventos de render.

java Resumen de registro
// Pre-render: modificar PoseStack, ocultar bones, cancelar el render
ClientMorphAPI.getEventBus().register(PlayerMorphPreRenderEvent.class, event -> { ... });

// Post-render: dibujar sobre el modelo ya renderizado
ClientMorphAPI.getEventBus().register(PlayerMorphPostRenderEvent.class, event -> { ... });

// Color: modificar el ARGB del modelo (transparencia, tinte, efectos)
ClientMorphAPI.getEventBus().register(PlayerMorphRenderColorEvent.class, event -> { ... });

// Nametag: modificar PoseStack o reemplazar el texto antes de dibujar el nametag
ClientMorphAPI.getEventBus().register(PlayerMorphNametagRenderEvent.class, event -> { ... });