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();

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);

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);

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.
});

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 software.bernie.geckolib.cache.object.GeoBone;

UUID playerId = player.getUuid();

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

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

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.

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();
});