Sistema de Interacciones en Mods¶
Esta guía completa cubre el uso del sistema de interacciones de Hytale en tus mods. Aprenderás a crear interacciones personalizadas completas, registrarlas, definir efectos y animaciones, y crear interacciones para bloques y entidades custom.
¿Qué son las Interacciones?¶
Las interacciones son acciones que los jugadores pueden realizar con items, bloques, entidades y el mundo. Son el núcleo de la jugabilidad en Hytale.
Tipos de Interacciones¶
| Tipo | Descripción | Ejemplos |
|---|---|---|
| RightClick | Click derecho con item | Usar poción, lanzar hechizo |
| LeftClick | Click izquierdo | Atacar, romper bloque |
| Mine | Minar bloque | Extraer mineral |
| Place | Colocar bloque | Construir |
| UseEntity | Usar entidad | Hablar con NPC, comerciar |
| Equipped | Item equipado | Efectos pasivos |
| Wielding | Sosteniendo item | Animación idle |
Arquitectura del Sistema¶
graph TD
A[Player Action] --> B[InteractionType]
B --> C[InteractionContext]
C --> D[Interaction]
D --> E{firstRun?}
E -->|Yes| F[Execute Logic]
E -->|No| G[Continue/Update]
F --> H[Update State]
G --> H
H --> I[Send to Client]
I --> J[Visual Effects]
Crear Interacciones Custom¶
Estructura Base¶
Todas las interacciones custom heredan de Interaction o sus subclases.
package com.ejemplo.mimod.interactions;
import com.hypixel.hytale.protocol.InteractionState;
import com.hypixel.hytale.protocol.InteractionType;
import com.hypixel.hytale.server.core.entity.InteractionContext;
import com.hypixel.hytale.server.core.modules.interaction.interaction.CooldownHandler;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.SimpleInstantInteraction;
import javax.annotation.Nonnull;
/**
* Plantilla base para una interacción custom.
*/
public class MiInteraccion extends SimpleInstantInteraction {
public MiInteraccion() {
this.id = "mimod:mi_interaccion";
this.runTime = 1.0f; // Duración en segundos
}
@Override
protected void firstRun(@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
// Tu lógica aquí
// Marcar como terminado
context.getState().state = InteractionState.Finished;
}
}
Clases Base Disponibles¶
1. SimpleInstantInteraction¶
Para interacciones instantáneas que se ejecutan una vez.
public class TeleportInteraction extends SimpleInstantInteraction {
private static final float TELEPORT_DISTANCE = 10.0f;
public TeleportInteraction() {
this.id = "mimod:teleport";
this.runTime = 0.5f; // Animación breve
}
@Override
protected void firstRun(@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
var entityRef = context.getEntity();
var buffer = context.getCommandBuffer();
var entity = buffer.getEntity(entityRef);
if (!(entity instanceof Player)) {
context.getState().state = InteractionState.Failed;
return;
}
Player player = (Player) entity;
// Calcular nueva posición
Vector3d position = player.getPosition();
Vector3d direction = player.getLookDirection();
Vector3d newPosition = new Vector3d(
position.x + direction.x * TELEPORT_DISTANCE,
position.y,
position.z + direction.z * TELEPORT_DISTANCE
);
// Teletransportar
player.setPosition(newPosition);
// Efectos
player.sendMessage("§b¡Teletransportado!");
context.getState().state = InteractionState.Finished;
}
}
2. SimpleInteraction¶
Para interacciones con lógica continua (tick).
public class ChargingInteraction extends SimpleInteraction {
private static final float CHARGE_TIME = 3.0f;
private static final int FULL_CHARGE_DAMAGE = 50;
public ChargingInteraction() {
this.id = "mimod:charging_attack";
this.runTime = CHARGE_TIME;
}
@Override
protected void tick0(boolean firstRun, float time,
@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
if (firstRun) {
// Inicio de la carga
sendMessage(context, "§eComenzando carga...");
}
// Calcular progreso de carga
float progress = time / CHARGE_TIME;
int damage = (int) (FULL_CHARGE_DAMAGE * progress);
// Actualizar indicador visual
updateChargeIndicator(context, progress);
// Cuando se completa
if (time >= CHARGE_TIME) {
executeChargedAttack(context, FULL_CHARGE_DAMAGE);
context.getState().state = InteractionState.Finished;
}
}
@Override
protected void simulateTick0(boolean firstRun, float time,
@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
// Simulación cliente-side
updateChargeIndicator(context, time / CHARGE_TIME);
}
private void updateChargeIndicator(InteractionContext context, float progress) {
// Actualizar UI, partículas, etc.
int bars = (int) (progress * 10);
String indicator = "§e" + "█".repeat(bars) + "§7" + "░".repeat(10 - bars);
sendActionBar(context, indicator + " §b" + (int)(progress * 100) + "%");
}
private void executeChargedAttack(InteractionContext context, int damage) {
// Ejecutar ataque con daño calculado
sendMessage(context, "§a¡Ataque cargado! Daño: " + damage);
}
}
InteractionContext¶
El contexto proporciona acceso a todos los datos de la interacción.
Propiedades Principales¶
public void ejemploUsoContext(InteractionContext context) {
// Entidad que ejecuta la interacción
Ref<EntityStore> entityRef = context.getEntity();
var buffer = context.getCommandBuffer();
var entity = buffer.getEntity(entityRef);
// Item en mano
ItemStack heldItem = context.getHeldItem();
// Estado de la interacción
InteractionSyncData state = context.getState();
state.state = InteractionState.Finished;
state.progress = 1.0f;
// Cadena de interacciones
InteractionChain chain = context.getChain();
// Metadata temporal de la interacción
var metaStore = context.getInstanceStore();
// Entidad objetivo (si aplica)
Ref<EntityStore> targetEntity = metaStore.getMetaObject(
Interaction.TARGET_ENTITY
);
// Bloque objetivo (si aplica)
BlockPosition targetBlock = metaStore.getMetaObject(
Interaction.TARGET_BLOCK
);
// Ubicación de impacto
Vector4d hitLocation = metaStore.getMetaObject(
Interaction.HIT_LOCATION
);
}
Efectos e Interacciones¶
Definir Efectos¶
Los efectos controlan aspectos visuales y auditivos de la interacción.
public class EffectfulInteraction extends SimpleInstantInteraction {
public EffectfulInteraction() {
this.id = "mimod:explosion_spell";
this.runTime = 1.0f;
// Configurar efectos
this.effects = new InteractionEffects();
// Animación de item
this.effects.setItemAnimationId("attack");
// Partículas
this.effects.setParticleEffect("mimod:explosion_particles");
// Sonidos
this.effects.setSound("mimod:explosion_sound");
// Velocidad de movimiento durante interacción
this.horizontalSpeedMultiplier = 0.5f; // 50% velocidad
// Distancia de visualización
this.viewDistance = 50.0; // 50 bloques
}
@Override
protected void firstRun(@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
// Lógica de explosión
createExplosion(context);
context.getState().state = InteractionState.Finished;
}
private void createExplosion(InteractionContext context) {
// Crear explosión en la ubicación del jugador
var entityRef = context.getEntity();
var buffer = context.getCommandBuffer();
var entity = buffer.getEntity(entityRef);
if (entity instanceof Player) {
Player player = (Player) entity;
Vector3d position = player.getPosition();
// Aplicar daño en área
damageEntitiesInRadius(buffer, position, 5.0, 20);
}
}
private void damageEntitiesInRadius(CommandBuffer<EntityStore> buffer,
Vector3d center,
double radius,
int damage) {
// Implementar daño en área
// (Simplificado - usar sistema de colisiones en producción)
}
}
Interacciones con Bloques Custom¶
Definir en JSON¶
assets/blocks/interactive_block.json:
{
"id": "mimod:interactive_block",
"name": "Interactive Block",
"model": "mimod:models/interactive_block",
"texture": "mimod:textures/interactive_block",
"interactions": {
"RightClick": "mimod:activate_block",
"LeftClick": "mimod:deactivate_block",
"Mine": "mimod:mine_special_block"
},
"hardness": 3.0,
"resistance": 5.0
}
Implementar Interacciones¶
public class ActivateBlockInteraction extends SimpleInstantInteraction {
public ActivateBlockInteraction() {
this.id = "mimod:activate_block";
this.runTime = 0.5f;
}
@Override
protected void firstRun(@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
// Obtener bloque objetivo
BlockPosition blockPos = context.getInstanceStore().getMetaObject(
Interaction.TARGET_BLOCK
);
if (blockPos == null) {
context.getState().state = InteractionState.Failed;
return;
}
// Obtener jugador
var entityRef = context.getEntity();
var buffer = context.getCommandBuffer();
var entity = buffer.getEntity(entityRef);
if (!(entity instanceof Player)) {
context.getState().state = InteractionState.Failed;
return;
}
Player player = (Player) entity;
// Activar bloque
activateBlock(player, blockPos);
// Efectos visuales
spawnActivationParticles(blockPos);
playSound(blockPos, "mimod:block_activate");
player.sendMessage("§a¡Bloque activado!");
context.getState().state = InteractionState.Finished;
}
private void activateBlock(Player player, BlockPosition pos) {
// Lógica de activación
// - Cambiar estado del bloque
// - Activar mecanismo
// - Dar recompensa
// etc.
}
private void spawnActivationParticles(BlockPosition pos) {
// Crear partículas
Vector3d center = new Vector3d(
pos.x + 0.5,
pos.y + 0.5,
pos.z + 0.5
);
// Spawear partículas en espiral
for (int i = 0; i < 20; i++) {
double angle = i * Math.PI / 10;
double radius = 0.5;
Vector3d particlePos = new Vector3d(
center.x + Math.cos(angle) * radius,
center.y + (i * 0.1),
center.z + Math.sin(angle) * radius
);
// spawnParticle("mimod:magic_sparkle", particlePos);
}
}
private void playSound(BlockPosition pos, String soundId) {
// Reproducir sonido en la posición del bloque
Vector3d soundPos = new Vector3d(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5);
// playSoundAtPosition(soundId, soundPos, 1.0f, 1.0f);
}
}
Interacciones con Entidades Custom¶
Definir en JSON¶
assets/entities/custom_npc.json:
{
"id": "mimod:custom_npc",
"name": "Mysterious Merchant",
"type": "NPC",
"health": 100,
"model": "mimod:models/merchant",
"interactions": {
"RightClick": "mimod:talk_to_merchant",
"Sneak+RightClick": "mimod:trade_with_merchant",
"Attack": "mimod:npc_hurt_reaction"
},
"ai": "mimod:merchant_ai"
}
Implementar Interacciones¶
public class TalkToMerchantInteraction extends SimpleInstantInteraction {
private static final String[] DIALOGUES = {
"¡Bienvenido, viajero! Tengo mercancía única.",
"¿Buscas algo especial? Tengo lo que necesitas.",
"Mis precios son justos, te lo aseguro.",
"¡No encontrarás mejor calidad en ningún lado!"
};
public TalkToMerchantInteraction() {
this.id = "mimod:talk_to_merchant";
this.runTime = 0.1f;
}
@Override
protected void firstRun(@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
// Obtener jugador
var entityRef = context.getEntity();
var buffer = context.getCommandBuffer();
var entity = buffer.getEntity(entityRef);
if (!(entity instanceof Player)) {
context.getState().state = InteractionState.Failed;
return;
}
Player player = (Player) entity;
// Obtener NPC objetivo
Ref<EntityStore> targetRef = context.getInstanceStore().getMetaObject(
Interaction.TARGET_ENTITY
);
if (targetRef == null || !targetRef.isValid()) {
context.getState().state = InteractionState.Failed;
return;
}
// Diálogo aleatorio
String dialogue = DIALOGUES[(int)(Math.random() * DIALOGUES.length)];
// Mostrar diálogo
player.sendMessage("§e[Merchant]§f " + dialogue);
// Abrir GUI de comercio (opcional)
// openTradeGUI(player, targetRef);
context.getState().state = InteractionState.Finished;
}
}
public class TradeWithMerchantInteraction extends SimpleInstantInteraction {
public TradeWithMerchantInteraction() {
this.id = "mimod:trade_with_merchant";
this.runTime = 0.1f;
}
@Override
protected void firstRun(@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
var entityRef = context.getEntity();
var buffer = context.getCommandBuffer();
var entity = buffer.getEntity(entityRef);
if (!(entity instanceof Player)) {
context.getState().state = InteractionState.Failed;
return;
}
Player player = (Player) entity;
// Obtener NPC
Ref<EntityStore> merchantRef = context.getInstanceStore().getMetaObject(
Interaction.TARGET_ENTITY
);
if (merchantRef == null || !merchantRef.isValid()) {
context.getState().state = InteractionState.Failed;
return;
}
// Abrir interfaz de comercio
openMerchantGUI(player, merchantRef);
player.sendMessage("§a¡Abriendo tienda del mercader!");
context.getState().state = InteractionState.Finished;
}
private void openMerchantGUI(Player player, Ref<EntityStore> merchant) {
// Crear y mostrar GUI de comercio
// Esto requeriría integración con el sistema de UI de Hytale
// (Simplificado para el ejemplo)
List<TradeOffer> offers = List.of(
new TradeOffer("mimod:mystic_gem", 5, "hytale:gold_coin", 10),
new TradeOffer("mimod:magic_staff", 1, "hytale:gold_coin", 50),
new TradeOffer("hytale:health_potion", 3, "hytale:silver_coin", 20)
);
// Mostrar GUI
// player.openGUI(new MerchantGUI(offers));
}
private static class TradeOffer {
String sellItem;
int sellAmount;
String buyItem;
int buyAmount;
TradeOffer(String sellItem, int sellAmount, String buyItem, int buyAmount) {
this.sellItem = sellItem;
this.sellAmount = sellAmount;
this.buyItem = buyItem;
this.buyAmount = buyAmount;
}
}
}
Interacciones Avanzadas¶
Interacciones Encadenadas¶
Crear secuencias de interacciones.
public class ComboAttackInteraction extends SimpleInstantInteraction {
private static final String COMBO_KEY = "mimod:combo_count";
private static final long COMBO_TIMEOUT = 2000; // 2 segundos
public ComboAttackInteraction() {
this.id = "mimod:combo_attack";
this.runTime = 0.3f;
}
@Override
protected void firstRun(@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
var entityRef = context.getEntity();
var buffer = context.getCommandBuffer();
var entity = buffer.getEntity(entityRef);
if (!(entity instanceof Player)) {
context.getState().state = InteractionState.Failed;
return;
}
Player player = (Player) entity;
// Obtener combo actual
ComboData combo = getComboData(player);
// Verificar timeout
if (System.currentTimeMillis() - combo.lastHitTime > COMBO_TIMEOUT) {
combo.count = 0;
}
// Incrementar combo
combo.count++;
combo.lastHitTime = System.currentTimeMillis();
// Ejecutar ataque según combo
executeComboAttack(player, combo.count);
// Guardar combo
saveComboData(player, combo);
// Mostrar combo
player.sendMessage("§e⚔ Combo x" + combo.count + "!");
context.getState().state = InteractionState.Finished;
}
private void executeComboAttack(Player player, int comboLevel) {
int damage = 10 + (comboLevel * 5); // Daño escalado
// Ejecutar ataque
// dealDamageInFront(player, damage);
// Efectos especiales según nivel de combo
if (comboLevel >= 3) {
// Combo nivel 3+: Área de efecto
// createShockwave(player.getPosition(), 3.0, damage);
player.sendMessage("§6✦ ¡Shockwave!");
}
if (comboLevel >= 5) {
// Combo nivel 5+: Crítico
// applyCriticalEffect(player);
player.sendMessage("§c★ ¡CRÍTICO!");
}
}
// Sistema de combo por jugador
private static final Map<Player, ComboData> COMBO_DATA = new ConcurrentHashMap<>();
private ComboData getComboData(Player player) {
return COMBO_DATA.computeIfAbsent(player, p -> new ComboData());
}
private void saveComboData(Player player, ComboData data) {
COMBO_DATA.put(player, data);
}
private static class ComboData {
int count = 0;
long lastHitTime = 0;
}
}
Interacciones Condicionales¶
Ejecutar interacciones basadas en condiciones.
public class ConditionalSpellInteraction extends SimpleInstantInteraction {
private static final int MANA_COST = 30;
private static final int MIN_LEVEL = 5;
public ConditionalSpellInteraction() {
this.id = "mimod:conditional_spell";
this.runTime = 1.0f;
}
@Override
protected void firstRun(@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
var entityRef = context.getEntity();
var buffer = context.getCommandBuffer();
var entity = buffer.getEntity(entityRef);
if (!(entity instanceof Player)) {
context.getState().state = InteractionState.Failed;
return;
}
Player player = (Player) entity;
// Verificar condiciones
if (!checkConditions(player)) {
context.getState().state = InteractionState.Failed;
return;
}
// Ejecutar hechizo
castSpell(player);
context.getState().state = InteractionState.Finished;
}
private boolean checkConditions(Player player) {
// Condición 1: Nivel mínimo
int playerLevel = getPlayerLevel(player);
if (playerLevel < MIN_LEVEL) {
player.sendMessage("§c¡Necesitas nivel " + MIN_LEVEL + "!");
return false;
}
// Condición 2: Mana suficiente
int currentMana = getPlayerMana(player);
if (currentMana < MANA_COST) {
player.sendMessage("§c¡Mana insuficiente! (" +
currentMana + "/" + MANA_COST + ")");
return false;
}
// Condición 3: No en cooldown
if (isOnCooldown(player, this.id)) {
long remainingTime = getCooldownRemaining(player, this.id);
player.sendMessage("§c¡Cooldown! Espera " +
remainingTime / 1000 + "s");
return false;
}
// Condición 4: Ambiente correcto (ejemplo: no bajo el agua)
if (isUnderwater(player)) {
player.sendMessage("§c¡No puedes lanzar este hechizo bajo el agua!");
return false;
}
return true;
}
private void castSpell(Player player) {
// Consumir mana
consumeMana(player, MANA_COST);
// Aplicar cooldown
setCooldown(player, this.id, 10000); // 10 segundos
// Ejecutar hechizo
player.sendMessage("§b✦ ¡Hechizo lanzado!");
// Efectos
// ...
}
// Helpers (simplificados)
private int getPlayerLevel(Player player) { return 1; }
private int getPlayerMana(Player player) { return 100; }
private void consumeMana(Player player, int amount) { }
private boolean isOnCooldown(Player player, String id) { return false; }
private long getCooldownRemaining(Player player, String id) { return 0; }
private void setCooldown(Player player, String id, long ms) { }
private boolean isUnderwater(Player player) { return false; }
}
Registrar Interacciones en el AssetStore¶
Registro Básico¶
package com.ejemplo.mimod;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction;
import com.ejemplo.mimod.interactions.*;
import java.util.List;
public class MiMod {
public static void registerInteractions() {
var store = Interaction.getAssetStore();
// Cargar interacciones
store.loadAssets("mimod:mimod", List.of(
new TeleportInteraction(),
new ChargingInteraction(),
new ActivateBlockInteraction(),
new TalkToMerchantInteraction(),
new TradeWithMerchantInteraction(),
new ComboAttackInteraction(),
new ConditionalSpellInteraction()
));
System.out.println("[MiMod] Registered 7 interactions");
}
}
Registro Dinámico¶
public class DynamicInteractionRegistry {
private static final List<Interaction> INTERACTIONS = new ArrayList<>();
/**
* Registra una interacción.
*/
public static void register(Interaction interaction) {
INTERACTIONS.add(interaction);
System.out.println("[Registry] Registered: " + interaction.getId());
}
/**
* Carga todas las interacciones registradas.
*/
public static void loadAll() {
if (INTERACTIONS.isEmpty()) {
System.out.println("[Registry] No interactions to load");
return;
}
var store = Interaction.getAssetStore();
store.loadAssets("mimod:mimod", INTERACTIONS);
System.out.println("[Registry] Loaded " + INTERACTIONS.size() + " interactions");
}
/**
* API pública para que otros plugins registren interacciones.
*/
public static void registerExternal(String modId, Interaction interaction) {
register(interaction);
}
}
// Uso:
public class MiMod {
public static void initialize() {
// Registrar interacciones
DynamicInteractionRegistry.register(new TeleportInteraction());
DynamicInteractionRegistry.register(new ChargingInteraction());
// Cargar todas
DynamicInteractionRegistry.loadAll();
}
}
Debugging de Interacciones¶
Logging Detallado¶
public class DebuggableInteraction extends SimpleInstantInteraction {
private static final boolean DEBUG = Boolean.getBoolean("mimod.debug");
public DebuggableInteraction() {
this.id = "mimod:debuggable";
this.runTime = 1.0f;
}
@Override
protected void firstRun(@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
debug("=== Interaction Started ===");
debug("Type: " + type);
debug("Entity: " + context.getEntity());
debug("Held Item: " + context.getHeldItem());
// Lógica
debug("State: " + context.getState().state);
debug("=== Interaction Finished ===");
context.getState().state = InteractionState.Finished;
}
private void debug(String message) {
if (DEBUG) {
System.out.println("[Debug-" + this.id + "] " + message);
}
}
}
Mejores Prácticas¶
1. Validar Siempre¶
@Override
protected void firstRun(@Nonnull InteractionType type,
@Nonnull InteractionContext context,
@Nonnull CooldownHandler cooldownHandler) {
// Validar entidad
var entity = getValidPlayer(context);
if (entity == null) {
context.getState().state = InteractionState.Failed;
return;
}
// Tu lógica aquí
}
private Player getValidPlayer(InteractionContext context) {
var entityRef = context.getEntity();
if (!entityRef.isValid()) {
return null;
}
var buffer = context.getCommandBuffer();
var entity = buffer.getEntity(entityRef);
return entity instanceof Player ? (Player) entity : null;
}
2. Manejar Estados Correctamente¶
// ✅ BIEN
context.getState().state = InteractionState.Finished;
// ❌ MAL - No dejar estado sin definir
// (El estado por defecto podría causar problemas)
3. Usar Cooldowns¶
4. Cleanup de Recursos¶
@Override
public void cleanup() {
// Limpiar recursos cuando la interacción termina
COMBO_DATA.clear();
COOLDOWNS.clear();
}
Recursos Adicionales¶
- Creating a Mod: Tutorial completo de mods
- Meta System Usage: Datos persistentes
- API Reference: Documentación completa
¿Problemas? Consulta Troubleshooting