Ir para o conteúdo

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

// Aplicar cooldown después de usar
setCooldown(player, this.id, 5000); // 5 segundos

4. Cleanup de Recursos

@Override
public void cleanup() {
    // Limpiar recursos cuando la interacción termina
    COMBO_DATA.clear();
    COOLDOWNS.clear();
}

Recursos Adicionales


¿Problemas? Consulta Troubleshooting