import { system, world, Player, EntityComponentTypes, EntityDamageCause, EquipmentSlot, Entity, Dimension } from "@minecraft/server";
import * as consts from "../constants.js";
import * as attackUtils from "../attackUtils.js";
import * as atk from "./attackClasses.js";
import * as anims from "./animationHandling.js";

// TODO: Ignore/Cancel player primary attacks and punches while casting an attack
// Can't do that since I can't cancel punches...


class ActionBarDisplayManager {
    /**
     * Creates a new ActionBarDisplayManager for the given player.
     * @param {Player} player The player to manage the action bar for
     */
    constructor(player) {
        this.player = player;
    }

    /**
     * Updates the action bar to display the cooldowns and progress bar for the equipped gauntlet.
     * @remarks
     * The cooldowns are displayed first, followed by the progress bar below them.
     * If no gauntlet is equipped, this function does nothing.
     */
    update() {
        const gauntlet = this.player?.gauntletManager?.equippedGauntlet;
        if (!gauntlet) return; // No gauntlet equipped, do nothing

        const cooldowns = this.formatCooldowns(gauntlet);
        const progressBar = this.constructProgressBar(gauntlet.activeAttack);
        const heatMeter = gauntlet.heatMeter !== undefined ? this.constructHeatMeter(gauntlet) : '';

        // Display cooldowns first, then the progress bar below
        const actionBarMessage = {
            rawtext: [
                { text: progressBar },
                { text: heatMeter },
                cooldowns,
            ]
        };

        this.player.onScreenDisplay.setActionBar(actionBarMessage);
    }

    /**
     * Clears the action bar display for the player.
     * @remarks
     * This function resets the action bar to an empty state,
     * effectively removing any displayed messages or icons.
     */
    clear() {
        this.player.onScreenDisplay.setActionBar('§');
    }

    /**
     * Determines whether to show "Aiming..." or the cast progress bar.
     * @param {GauntletAttack} activeAttack The currently active attack.
     * @returns {string} The display string for either aiming or progress.
     */
    constructProgressBar(activeAttack) {
        // if (!activeAttack) return this.getEmptyProgressBar();
        if (!activeAttack) return `\n`;

        if (activeAttack.isAiming && activeAttack?.chargeStages) {
            // Check if the attack has charges and is in a charging state (has a currentChargeStage > 0)
            return this.getChargeProgressBar(activeAttack);
        }

        // return this.getCastProgressBar(activeAttack);
        return `\n`;
    }

    /**
     * Constructs a visual representation of the charge progress for an active attack.
     * @param {GauntletAttack} activeAttack - The currently active attack with charge stages.
     * @returns {string} A formatted string representing the charge progress bar.
     */
    getChargeProgressBar(activeAttack) {
        let progressBar = " ";

        for (let i = 0; i < activeAttack.chargeStages.length; i++) {
            progressBar += (i < activeAttack.currentChargeStage ? `§a[]` : `§8[]`) + " ";
        }

        return progressBar + "\n";
    }

    /**
     * Formats the cooldown information for the player's equipped gauntlet.
     * @param {Gauntlet} gauntlet - The gauntlet object containing cooldown data.
     * @returns {RawtextObject} An object in rawtext format representing the cooldown status
     * of each attack, formatted with corresponding icons and remaining cooldown time.
     */
    formatCooldowns(gauntlet) {
        const cooldowns = {
            text: " ",
            rawtext: consts.ATTACK_MAPPINGS.map(({ icon, attackKey }) => {
                const attackProperty = gauntlet[attackKey]; // Access the attack property

                if (!attackProperty) return { text: `§m[ X ]§r` }; // In case the attack is missing or undefined

                const cooldownExpiry = attackProperty.cooldownExpiry || 0; // The time when the cooldown will end
                const remainingTicks = Math.max(0, cooldownExpiry - system.currentTick); // Remaining ticks until cooldown expires

                // Convert ticks to seconds (1 tick = 1/20 second)
                const remainingSeconds = remainingTicks / 20;

                // Determine display text (show emoji or "Ready" message)
                // const charges = attackProperty.maxCharges > 1 && remainingSeconds === 0 ? `${attackProperty.remainingCharges}` : icon;
                const charges = attackProperty.maxCharges > 1 && remainingSeconds === 0 ? `${attackProperty.remainingCharges}` : '-';
                const cooldown = remainingSeconds > 0 ? `§c${remainingSeconds.toFixed(1)}` : ` ${charges} `;

                const colorOut = attackProperty.isAiming > 0 ? "§6" : "§8";
                const colorIN = remainingSeconds > 0 ? "§c" : attackProperty.isAiming > 0 ? "§6" : "§a";

                // Create a formatted message with the attack icon and its cooldown state
                return { text: `${colorOut}[${colorIN}${cooldown}${colorOut}] ` };
            })
        };

        return cooldowns;
    }

    constructHeatMeter(gauntlet) {
        const heatBarLength = 41;
        const heatPercentage = gauntlet.heatMeter / gauntlet.maxHeat;
        const filledLength = Math.ceil(heatBarLength * heatPercentage);

        const activationThreshold = gauntlet.upgradeStages.primary >= 3 ? 0.5 : 0.75;

        let progressBar = "";

        for (let i = 0; i < heatBarLength; i++) {
            progressBar += (i < filledLength ? heatPercentage >= activationThreshold ? `§6|` : `§c|` : `§8|`);
        }

        return progressBar + "\n";
    }

    /**
     * Generates the progress bar for the current attack based on the elapsed time vs the cast time.
     * @param {GauntletAttack} activeAttack The current attack in progress.
     * @returns {string} The progress bar string.
     */
    getCastProgressBar(activeAttack) {
        if (!activeAttack || !activeAttack.isCasting) return this.getEmptyProgressBar();

        const elapsedTicks = system.currentTick - activeAttack.attackCastTick; // Fixed reference
        const progress = Math.min(elapsedTicks / activeAttack.castTime, 1); // Ensure max 100%
        const filledSegments = Math.ceil(progress * consts.PROGRESSBAR_LENGTH);
        const unfilledSegments = consts.PROGRESSBAR_LENGTH - filledSegments;

        // Construct the progress bar
        return `§6${consts.PROGRESSBAR_ICON_STAGE}${"§6" + consts.PROGRESSBAR_ICON_NORMAL.repeat(filledSegments)}§8${consts.PROGRESSBAR_ICON_NORMAL.repeat(unfilledSegments)}§6${consts.PROGRESSBAR_ICON_STAGE}`;
    } // DEPRECATED

    /**
     * Returns an empty progress bar string (all segments unfilled)
     * @returns {string}
     */
    getEmptyProgressBar() {
        return `§6${consts.PROGRESSBAR_ICON_STAGE}§8${consts.PROGRESSBAR_ICON_NORMAL.repeat(consts.PROGRESSBAR_LENGTH)}§6${consts.PROGRESSBAR_ICON_STAGE}`;
    } // DEPRECATED

}

export class GauntletManager {
    /**
     * Creates a new gauntlet manager for the given player.
     * @param {Player} player The player to manage gauntlets for
     * @constructor
     */
    constructor(player) {
        this.player = player;
        this.gauntlets = new Map();
        this.equippedGauntlet = null;
        this.isSneaking = false;
        this.LCDetectorActive = false;
        this.LCDetectorEntity = undefined;

        this.registerGauntlet("oct_eg:pyro_gauntlet", PyroGauntlet);
        this.registerGauntlet("oct_eg:hydro_gauntlet", HydroGauntlet);
        this.registerGauntlet("oct_eg:anemo_gauntlet", AnemoGauntlet);
        this.registerGauntlet("oct_eg:geo_gauntlet", GeoGauntlet);
        this.registerGauntlet("oct_eg:electro_gauntlet", ElectroGauntlet);
        this.registerGauntlet("oct_eg:necro_gauntlet", NecroGauntlet);
        this.registerGauntlet("oct_eg:ender_gauntlet", EnderGauntlet);

        this.actionBarDisplayManager = new ActionBarDisplayManager(this.player);

        // Event listeners
        this.jumpListener = null;
        this.sneakListener = null;
        this.itemUseListener = null;
        this.itemStopUseListener = null;
        this.gotHitListener = null;
        this.LCDitectorListener = null;
        this.primaryHitEntityListener = null;
        this.primaryHiBlockListener = null;
        this.killedEntityListener = null;

        this.subscribeEventListeners();
    }

    /**
     * Registers a new gauntlet class to be used when a player equips a weapon with the given typeId.
     * @param {string} itemTypeId The typeId of the gauntlet item
     * @param {Gauntlet} GauntletClass The Gauntlet class to be used for this tag
     */
    registerGauntlet(itemTypeId, GauntletClass) {
        this.gauntlets.set(itemTypeId, new GauntletClass(this.player));
    }

    /**
     * Checks the currently equipped gauntlet on the player and updates it if necessary.
     * If a new gauntlet is equipped, triggers the onEquip event; if a gauntlet is unequipped,
     * triggers the onUnequip event. The function ensures the equipped gauntlet in the player's main hand is synchronized
     * with the gauntlet manager's state.
     */
    checkEquippedGauntlet() {
        const equippableComp = this.player?.getComponent(EntityComponentTypes.Equippable);
        const equipment = equippableComp?.getEquipment(EquipmentSlot.Mainhand);

        // Assign new gauntlet (sets to 'null' if not a gauntlet)
        const equippedItem = equipment
            ? this.gauntlets.get(equipment?.getTags().find(typeId => this.gauntlets.has(typeId))) || null
            : null;

        if (equippedItem === this.equippedGauntlet) return;

        if (this.equippedGauntlet instanceof Gauntlet) {
            //world.sendMessage(`§cUnequipped gauntlet`);
            this.equippedGauntlet.onUnequip()
        }

        // Update equipped item
        this.equippedGauntlet = equippedItem;

        if (!equippedItem) { // If the item was NOT a gauntlet
            //world.sendMessage(`§cRESET ANIMS`);
            anims.controllerSetupSelect(this.player, 'reset', 'common');
            return;
        };

        //world.sendMessage(`§2Equipped new gauntlet`);
        this.equippedGauntlet.onEquip();
        anims.controllerSetupSelect(this.player, 'set', 'common');

    }

    cancelActiveAttack() {
        // Loop through all gauntlets and cancel active attacks
        for (const gauntlet of this.gauntlets.values()) {
            if (gauntlet.activeAttack) {
                gauntlet.activeAttack?.onCancel();
                gauntlet.activeAttack = null;
            }
        }
    }

    /**
     * Updates the GauntletManager state by checking and synchronizing the equipped gauntlet,
     * triggering its tick logic if present, and updating the action bar display.
     */
    tick() {
        this.checkEquippedGauntlet();

        //this.checkForActiveLCDetector();
        //this.updateLCDetectorPosition();

        this.equippedGauntlet?.tick();

        this.gauntlets.get("oct_eg:pyro_gauntlet")?.naturalHeatDepletion();

        if (system.currentTick % 20 === 0) this.checkArmorBonuses();
        this.activeArmorBonuses();

        this.actionBarDisplayManager.update();
    }

    /**
     * Checks the relevant equipped gear for all registered gauntlets (even those not currently equipped).
     */
    checkArmorBonuses() {
        const equippableComp = this.player?.getComponent(EntityComponentTypes.Equippable);

        if (!equippableComp) return;
        if (!this.gauntlets.size) return;

        // Iterate over all gauntlets
        for (let gauntlet of this.gauntlets.values()) {
            let armorPieceCount = 0;

            // Check the player's equipment and match against the armorSlotMappings
            Object.keys(gauntlet?.armorSlotMappings).forEach(slot => {
                if (equippableComp?.getEquipment(EquipmentSlot[slot])?.typeId === gauntlet.armorSlotMappings[slot]) {
                    armorPieceCount++;
                }
            });

            // Set bonus flags for the gauntlet
            // gauntlet.twoPieceBonus = armorPieceCount >= 2 && armorPieceCount < 4;
            // gauntlet.fullSetBonus = armorPieceCount === 4;
            gauntlet.twoPieceBonus = armorPieceCount >= 2;
            gauntlet.fullSetBonus = armorPieceCount >= 4;
        }
    }

    /**
     * Runs the armor bonus effects for all gauntlets owned by the player.
     */
    activeArmorBonuses() {
        for (let gauntlet of this.gauntlets.values()) {
            // Run the two-piece bonus effect if active
            if (gauntlet.twoPieceBonus) {
                gauntlet.twoPieceBonusEffect();
            }

            // Run the full set bonus effect if active
            if (gauntlet.fullSetBonus) {
                gauntlet.fullSetBonusEffect();
            }
        }
    }

    /**
     * Subscribes to global event listeners for player interactions.
     */
    subscribeEventListeners() {

        this.itemUseListener = world.afterEvents.itemUse.subscribe(({ source }) => {
            if (!this.equippedGauntlet) return;
            if (!(source instanceof Player) || source !== this.player) return;
            if (this.equippedGauntlet?.activeAttack) return;

            const attack = this.isSneaking ? this.equippedGauntlet.skill2Attack : this.equippedGauntlet.skill1Attack;

            try {
                this.equippedGauntlet.startSkillAttack(attack);
            } catch (e) {
                //world.sendMessage(`§c${e}`);
            }

        });

        this.itemStopUseListener = world.afterEvents.itemStopUse.subscribe(({ source }) => {
            if (!this.equippedGauntlet) return;
            if (!(source instanceof Player) || source !== this.player) return;

            try {
                if (this.equippedGauntlet?.activeAttack?.isAiming) {
                    this.equippedGauntlet.activeAttack.onRelease();
                }
            } catch (e) {
                //world.sendMessage(`§c${e}`);
            }

        });

        this.jumpListener = world.afterEvents.playerButtonInput.subscribe(event => {
            if (!this.equippedGauntlet) return;
            if (event.player !== this.player || event.button !== "Jump" || event.newButtonState !== "Pressed") return;


            if (this.equippedGauntlet?.enableDoubleJump) {

                if (system.currentTick - this.equippedGauntlet.lastJumpTick <= consts.DOUBLE_JUMP_TICKS
                    && !this.equippedGauntlet.hasDoubleJumped
                ) {
                    //world.sendMessage(`§cDouble jump`);
                    this.equippedGauntlet.onDoubleJump();

                } else {
                    this.equippedGauntlet.onJump();
                }

                this.equippedGauntlet.lastJumpTick = system.currentTick;

            } else {
                this.equippedGauntlet.onJump();
            }

        });

        this.sneakListener = world.afterEvents.playerButtonInput.subscribe(event => {
            if (!this.equippedGauntlet) return;
            if (event.player !== this.player || event.button !== "Sneak") return;

            const gauntlet = this.equippedGauntlet;

            if (event.newButtonState === "Pressed") {

                if (gauntlet) {
                    const currentTick = system.currentTick;
                    if (!gauntlet.activeAttack &&
                        !gauntlet.hasDoubleSneaked &&
                        (currentTick - gauntlet.lastSneakTick <= consts.DOUBLE_SNEAK_TICKS)
                    ) {
                        try {
                            gauntlet.startSkillAttack(gauntlet.skill3Attack);
                        } catch (e) { }

                        gauntlet.hasDoubleSneaked = true;  // Lock the effect from triggering again until released
                    }

                    gauntlet.lastSneakTick = currentTick;

                }

                this.isSneaking = true;
                //this.checkForActiveLCDetector(); // Immediate update
            }

            if (event.newButtonState === "Released") {

                if (gauntlet) {
                    gauntlet.hasDoubleSneaked = false;
                }

                this.isSneaking = false;
                //this.checkForActiveLCDetector(); // Immediate update
            }

        });

        this.gotHitListener = world.afterEvents.entityHurt.subscribe(event => {
            if (event.hurtEntity !== this.player) return;
            if (event.damageSource.damagingEntity === this.player) return;

            try {
                this.gauntlets.get("oct_eg:ender_gauntlet")?.twoPieceBonusEffectFromHit(event.damageSource);
                this.gauntlets.get("oct_eg:anemo_gauntlet")?.twoPieceBonusEffectFromHit(event.damageSource);

                if (!this.equippedGauntlet) return;
                this.equippedGauntlet.onGotHit(event.damageSource, event.damage);

            } catch (e) { }

        }, { entities: [this.player] });

        this.killedEntityListener = world.afterEvents.entityDie.subscribe(event => {
            if (!event.deadEntity.isValid()) return;
            if (event.damageSource.damagingEntity !== this.player) return;
            try {
                this.gauntlets.get("oct_eg:necro_gauntlet")?.twoPieceBonusEffectFromKill();

            } catch (e) { }

        });

        this.primaryHitEntityListener = world.afterEvents.entityHurt.subscribe(({ damage, damageSource, hurtEntity }) => {
            if (!this.equippedGauntlet) return;
            if (this.equippedGauntlet.activeAttack) return;
            if (damageSource?.damagingEntity !== this.player || damageSource.cause !== "entityAttack") return;

            try {
                this.equippedGauntlet?.onPrimaryAttack("entity", { hurtEntity, damage });
            } catch (e) { }

        });

        this.primaryHiBlockListener = world.afterEvents.entityHitBlock.subscribe(({ damagingEntity, hitBlock }) => {
            if (!this.equippedGauntlet) return;
            if (this.equippedGauntlet.activeAttack) return;
            if (damagingEntity !== this.player) return;

            try {
                this.equippedGauntlet?.onPrimaryAttack("block", { hitBlock });
            } catch (e) { }

        });

        /*
        this.LCDitectorListener = world.afterEvents.entityHitEntity.subscribe(event => {
            if (!this.equippedGauntlet) return;
            if (!this.LCDetectorActive || !this.LCDetectorEntity) return;
            if (event.damagingEntity !== this.player || event.hitEntity !== this.LCDetectorEntity) return;

            if (!this.equippedGauntlet || this.equippedGauntlet?.activeAttack) return;
            this.equippedGauntlet.startSkillAttack(this.equippedGauntlet.skillXAttack);
        })
        */
    }

    /**
     * Unsubscribes from all global event listeners when necessary.
     */
    unsubscribeEventListeners() {
        if (this?.equippedGauntlet?.activeAttack) {
            this.equippedGauntlet.activeAttack?.onCancel();
            this.equippedGauntlet.activeAttack = null;
        }

        if (this.itemUseListener) {
            world.beforeEvents.itemUse.unsubscribe(this.itemUseListener);
            this.itemUseListener = null;
        }
        if (this.itemStopUseListener) {
            world.afterEvents.itemStopUse.unsubscribe(this.itemStopUseListener);
            this.itemStopUseListener = null;
        }
        if (this.jumpListener) {
            world.afterEvents.playerButtonInput.unsubscribe(this.jumpListener);
            this.jumpListener = null;
        }
        if (this.sneakListener) {
            world.afterEvents.playerButtonInput.unsubscribe(this.sneakListener);
            this.sneakListener = null;
        }
        if (this.gotHitListener) {
            world.afterEvents.entityHurt.unsubscribe(this.gotHitListener);
            this.gotHitListener = null;
        }
        if (this.killedEntityListener) {
            world.afterEvents.entityDie.unsubscribe(this.killedEntityListener);
            this.killedEntityListener = null;
        }
        if (this.primaryHitEntityListener) {
            world.afterEvents.entityHurt.unsubscribe(this.primaryHitEntityListener);
            this.primaryHitEntityListener = null;
        }
        if (this.primaryHiBlockListener) {
            world.afterEvents.entityHitBlock.unsubscribe(this.primaryHiBlockListener);
            this.primaryHiBlockListener = null;
        }

        /*
        if (this.LCDitectorListener) {
            world.afterEvents.entityHitEntity.unsubscribe(this.LCDitectorListener);
            this.LCDitectorListener = null;
        }
        */
    }

    /**
     * Returns the typeId of the currently equipped gauntlet or null if none is equipped.
     * @returns {string|null}
     */
    getEquipedGauntletTypeId() {
        if (!this.equippedGauntlet) return null;
        for (let [typeId, gauntlet] of this.gauntlets.entries()) {
            if (gauntlet === this.equippedGauntlet) {
                return typeId;
            }
        }
        return null;
    }

    // ----------
    // DEPRECATED
    // ----------
    checkForActiveLCDetector() { // NOTE: Uncoment the calls of this function to re-enable
        if (!this.equippedGauntlet) {
            if (this.LCDetectorActive) this.removeLCDetector();
        }
        else if (!this.isSneaking) {
            if (this.LCDetectorActive) this.removeLCDetector();
        }
        else if (this.equippedGauntlet?.activeAttack?.isAiming || this.equippedGauntlet?.activeAttack?.isCasting) {
            if (this.LCDetectorActive) this.removeLCDetector();
        }
        else if (!this.LCDetectorActive) {
            this.addLCDetector();
        }
    }

    addLCDetector() {
        if (this.LCDetectorActive) return; // Prevent duplicate detectors

        // Get the player's look direction
        const lookVector = this.player.getViewDirection();

        // Offset the spawn position 1 block in the direction the player is facing (including Y)
        const spawnPosition = {
            x: this.player.location.x + lookVector.x * 3.0,
            y: this.player.location.y + lookVector.y * 3.0,
            z: this.player.location.z + lookVector.z * 3.0
        };
        const lcDetector = this.player.dimension.spawnEntity("oct_eg:lc_detect", spawnPosition);
        lcDetector.setDynamicProperty("oct_eg:isLoadedByPlayer", true); // Emsures that this gets despawned the next time is loaded

        // Store the detector entity
        this.LCDetectorEntity = lcDetector;
        this.LCDetectorActive = true;

        this.updateLCDetectorPosition();

    }

    removeLCDetector() {
        if (!this.LCDetectorActive || !this.LCDetectorEntity) return;

        if (this.LCDetectorEntity.isValid()) this.LCDetectorEntity.remove();
        this.LCDetectorEntity = undefined;
        this.LCDetectorActive = false;
    }

    updateLCDetectorPosition() {
        if (!this.LCDetectorActive || !this.LCDetectorEntity) return;
        if (!this.LCDetectorEntity?.isValid()) return;

        const velocity = this.player.getVelocity();

        // Compute the actual movement direction (ignoring Y)
        const horizontalSpeed = Math.sqrt(velocity.x ** 2 + velocity.z ** 2);
        let moveDir = { x: 0, z: 0 };

        if (horizontalSpeed > 0) {
            moveDir.x = velocity.x / horizontalSpeed;
            moveDir.z = velocity.z / horizontalSpeed;
        }

        // XZ offset is now based on movement direction, not look direction
        const XZoffset = Math.min(horizontalSpeed * 7.0, 7.0);
        const Yoffset = velocity.y * 4.0; // Keep vertical movement the same

        const newPosition = {
            x: this.player.location.x + moveDir.x * XZoffset,
            y: this.player.location.y + Yoffset + 1.0,
            z: this.player.location.z + moveDir.z * XZoffset
        };

        this.LCDetectorEntity.teleport(newPosition, {
            checkForBlocks: false,
            dimension: this.player.dimension,
            keepVelocity: true
        });
    }

}

class Gauntlet {
    player = null;
    animsInitialized = false;
    animKey = "";

    primaryAttack = undefined;
    skill1Attack = undefined;
    skill2Attack = undefined;
    skill3Attack = undefined;

    upgradeStages = {
        passive: 0,
        primary: 0,
        skill1: 0,
        skill2: 0,
        skill3: 0
    }

    activeAttack = null; // Tracks which attack is currently casting

    // Contoller Variables
    enableDoubleJump = false; // If enabled, the jump even listener is activated
    lastJumpTick = 0;
    lastSneakTick = 0;
    hasDoubleJumped = false;
    hasDoubleSneaked = false;

    // Gear bonus flags
    twoPieceBonus = false;
    fullSetBonus = false;
    armorSlotMappings = {
        "Head": "minecraft:diamond_helmet",  // Default: diamond helmet
        "Chest": "minecraft:diamond_chestplate",  // Default: diamond chestplate
        "Legs": "minecraft:diamond_leggings",  // Default: diamond leggings
        "Feet": "minecraft:diamond_boots"  // Default: diamond boots
    };

    constructor(player) {
        this.player = player;
    }



    tick() { // Runs every tick

        // Active attack logic
        if (this.activeAttack) {
            if (this.activeAttack.isAiming) { // While aiming
                this.activeAttack.whileAiming();

            } else if (system.currentTick - this.activeAttack?.attackCastTick < this.activeAttack?.castTime) { // While casting
                this.activeAttack.whileCasting();
                // this.player.addEffect("minecraft:weakness", 99999, { amplifier: 255, showParticles: false });

            } else if (system.currentTick - this.activeAttack?.attackCastTick >= this.activeAttack?.castTime) { // End ongoing attack
                this.activeAttack.onEnd();
                this.activeAttack = null;
                // this.player.removeEffect("minecraft:weakness");
            }
        }

        // Execute Passive logic
        this.passive();

        // Reset double jump flag when landing
        if (this.hasDoubleJumped && (this.player.isOnGround || this.player.isInWater)) {
            this.onLanded();
        }

        // Check if the player is equipped with the gauntlet
        if (this.animsInitialized) return;
        if (this.player.getDynamicProperty("oct_eg:clientReady") && !this.animsInitialized) {
            anims.controllerSetupSelect(this.player, 'set', 'common');
            anims.controllerSetupSelect(this.player, 'set', this.animKey);
            this.animsInitialized = true;
        }

    }

    passive() { } // Passive logic, overriden by subclasses
    onJump() { }
    onDoubleJump() { // Double jump logic, overriden by subclasses
        this.hasDoubleJumped = true;
    }
    onLanded() {
        this.hasDoubleJumped = false;
    }
    twoPieceBonusEffect() { } // Runs when called and the 2-piece bonus is active, overriden by subclasses
    fullSetBonusEffect() { } // Runs when called and the 4-piece bonus is active, overriden by subclasses
    onGotHit(damageSource, damage) { } // Runs when the gauntlet user is hurt, overriden by subclasses

    onEquip() { // Runs when the gauntlet is equipped
        this.subscribeEventListeners();

        if (this.player.getDynamicProperty("oct_eg:clientReady")) {
            if (!this.animsInitialized) {
                anims.controllerSetupSelect(this.player, 'set', this.animKey);
                this.animsInitialized = true;
            }
        }
    }

    onUnequip() { // Runs when the gauntlet is unequipped
        this.unsubscribeEventListeners();

        if (this.animsInitialized) {
            anims.controllerSetupSelect(this.player, 'reset', this.animKey);
            this.animsInitialized = false;
        }

        // If an attack is currently active, cancel it
        if (this.activeAttack) {
            this.activeAttack.onCancel();
            this.activeAttack = null;
        }

        // Clear the action bar
        this.player?.gauntletManager?.actionBarDisplayManager.clear();
    }

    /**
     * Executes the primary attack on the specified entity.
     * @param {String} hit - The hit type: "entity" or "block".
     * @param {Object} options - hit event parameters.
     */

    onPrimaryAttack(hit, options) {
        if (hit === "block") {
            this.primaryAttack.onBlockAttack(options.hitBlock);
        } else if (hit === "entity") {
            this.primaryAttack.onEntityAttack(options.hurtEntity, options.damage);
        }
    }

    startSkillAttack(attack) { // Starts the specified attack
        if (!attack || attack.isCasting) return;
        if (attack.isOnCooldown() || this.player?.hasStun) {
            // world.sendMessage(`§6Attack on cooldown: ${attack.name}`);
            this.player.playSound("random.click", { volume: 0.5, pitch: 1.5 });
            return;
        }

        this.activeAttack = attack;
        this.activeAttack.onStart();
    }

    // In case a gauntlet has a specific event listener to subscribe to
    subscribeEventListeners() { }
    unsubscribeEventListeners() { }
}


// Elemental Gauntlets
class AnemoGauntlet extends Gauntlet {
    primaryAttack = new atk.QuickPunch(this, this.player);
    skill1Attack = new atk.AirSlash(this, this.player, 20);
    skill2Attack = new atk.Tornado(this, this.player, 45);
    skill3Attack = new atk.Disengage(this, this.player, 15);

    disengageLand = new atk.DisengageLand(this, this.player);

    armorSlotMappings = {
        "Head": "oct_eg:anemo_helmet",
        "Chest": "oct_eg:anemo_chestplate",
        "Legs": "oct_eg:anemo_leggings",
        "Feet": "oct_eg:anemo_boots"
    };

    highestY = 0; // Track the highest Y position
    inAirLogic = false;

    constructor(player) {
        super(player);
        this.enableDoubleJump = true;
        this.highestY = this.player.location.y;
        this.animKey = "anemo";
        this.baseAirslashCharges = 4;

        // Fetch stored upgrade stages
        this.upgradeStages.passive = this.player.getDynamicProperty("oct_eg:ul.anemo.passive") || 0;
        this.upgradeStages.primary = this.player.getDynamicProperty("oct_eg:ul.anemo.primary") || 0;
        this.upgradeStages.skill1 = this.player.getDynamicProperty("oct_eg:ul.anemo.skill1") || 0;
        this.upgradeStages.skill2 = this.player.getDynamicProperty("oct_eg:ul.anemo.skill2") || 0;
        this.upgradeStages.skill3 = this.player.getDynamicProperty("oct_eg:ul.anemo.skill3") || 0;

        this.disengageInAirActive = false;
        this.disengageLastExecutedTime = 0;
        this.disengageInAirTime = 100;

        this.initializeAirSlashCharges();
    }

    initializeAirSlashCharges() {
        const modifiedMaxCharges = this.baseAirslashCharges
            + (this.upgradeStages.skill1 >= 2 ? 2 : 0)
            + (this.upgradeStages.skill1 >= 3 ? 1 : 0);

        if (this.skill1Attack.maxCharges !== modifiedMaxCharges) {
            this.skill1Attack.maxCharges = modifiedMaxCharges;
            this.skill1Attack.remainingCharges = modifiedMaxCharges;
        }
    }

    onEquip() {
        super.onEquip();
        this.skill1Attack.lastReplenishTime = system.currentTick;
    }

    onUnequip() {
        super.onUnequip();
        this.disengageInAirActive = false;
    }

    twoPieceBonusEffectFromHit(damageSource) { // executed by the onGotHit event
        if (!this.twoPieceBonus) return;

        const randomChance = 0.25;
        if (Math.random() >= randomChance) return;

        const damager = damageSource.damagingEntity;
        if (!damager) return;

        const delta = attackUtils.VOP.sub(damager.location, this.player.location);
        const distance = Math.sqrt(delta.x ** 2 + delta.y ** 2 + delta.z ** 2);
        if (distance >= 3) return;

        const pushDirection = {
            x: delta.x / distance,
            y: delta.y / distance,
            z: delta.z / distance,
        };

        damager.applyKnockback(pushDirection.x, pushDirection.z, 2.0, 0.3);

        this.player.dimension.spawnParticle(
            "oct_eg:full_set_knock",
            this.player.location,
        )
        this.player.dimension.playSound(
            "octovon.eg.gauntlet.anemo.air_spin",
            this.player.location,
            {
                pitch: 1.5
            }
        )
    }

    tick() {
        super.tick();

        if (this.disengageInAirActive && system.currentTick - this.disengageLastExecutedTime >= this.disengageInAirTime) {
            this.disengageInAirActive = false;

        } else if (this.disengageInAirActive
            && this.player.isSneaking
            && this.upgradeStages.skill3 >= 2
            && !(this.activeAttack || this.activeAttack?.isCasting || this.hasDoubleSneaked)
        ) {
            this.disengageInAirActive = false;
            this.startSkillAttack(this.disengageLand);
        }

        if (this.inAirLogic) {
            // Store highest Y
            if (this.player.location.y > this.highestY) this.highestY = this.player.location.y;

            // Apply slow falling
            if (this.highestY - this.player.location.y >= 2.0) {
                //world.sendMessage(`§cApplying slow falling`);
                this.player.addEffect(
                    "minecraft:slow_falling",
                    20,
                    {
                        "amplifier": 0,
                        "showParticles": false
                    }
                );
                this.highestY = 0; // Reset after applying slow falling
            }

            // Spawn particles
            if (system.currentTick % 2 === 0) {
                this.player.dimension.spawnParticle(
                    "oct_eg:double_jump.single",
                    this.player.location,
                )
            }
        }

        if (this.upgradeStages.skill1 >= 3) this.skill1Attack.checkReplenishCharge();

    }

    onJump() {
        super.onJump();

        if (this.hasDoubleJumped
            && !this.hasTripleJumped
            && this.upgradeStages.passive >= 3
        ) {
            this.onDoubleJump();
            this.hasTripleJumped = true;
            //world.sendMessage(`TRIPLE Jumping`);
        }
    }

    onLanded() {
        super.onLanded();
        this.inAirLogic = false;
        this.hasTripleJumped = false;
        this.disengageInAirActive = false;
        this.player.playAnimation(
            'animation.oct_eg.util.null_2',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_jumpTP`
            }
        );
    }

    onDoubleJump() {
        super.onDoubleJump();
        this.highestY = this.player.location.y; // Reset highest Y position to current

        attackUtils.addDirectionalKnockback(this.player, 0.75, 0.75);
        const shouldAddSpeed = this.upgradeStages.passive >= 1;
        if (shouldAddSpeed) {
            this.player.addEffect(
                "minecraft:speed",
                60,
                {
                    "amplifier": 0,
                    "showParticles": false
                }
            );
        }

        this.player.dimension.spawnParticle(
            "oct_eg:double_jump.start",
            { x: this.player.location.x, y: this.player.location.y + 0.5, z: this.player.location.z },
        )
        this.player.dimension.spawnParticle(
            "oct_eg:double_jump.dots",
            { x: this.player.location.x, y: this.player.location.y + 0.5, z: this.player.location.z },
        )
        this.player.dimension.playSound(
            "octovon.eg.gauntlet.anemo.double_jump",
            this.player.location,
        )
        this.player.dimension.playSound(
            "octovon.eg.gauntlet.anemo.air_spin",
            this.player.location,
        )


        this.player.playAnimation(
            'animation.oct_eg.gauntlet.anemo.tp.double_jump',
            {
                blendOutTime: 0.1,
                nextState: "animation.oct_eg.util.null_2",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_jumpTP`
            }
        );
        this.player.playAnimation(
            'animation.oct_eg.util.null_1',
            {
                blendOutTime: 0.0,
                nextState: "animation.oct_eg.gauntlet.anemo.tp.double_jump",
                stopExpression: "!v.is_first_person",
                controller: `_octEgCont_jumpTP`
            }
        );

        this.inAirLogic = true;
    }
}

class PyroGauntlet extends Gauntlet {
    primaryAttack = new atk.FieryPunch(this, this.player);
    skill1Attack = new atk.FlameDash(this, this.player, 1, 15);
    skill2Attack = new atk.FireBreath(this, this.player, 0);
    skill3Attack = new atk.FlamingMeteor(this, this.player, 30);
    armorSlotMappings = {
        "Head": "oct_eg:pyro_helmet",
        "Chest": "oct_eg:pyro_chestplate",
        "Legs": "oct_eg:pyro_leggings",
        "Feet": "oct_eg:pyro_boots"
    };

    constructor(player) {
        super(player);
        this.animKey = "pyro";
        this.heatMeter = 0;
        this.maxHeat = 1000;
        this.naturalDepletionRate = 4; // in ticks
        this.defaultHeatGain = 50; // Amount gained per hit dealt/received
        this.defaultHeatDepletion = 10; // Amount lost per depletion tick
        this.heatGainMult = 1;
        this.heatDepleteMult = 1;
        this.lastHitTime = 0;
        this.startDepletionAt = 40; // in ticks
        this.heatedParticlesActive = false;

        // Fetch stored upgrade stages
        this.upgradeStages.passive = this.player.getDynamicProperty("oct_eg:ul.pyro.passive") || 0;
        this.upgradeStages.primary = this.player.getDynamicProperty("oct_eg:ul.pyro.primary") || 0;
        this.upgradeStages.skill1 = this.player.getDynamicProperty("oct_eg:ul.pyro.skill1") || 0;
        this.upgradeStages.skill2 = this.player.getDynamicProperty("oct_eg:ul.pyro.skill2") || 0;
        this.upgradeStages.skill3 = this.player.getDynamicProperty("oct_eg:ul.pyro.skill3") || 0;
    }

    onGotHit(damageSource, damage) {
        super.onGotHit(damageSource, damage);

        if (consts.HEAT_INVALID_DAMAGE_CAUSES.includes(damageSource.cause)) return;
        this.increaseHeat();
    }

    passive() {
        // Passive upgrade STAGE 1: Heat gain is icreased by 20%
        const passiveMult = this.upgradeStages.passive >= 1 ? 0.2 : 0;
        const activationThreshold = this.upgradeStages.primary >= 3 ? 0.5 : 0.75;
        this.heatGainMult = 1 + passiveMult;

        // Passive upgrade STAGE 2: Activates speed effect when heat meter is above 50% of max
        if (this.upgradeStages.passive >= 2 && this.heatMeter >= this.maxHeat * 0.5) {
            this.player.addEffect("minecraft:speed", 20, { amplifier: 0, showParticles: false });
        }

        if (this.heatMeter >= this.maxHeat * activationThreshold && !this.heatedParticlesActive) {
            this.showHeatedParticles();
        } else if (this.heatMeter < this.maxHeat * activationThreshold && this.heatedParticlesActive) {
            this.hideHeatedParticles();
        }
    }
    onUnequip() {
        super.onUnequip();
        this.hideHeatedParticles();
    }

    twoPieceBonusEffect() {
        // Activate fire resistance
        this.player.addEffect("minecraft:fire_resistance", 20, { showParticles: false });
    }

    fullSetBonusEffect() { }

    showHeatedParticles() {
        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.pyro.heated.active',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: `0`,
                controller: `_octEgCont_heatedParticles`
            }
        );
        this.heatedParticlesActive = true;
    }
    hideHeatedParticles() {
        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.pyro.heated.inactive',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: `0`,
                controller: `_octEgCont_heatedParticles`
            }
        );
        this.heatedParticlesActive = false;
    }

    increaseHeat(heatGained = this.defaultHeatGain) {
        this.heatMeter += Math.round(heatGained * this.heatGainMult);
        if (this.heatMeter > this.maxHeat) {
            this.heatMeter = this.maxHeat;
        }
        this.lastHitTime = system.currentTick;
    }

    decreaseHeat(lostHeat) {
        this.heatMeter -= lostHeat;
        if (this.heatMeter < 0) {
            this.heatMeter = 0;
        }
    }

    naturalHeatDepletion() { // Executes always from the gauntlet manager, even if gauntlet is not equipped
        if (
            system.currentTick % this.naturalDepletionRate !== 0 ||
            system.currentTick - this.lastHitTime < this.startDepletionAt
            || this.activeAttack
            || this.activeAttack?.isCasting
        ) return;

        // Passive upgrade STAGE 3: Sets the minimum heat to 25% of max
        const minHeat = this.upgradeStages.passive >= 3 ? Math.round(this.maxHeat * 0.25) : 0;

        if (this.heatMeter <= minHeat) {
            //this.heatMeter = minHeat;
            return;
        }

        // Full Set Bonus: Decrease heat depletion multiplier by 50%
        const mult = this.fullSetBonus ? 0.5 : 1;

        const lostHeat = Math.round(this.defaultHeatDepletion * this.heatDepleteMult * mult);
        this.heatMeter = Math.max(this.heatMeter - lostHeat, minHeat);

        //world.sendMessage(`§2lost heat: ${lostHeat}`);
    }

}

class EnderGauntlet extends Gauntlet {
    primaryAttack = new atk.EchoPunch(this, this.player);
    skill1Attack = new atk.ShulkerBarrage(this, this.player, 15);
    skill2Attack = new atk.EnderRift(this, this.player, 30);
    skill3Attack = new atk.VoidBeam(this, this.player, 20);
    voidStep = new atk.VoidStep(this, this.player);
    armorSlotMappings = {
        "Head": "oct_eg:ender_helmet",
        "Chest": "oct_eg:ender_chestplate",
        "Legs": "oct_eg:ender_leggings",
        "Feet": "oct_eg:ender_boots"
    };

    constructor(player) {
        super(player);
        this.animKey = "ender";
        this.enableDoubleJump = true;
        this.TwoPieceBonusTriggerChance = 0.25;

        // Fetch stored upgrade stages
        this.upgradeStages.passive = this.player.getDynamicProperty("oct_eg:ul.ender.passive") || 0;
        this.upgradeStages.primary = this.player.getDynamicProperty("oct_eg:ul.ender.primary") || 0;
        this.upgradeStages.skill1 = this.player.getDynamicProperty("oct_eg:ul.ender.skill1") || 0;
        this.upgradeStages.skill2 = this.player.getDynamicProperty("oct_eg:ul.ender.skill2") || 0;
        this.upgradeStages.skill3 = this.player.getDynamicProperty("oct_eg:ul.ender.skill3") || 0;
    }

    passive() { }

    twoPieceBonusEffectFromHit(damageSource) { // executed by the onGotHit event
        if (!this.twoPieceBonus) return;

        if (!damageSource?.damagingEntity || !damageSource.damagingEntity.isValid()) return;
        const randomChance = Math.random() < this.TwoPieceBonusTriggerChance;

        if (!randomChance) return;
        damageSource.damagingEntity?.addEffect("minecraft:levitation", 40, { showParticles: true, amplifier: 0 });
        damageSource.damagingEntity.dimension.spawnParticle("oct_eg:levitation", damageSource.damagingEntity.location);
    }

    fullSetBonusEffect() { }

    onDoubleJump() {
        super.onDoubleJump();

        if (this.activeAttack) return;
        this.startSkillAttack(this.voidStep);
    }

}

class ElectroGauntlet extends Gauntlet {
    primaryAttack = new atk.ThunderPunch(this, this.player);
    skill1Attack = new atk.ChargedDash(this, this.player, 8);
    skill2Attack = new atk.Discharge(this, this.player, 10);
    skill3Attack = new atk.LightningStrike(this, this.player, 30);
    armorSlotMappings = {
        "Head": "oct_eg:electro_helmet",
        "Chest": "oct_eg:electro_chestplate",
        "Legs": "oct_eg:electro_leggings",
        "Feet": "oct_eg:electro_boots"
    };

    constructor(player) {
        super(player);
        this.animKey = "electro";

        // Fetch stored upgrade stages
        this.upgradeStages.passive = this.player.getDynamicProperty("oct_eg:ul.electro.passive") || 0;
        this.upgradeStages.primary = this.player.getDynamicProperty("oct_eg:ul.electro.primary") || 0;
        this.upgradeStages.skill1 = this.player.getDynamicProperty("oct_eg:ul.electro.skill1") || 0;
        this.upgradeStages.skill2 = this.player.getDynamicProperty("oct_eg:ul.electro.skill2") || 0;
        this.upgradeStages.skill3 = this.player.getDynamicProperty("oct_eg:ul.electro.skill3") || 0;
    }

    passive() { }

    twoPieceBonusEffect() {
        this.player?.addEffect("minecraft:speed", 20, { showParticles: false, amplifier: 0 });
    }

    fullSetBonusEffect() {
        if (!this.player.isSprinting) return;
        try {
            this.player?.addEffect("minecraft:speed", 12, { showParticles: false, amplifier: 1 });
            this.player.dimension.spawnParticle("oct_eg:running_parks", this.player.location);
        } catch (e) { }
    }

    onGotHit(damageSource, damage) {
        super.onGotHit(damageSource, damage);

        if (this.upgradeStages.passive < 2) return;
        if (!damageSource?.damagingEntity || damageSource?.cause !== EntityDamageCause.entityAttack) return;

        this.applyStatic(1, damageSource.damagingEntity);
    }

    /**
     * Applies static charge to the target entity, modified by ability upgrades.
     *
     * @param {int} amount - The amount of static charge to apply to the target.
     * @param {Entity} target - The target entity to which the static charge is applied
     */
    applyStatic(amount, target) {
        if (!target?.isValid()) return;
        try {
            if (this.upgradeStages.passive >= 3) target.setDynamicProperty("oct_eg:static_adds_weakness", true);
            // If not passive 3, don't update the dynamic property

            target?.addStatic(amount, this.upgradeStages.passive >= 1 ? 5 : 3);

        } catch (e) { }
    }

}

class NecroGauntlet extends Gauntlet {
    primaryAttack = new atk.LeachingPunch(this, this.player);
    skill1Attack = new atk.SoulSnipe(this, this.player, 1);
    skill2Attack = new atk.PutridRibs(this, this.player, 15);
    skill3Attack = new atk.UndeadScythes(this, this.player, 25);

    armorSlotMappings = {
        "Head": "oct_eg:necro_helmet",
        "Chest": "oct_eg:necro_chestplate",
        "Legs": "oct_eg:necro_leggings",
        "Feet": "oct_eg:necro_boots"
    };

    constructor(player) {
        super(player);
        this.animKey = "necro";
        this.harvesterInputListener = null;
        this.harvesterStartTime = undefined;
        this.harvesterCastingTime = 40;
        this.harvesterEffectsPlayed = false;
        this.canUseHarvester = true;

        // Fetch stored upgrade stages
        this.upgradeStages.passive = this.player.getDynamicProperty("oct_eg:ul.necro.passive") || 0;
        this.upgradeStages.primary = this.player.getDynamicProperty("oct_eg:ul.necro.primary") || 0;
        this.upgradeStages.skill1 = this.player.getDynamicProperty("oct_eg:ul.necro.skill1") || 0;
        this.upgradeStages.skill2 = this.player.getDynamicProperty("oct_eg:ul.necro.skill2") || 0;
        this.upgradeStages.skill3 = this.player.getDynamicProperty("oct_eg:ul.necro.skill3") || 0;

        this.soulMeter = 0;
        this.baseSoulMeterMax = 5;
        this.soulMeterMax = this.baseSoulMeterMax;
    }

    tick() {
        super.tick();
        this.handleSouldMeter();
        this.checkHarvesterStatus();

        if (system.currentTick % 4 !== 0) return;
        for (const entity of this.player.dimension.getEntities(
            {
                maxDistance: 1,
                location: this.player.location,
                families: ["oct_eg.soul"],
            }
        )) {
            if (entity.typeId !== "oct_eg:soul") continue;

            const addedSouls = this.addSouls(1);
            if (!addedSouls) continue;

            this.player.dimension.playSound("octovon.eg.gauntlet.necro.soul_collect", this.player.location);
            this.player.dimension.spawnParticle("oct_eg:smoke.undead", this.player.location);
            entity.remove();
        }
    }

    handleSouldMeter() {
        this.soulMeter = this.player.getDynamicProperty("oct_eg:soul_meter") || 0;

        this.soulMeterMax = this.baseSoulMeterMax
            + (this.upgradeStages.passive >= 2 ? 1 : 0)
            + (this.upgradeStages.passive >= 3 ? 1 : 0);

        this.player.playAnimation(
            `animation.oct_eg.util.soul_meter.set.${this.soulMeter}`,
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_soulMeter`
            }
        );
    }

    addSouls(amount = 1) {
        const availableSpace = this.soulMeterMax - this.soulMeter;
        if (availableSpace <= 0) return false;

        this.soulMeter += Math.min(amount, availableSpace);
        this.player.setDynamicProperty("oct_eg:soul_meter", this.soulMeter);
        return true;
    }
    removeSouls(amount = 1) {
        if (this.soulMeter <= 0) return false;

        const soulsToRemove = Math.min(amount, this.soulMeter);
        this.soulMeter -= soulsToRemove;
        this.player.setDynamicProperty("oct_eg:soul_meter", this.soulMeter);
        return true;
    }

    onUnequip() {
        super.onUnequip();
        this.player.playAnimation(
            `animation.oct_eg.util.soul_meter.set.0`,
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_soulMeter`
            }
        );
        this.harvesterCancel();
    }

    lifeSteal(amount = 1) {
        const healthComp = this.player.getComponent("minecraft:health");
        const maxHealth = healthComp.effectiveMax;
        const currentHP = healthComp.currentValue;
        healthComp.setCurrentValue(Math.min(currentHP + amount, maxHealth));

        this.player.dimension.spawnParticle("oct_eg:life_steal", this.player.location);
        this.player.dimension.playSound("octovon.eg.gauntlet.necro.lifesteal", this.player.location);

        //world.sendMessage(`§6Life Steal: ${amount}`);
    }

    twoPieceBonusEffectFromKill() {
        if (!this.twoPieceBonus) return;
        // world.sendMessage(`§6Two Piece Bonus Triggered`);
        this.player.addEffect("minecraft:saturation", 1, { showParticles: false, amplifier: 0 });
    }

    subscribeEventListeners() {
        super.subscribeEventListeners();

        this.harvesterInputListener = world.afterEvents.playerButtonInput.subscribe(event => {
            if (event.player !== this.player || event.button !== "Sneak") return;

            if (event.newButtonState === "Pressed" && this.canUseHarvester && this.soulMeter > 0) {
                this.canUseHarvester = false;
                this.harvesterStartTime = system.currentTick;
                //world.sendMessage(`§6preparing to harvest...`);
            }

            if (event.newButtonState === "Released") {
                this.harvesterCancel();
            }
        });
    }

    unsubscribeEventListeners() {
        super.unsubscribeEventListeners();

        if (this.harvesterInputListener) {
            world.afterEvents.playerButtonInput.unsubscribe(this.harvesterInputListener);
            this.harvesterInputListener = null;
        }
    }

    checkHarvesterStatus() {
        if (!this.harvesterStartTime) return;
        //world.sendMessage(`§8Harvesting in ${Math.floor((this.harvesterCastingTime - (system.currentTick - this.harvesterStartTime)) / 20)} seconds...`);

        const elapsed = system.currentTick - this.harvesterStartTime;

        if (elapsed >= 30 && !this.harvesterEffectsPlayed) {
            this.player.dimension.playSound("octovon.eg.gauntlet.necro.harvester.charging", this.player.location);
            this.player.dimension.spawnParticle("oct_eg:harvester.insignia", this.player.location);
            this.harvesterEffectsPlayed = true;
        }

        if (elapsed >= this.harvesterCastingTime) {
            this.harvesterFinished();

        } else if (this.harvesterEffectsPlayed) {
            this.player.runCommand('camerashake add @s 0.1 0.2 positional');
            this.player.dimension.spawnParticle("oct_eg:harvester.lines", this.player.location);

            if (this.soulMeter <= 0) this.harvesterCancel();
        }
    }

    harvesterCancel() {
        this.canUseHarvester = true;
        this.harvesterStartTime = undefined;

        if (!this.harvesterEffectsPlayed) return;
        this.harvesterEffectsPlayed = false;
        this.player.dimension.playSound(
            "extinguish.candle",
            this.player.location,
        );
        //world.sendMessage(`§cCancelled harvesting...`);
    }

    harvesterFinished() {
        this.harvesterStartTime = undefined;
        this.harvesterEffectsPlayed = false;
        //world.sendMessage(`§6Harvesting...`);

        const spawnRange = 2;
        const minSpawnRange = 0.5;
        const randomRange = Math.random() * (spawnRange - minSpawnRange) + minSpawnRange;
        const randomAngle = Math.random() * Math.PI * 2;
        const randomPosition = {
            x: this.player.location.x + Math.cos(randomAngle) * randomRange,
            y: this.player.location.y,
            z: this.player.location.z + Math.sin(randomAngle) * randomRange,
        };


        const undeadMinion = this.player.dimension.spawnEntity("oct_eg:undead_minion", this.player.location);
        undeadMinion.getComponent("minecraft:tameable")?.tame(this.player);

        if (this.fullSetBonus) undeadMinion.setProperty("oct_eg:full_set_bonus", true);
        if (this.upgradeStages.passive >= 1) undeadMinion.setProperty("oct_eg:passive_upgrade_1", true);

        undeadMinion.tryTeleport(randomPosition, { checkForBlocks: true });
        this.player.dimension.playSound("octovon.eg.gauntlet.necro.undead_minion.spawn", undeadMinion.location);
        undeadMinion.nameTag = `§3${this.player.name}'s\n§8Undead Minion§r`;

        this.removeSouls(1);
    }
}

class HydroGauntlet extends Gauntlet {
    primaryAttack = new atk.FrozenPunch(this, this.player);
    skill1Attack = new atk.IceSpikes(this, this.player, 15);
    skill2Attack = new atk.Tsunami(this, this.player, 30);
    skill3Attack = new atk.SlipNSlide(this, this.player, 20);
    aquaticDashAttack = new atk.AquaticDash(this, this.player);

    armorSlotMappings = {
        "Head": "oct_eg:hydro_helmet",
        "Chest": "oct_eg:hydro_chestplate",
        "Legs": "oct_eg:hydro_leggings",
        "Feet": "oct_eg:hydro_boots"
    };

    constructor(player) {
        super(player);
        this.animKey = "hydro";
        this.enableDoubleJump = true;
        this.baseIceSpikeCharges = 3;

        // Fetch stored upgrade stages
        this.upgradeStages.passive = this.player.getDynamicProperty("oct_eg:ul.hydro.passive") || 0;
        this.upgradeStages.primary = this.player.getDynamicProperty("oct_eg:ul.hydro.primary") || 0;
        this.upgradeStages.skill1 = this.player.getDynamicProperty("oct_eg:ul.hydro.skill1") || 0;
        this.upgradeStages.skill2 = this.player.getDynamicProperty("oct_eg:ul.hydro.skill2") || 0;
        this.upgradeStages.skill3 = this.player.getDynamicProperty("oct_eg:ul.hydro.skill3") || 0;

        this.initializeIceSpikeCharges();
    }

    initializeIceSpikeCharges() {
        const modifiedMaxCharges = this.baseIceSpikeCharges
            + (this.upgradeStages.skill1 >= 2 ? 1 : 0)
            + (this.upgradeStages.skill1 >= 3 ? 1 : 0);

        if (this.skill1Attack.maxCharges !== modifiedMaxCharges) {
            this.skill1Attack.maxCharges = modifiedMaxCharges;
            this.skill1Attack.remainingCharges = modifiedMaxCharges;
        }
    }

    onDoubleJump() {
        //world.sendMessage(`§6Double Jumping`);
        super.onDoubleJump();
        if (this.upgradeStages.passive > 1 && !(this.activeAttack || this.activeAttack?.isCasting) && this.player.isInWater) {
            this.startSkillAttack(this.aquaticDashAttack);
        }
        if (this.activeAttack === this.skill3Attack && this.skill3Attack.isCasting && this.upgradeStages.skill3 >= 3) {
            //world.sendMessage(`§6Double Jumping while casting SlipNSlide`);
            this.skill3Attack.isCasting = false;
            this.skill3Attack.attackStartTick = null;
            this.skill3Attack.attackCastTick = null;
            this.activeAttack = null;
            this.skill3Attack.onEnd();
        }
    }

    passive() {
        super.passive();
        const isSwimming = this.player.isInWater;
        if (isSwimming) this.player.addEffect("minecraft:water_breathing", 10, { amplifier: 0, showParticles: false });
    }

    twoPieceBonusEffect() {
        const isSwimming = this.player.isInWater;
        if (isSwimming && system.currentTick % 50 === 0) this.player.addEffect("minecraft:regeneration", 52, { amplifier: 0, showParticles: false });
    }

}

class GeoGauntlet extends Gauntlet {
    primaryAttack = new atk.HeavyPunch(this, this.player);
    skill1Attack = new atk.LeapSlam(this, this.player, 15);
    skill2Attack = new atk.StoneWall(this, this.player, 30);
    skill3Attack = new atk.SeismicSmash(this, this.player, 40);

    armorSlotMappings = {
        "Head": "oct_eg:geo_helmet",
        "Chest": "oct_eg:geo_chestplate",
        "Legs": "oct_eg:geo_leggings",
        "Feet": "oct_eg:geo_boots"
    };


    constructor(player) {
        super(player);
        this.animKey = "geo";

        // Fetch stored upgrade stages
        this.upgradeStages.passive = this.player.getDynamicProperty("oct_eg:ul.geo.passive") || 0;
        this.upgradeStages.primary = this.player.getDynamicProperty("oct_eg:ul.geo.primary") || 0;
        this.upgradeStages.skill1 = this.player.getDynamicProperty("oct_eg:ul.geo.skill1") || 0;
        this.upgradeStages.skill2 = this.player.getDynamicProperty("oct_eg:ul.geo.skill2") || 0;
        this.upgradeStages.skill3 = this.player.getDynamicProperty("oct_eg:ul.geo.skill3") || 0;

        this.baseIronSkinReflectPercentage = 0.2;
        this.baseIronSkinReflectRadius = 2.5;
        this.baseIronSkinWeaknessDuration = 60;
    }


    twoPieceBonusEffect() {
        if (this.player.getEffect("minecraft:resistance")?.amplifier >= 1) return;
        this.player.addEffect("minecraft:resistance", 20, { amplifier: 0, showParticles: false });
    }

    fullSetBonusEffect() { }

    onGotHit(damageSource, damage) {
        super.onGotHit(damageSource, damage);

        if (!damageSource?.damagingEntity || damageSource?.cause !== EntityDamageCause.entityAttack) return;

        this.ironSkinEffect(damage, damageSource.damagingEntity);
    }

    ironSkinEffect(damage, entity) {
        const hpComp = this.player.getComponent("minecraft:health");
        const currentHpPercentage = hpComp.currentValue / hpComp.effectiveMax;

        const reflectedPercentage = this.baseIronSkinReflectPercentage
            + (this.fullSetBonus ? 0.1 : 0)
            + (this.upgradeStages.passive >= 2 && currentHpPercentage <= 0.5 ? 0.1 : 0);

        const shouldReflectInRadius = this.upgradeStages.passive >= 1 ? true : false;
        const shouldApplyWeakness = this.upgradeStages.passive >= 3 && currentHpPercentage <= 0.5 ? true : false;
        const reflectRadius = this.baseIronSkinReflectRadius;

        const reflectedDamage = damage * reflectedPercentage;

        entity.applyDamage(reflectedDamage, { cause: EntityDamageCause.thorns });

        if (shouldReflectInRadius) {

            for (const target of attackUtils.getValidTargetsInRange(this.player, { maxDistance: reflectRadius })) {

                target.applyDamage(
                    reflectedDamage,
                    { cause: EntityDamageCause.thorns }
                );

                if (shouldApplyWeakness) {
                    target.addEffect(
                        "minecraft:weakness",
                        this.baseIronSkinWeaknessDuration,
                        { showParticles: true, amplifier: 0 }
                    );
                }
            }

            this.player.dimension.spawnParticle("oct_eg:iron_skin.sparks", this.player.location);
        }

        this.player.dimension.playSound("octovon.eg.gauntlet.geo.iron_skin", this.player.location, { volume: 0.7 });

        //world.sendMessage(`damage dealt: ${(reflectedDamage / damage).toFixed(2)}`);

    }

}