import { system, world, Player, InputPermissionCategory, EasingType, EntityDamageCause, MolangVariableMap, EntityComponentTypes, BlockPermutation } from "@minecraft/server";
import * as attackUtils from "../attackUtils.js";
import * as projectiles from "../entities/projectiles.js";
import * as consts from "../constants.js";
import { attackCastingPreviewMethods } from "../entities/attack_preview_methods.js";

export class GauntletAttack {
    /**
     * @param {Gauntlet} gauntlet
     * @param {Player} player - The player executing the attack.
     * @param {int} castTime - Ticks required to complete the attack.
     * @param {int} maxCharges - Number of times the attack can be used before cooldown.
     * @param {int} cooldownTime - Cooldown time in seconds.
     * @param {string} mode - Attack mode ("instant" or "aimed").
     */
    constructor(gauntlet, player, castTime = 10, maxCharges = 1, cooldownTime = 3, mode = "instant") {
        this.gauntlet = gauntlet;
        this.player = player;
        this.mode = mode;
        this.isCasting = false;
        this.isAiming = false;
        this.castTime = castTime;
        this.attackStartTick = null;
        this.attackCastTick = null;
        this.maxCharges = maxCharges;
        this.remainingCharges = maxCharges;
        this.cooldownTime = Math.round(cooldownTime * 20);
        this.cooldownExpiry = 0;
    }

    // Starts attack (for instant mode) or aiming (for aimed mode)
    onStart() {
        if (this.isCasting || this.remainingCharges <= 0 || this.isOnCooldown()) return;

        if (this.mode === "aimed") {
            this.isAiming = true;
            this.attackStartTick = system.currentTick;
            this.whileAiming();

        } else {
            this.executeAttack();
        }
    }

    // Called repeatedly while aiming
    whileAiming() {
        if (!this.isAiming) return;
        //world.sendMessage("§8Aiming...");
    }

    // Called repeatedly while casting
    whileCasting() {
        if (!this.isCasting) return;
        //world.sendMessage("§8Casting...");
    }

    // Executes the attack
    executeAttack() {
        this.isAiming = false;
        this.isCasting = true;
        this.attackCastTick = system.currentTick;
        this.remainingCharges--;

        // If out of charges, trigger cooldown
        if (this.remainingCharges === 0) {
            this.triggerCooldown();
        }
    }

    // Called when input is released (fires for aimed mode)
    onRelease() {
        if (this.isAiming) {
            this.executeAttack();
        }
    }

    // Cancels the on going attack
    onCancel() {
        this.isAiming = false;
        this.isCasting = false;

        this.player.playSound(
            "extinguish.candle",
            this.player.location,
        )
        //world.sendMessage("§cCANCELED...");
    }

    // Called when the attack ends
    onEnd() {
        //world.sendMessage("§cENDED..");
        this.isAiming = false;
        this.isCasting = false;
        this.attackStartTick = null;
        this.attackCastTick = null;
    }

    // Checks if attack is on cooldown
    isOnCooldown() {
        return system.currentTick < this.cooldownExpiry;
    }

    // Triggers cooldown and resets charges
    triggerCooldown() {
        this.cooldownExpiry = system.currentTick + this.cooldownTime;
        this.remainingCharges = this.maxCharges;
    }
}

export class GauntletChargedAttack extends GauntletAttack {
    /**
     * @param {Gauntlet} gauntlet
     * @param {Player} player - The player executing the attack.
     * @param {int} castTime - Ticks required to complete the attack.
     * @param {int} maxCharges - Number of times the attack can be used before cooldown.
     * @param {int} cooldownTime - Cooldown time in seconds.
     * @param {array} chargeStages - Array of times (in seconds) for each charge level.
     */
    constructor(gauntlet, player, castTime = 10, maxCharges = 1, cooldownTime = 3, chargeStages = [2, 4]) {
        super(gauntlet, player, castTime, maxCharges, cooldownTime, "aimed");

        this.chargeStages = chargeStages.map(time => Math.round(time * 20)); // Convert seconds to ticks
        this.currentChargeStage = 0;

        this.hasAttackPreviewLogic = false;
        this.linkedAttackPreview = null;
        this.attackPreviewTypeId = undefined;
        this.castingRange = 0;
        this.canCastOnLiquid = false;
        Object.assign(this, attackCastingPreviewMethods);

        this.setAttackSpecificProperties();

    }
    setAttackSpecificProperties() { }

    whileAiming() {
        super.whileAiming();

        const elapsedTicks = system.currentTick - this.attackStartTick; // Time since the start of the attack
        let timePassed = 0;
        let newStage = 0;

        for (let i = 0; i < this.chargeStages.length; i++) {
            timePassed += this.chargeStages[i];
            if (elapsedTicks >= timePassed) {
                newStage = i + 1;
            } else {
                break;
            }
        }

        // Stage update
        if (newStage !== this.currentChargeStage) {

            this.currentChargeStage = newStage;

            if (this.currentChargeStage > 0) {
                //world.sendMessage(`§8Charge Stage: §a${this.currentChargeStage}`);
                this.player.playSound("octovon.eg.gauntlet.stage_charged", {
                    volume: 0.3,
                    pitch: 1 + ((this.currentChargeStage - 1) * 0.2),
                });
            }
        }

        this.updateAttackPreviewAnimations();

        // world.sendMessage(`§8Charge Stage: ${this.currentChargeStage}`);
    }

    executeAttack() {
        if ((this.hasAttackPreviewLogic && !this.linkedAttackPreview) || (this.chargeStages.length > 0 && this.currentChargeStage === 0)) {
            //world.sendMessage(`§cattack failed!`);
            this.onCancel();

        } else {
            this.chargedAttackSuccess();
        }
    }

    chargedAttackSuccess() {
        super.executeAttack();
    }

    onStart() {
        super.onStart();
        this.currentChargeStage = 0;
    }

    onCancel() {
        this.currentChargeStage = 0;
        if (this.hasAttackPreviewLogic) this.removeAttackPreview();
        super.onCancel();
    }

    onEnd() {
        this.currentChargeStage = 0;
        if (this.hasAttackPreviewLogic) this.removeAttackPreview();
        super.onEnd();
    }

    onPreviewSpawn() { }
    onStageChange() { }

    updateAttackPreviewAnimations() {
        if (!this.hasAttackPreviewLogic || !this.linkedAttackPreview || !this.linkedAttackPreview.isValid()) return;
    }
}

export class GauntletPrimary {
    /**
     * @param {Gauntlet} gauntlet
     * @param {Player} player - The player executing the attack.
     */
    constructor(gauntlet, player) {
        this.player = player;
        this.gauntlet = gauntlet;

        this.maxPunchCount = 2;
        this.punchResetTime = 30; // in ticks
        this.punchCount = 0;
        this.lastPunchTime = 0;
    }

    onEntityAttack(hurtEntity, damage) {
        if (!hurtEntity || !hurtEntity.isValid()) return;
        this.attackParticles(hurtEntity.location);
        this.onAttack();
    }

    onBlockAttack(hitBlock) {
        this.attackParticles(hitBlock.bottomCenter());
        this.onAttack();
    }

    onAttack() {
        if (system.currentTick - this.lastPunchTime <= 8) { // Magic number, don't question it
            //world.sendMessage(`§8Punch Count: §c${this.punchCount}`);
            return;
        };
        this.iteratePunchCount();
    }

    attackParticles(targetLoc) {
        const dir = this.player.getViewDirection();
        const playerLoc = this.player.location;

        const dx = targetLoc.x - playerLoc.x;
        const dy = targetLoc.y - playerLoc.y;
        const dz = targetLoc.z - playerLoc.z;

        const offset = Math.sqrt(dx * dx + dy * dy + dz * dz) - 0.5;

        const impactPos = {
            x: playerLoc.x + dir.x * offset,
            y: playerLoc.y + 1.5 + dir.y * offset,
            z: playerLoc.z + dir.z * offset
        };

        this.player.dimension.spawnParticle("oct_eg:punch.impact", impactPos);
    }

    iteratePunchCount() {
        if (this.punchCount >= this.maxPunchCount || system.currentTick - this.lastPunchTime >= this.punchResetTime) {
            this.punchCount = 0;
            //world.sendMessage(`Reset to 0`);
        }

        this.punchCount += 1;
        this.lastPunchTime = system.currentTick;

        this.player.playAnimation(`animation.oct_eg.util.gauntlet.common.punch_id.${this.punchCount}`,
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: `0`,
                controller: `_octEgCont_punchCounter`
            }
        )

        //world.sendMessage(`Punch Count: §6${this.punchCount}`);
    }
}



// PYRO ATTACKS
export class FieryPunch extends GauntletPrimary {
    constructor(gauntlet, player) {
        super(gauntlet, player);
        this.defaultBurnDuration = 5;
        this.explosionRadius = 2;
        this.maxPunchCount = 2;
    }

    onEntityAttack(hurtEntity, damage) {
        super.onEntityAttack(hurtEntity, damage);

        // Primary upgrade STAGE 3: Activates effect when heat meter is 50% of max
        const activationThreshold = this.gauntlet.upgradeStages.primary >= 3 ? 0.5 : 0.75;

        if (this.gauntlet.heatMeter >= this.gauntlet.maxHeat * activationThreshold) {

            // Primary upgrade STAGE 1: Adds burn duration
            const burnDuration = this.defaultBurnDuration + (this.gauntlet.upgradeStages.primary >= 1 ? 2 : 0);

            // Primary upgrade STAGE 2: Activates explosion on hit
            const shouldExplode = this.gauntlet.upgradeStages.primary >= 2;
            const isIgnitedEntity = hurtEntity.getComponent(EntityComponentTypes.OnFire);

            if (shouldExplode && isIgnitedEntity) {

                for (const entity of attackUtils.getValidTargetsInRange(hurtEntity, {
                    maxDistance: this.explosionRadius,
                    excludedEntities: [this.player],
                })) {
                    attackUtils.applyModifiedDamage(
                        entity,
                        Math.round(damage * 0.25),  // 25% of original damage
                        {
                            cause: EntityDamageCause.entityExplosion,
                            damagingEntity: this.player
                        }
                    );
                }

                hurtEntity.dimension.spawnParticle("oct_eg:fiery_punch.stage2explosion", hurtEntity.location);
                hurtEntity.dimension.playSound("octovon.eg.gauntlet.pyro.fieryPunchExplosion", hurtEntity.location);
            }

            hurtEntity.setOnFire(burnDuration, true);
        }

        // Increase heat
        this.gauntlet.increaseHeat();
    }

}
export class FlamingMeteor extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = Math.ceil(3.0 * 20); // The animation length
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime);

        this.currentFrameIndex = 0;
        this.yrot = 0;
        this.location = this.player.location;

        this.animationFrames = [
            {
                time: 0.0, fn: () => {
                    this.updateCamera("minecraft:free", { x: 0, y: 20, z: -4 }, { x: 80, y: "~" }, 0.5, `in_sine`);
                    this.player.addEffect("minecraft:levitation", 40, { amplifier: 5, showParticles: false });
                }
            },
            { time: 0.5, fn: () => this.updateCamera("minecraft:free", { x: 0, y: 32, z: -6 }, { x: 75, y: "~" }, 0.5, `linear`) },
            { time: 1.0, fn: () => this.updateCamera("minecraft:free", { x: 0, y: 28, z: -5 }, { x: 80, y: "~" }, 0.5, `out_sine`) },
            { time: 1.5, fn: () => this.updateCamera("minecraft:free", { x: 0, y: 5, z: -3.0 }, { x: 55, y: "~" }, 0.6, `out_sine`) },
            {
                time: 2.1, fn: () => {
                    this.player.teleport(this.location, { rotation: { x: 0, y: this.yrot } });
                    this.trackedVisualEntity = null; // Stop tracking the visual entity

                    this.attackImpact();
                }
            },
            {
                time: 2.3, fn: () => {
                    this.player.camera.setCamera("minecraft:free", {
                        location: this.player.getHeadLocation(),
                        rotation: this.player.getRotation(),
                        easeOptions: {
                            easeTime: 0.7,
                            easeType: EasingType.InOutSine,
                        },
                    });

                    this.player.playAnimation("animation.oct_eg.gauntlet.pyro.tp.flaming_meteor.end",
                        {
                            blendOutTime: 0.0,
                            nextState: 'animation.oct_eg.util.null_1',
                            stopExpression: 'q.all_animations_finished',
                            controller: `_octEgCont_FlamingMeteor`
                        }
                    );
                }
            },
            // { // Dont run this, breaks the logic. Instead, it runs on its own after the attack is finished
            //     time: 3.0, fn: () => {
            //         this.onEnd();
            //     }
            // },
        ];

        this.trackedVisualEntity = null;
        this.batchId = "batch-1";
    }

    onEnd() {
        super.onEnd();

        // Reset attack values
        this.attackStartTick = null;
        this.attackCastTick = null;
        this.currentFrameIndex = 0;

        // Reset player
        this.player.camera.clear();
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, true);
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);
    }

    onCancel() { // Only called when the attack is cancelled midway
        super.onCancel();

        // Reset attack values
        this.attackStartTick = null;
        this.attackCastTick = null;
        this.currentFrameIndex = 0;

        // Reset player
        this.player.camera.clear();
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, true);
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);

        this.player.teleport(this.location, { rotation: { x: 0, y: this.yrot } });
        if (this.player.getEffect("minecraft:levitation")) this.player.removeEffect("minecraft:levitation");
        this.player.playAnimation("animation.oct_eg.util.null_1",
            {
                blendOutTime: 0.0,
                nextState: '_null',
                stopExpression: '0.0',
                controller: `_octEgCont_FlamingMeteor`
            }
        );

        // Remove visual entity
        if (this.trackedVisualEntity && this.trackedVisualEntity.isValid()) this.trackedVisualEntity.remove();

    }

    updateCamera(preset, pos, rot, eTime, eType) {
        const cmd = `execute positioned ${this.location.x} ${this.location.y} ${this.location.z} rotated ${this.yrot} 0 positioned ^${pos.x}^${pos.y}^${pos.z} run camera @s set ${preset} ease ${eTime} ${eType} pos ~~~ rot ${rot.x} ${rot.y}`;

        this.player.runCommand(cmd);
    }

    executeAttack() {
        super.executeAttack();
        //world.sendMessage(`§6Flaming Meteor Started!`);

        try {
            // Get the start position
            let blockRay = undefined;
            try {
                blockRay = this.player.dimension.getBlockFromRay(this.player.location,
                    { x: 0, y: -1, z: 0 },
                    {
                        includeLiquidBlocks: false,
                        includePassableBlocks: false,
                        maxDistance: 10
                    }
                );
            } catch (e) { }

            if (!blockRay || !blockRay.block) {
                super.onCancel(); // Cancel the simple way since the attack logic didn't start

                this.attackStartTick = null;
                this.attackCastTick = null;
                this.currentFrameIndex = 0;

                // world.sendMessage(`§cFlaming Meteor Failed because it couldn't find the ground!`);
                return;
            }

            const loc = blockRay.block?.above(1).bottomCenter() || blockRay.block.location;
            this.location = loc;

            // Get the start rotation
            this.yrot = this.player.getRotation().y;

            // Setup the animation
            this.currentFrameIndex = 0;

            const visualEntity = this.player.dimension.spawnEntity('oct_eg:flaming_meteor', this.location);
            this.trackedVisualEntity = visualEntity; // Track the visual entity to remove it later if needed

            this.trackedVisualEntity.setProperty("oct_eg:ryinit", this.yrot);
            this.trackedVisualEntity.setProperty("oct_eg:ult_stage1", this.gauntlet.upgradeStages.skill3 >= 1 ? true : false);
            this.batchId = `batch${this.trackedVisualEntity.id}`;

            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, false);
            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, false);

            // this.player.addEffect("minecraft:levitation", 40, { amplifier: 5, showParticles: false });
            this.player.addEffect("minecraft:resistance", 60, { amplifier: 255, showParticles: false });
            this.player.addEffect("minecraft:slow_falling", 60, { amplifier: 255, showParticles: false });

            this.player.playAnimation("animation.oct_eg.gauntlet.pyro.tp.flaming_meteor.ongoing",
                {
                    blendOutTime: 0.0,
                    nextState: '_null',
                    stopExpression: '0',
                    controller: `_octEgCont_FlamingMeteor`
                }
            );

            this.player.playAnimation("animation.oct_eg.gauntlet.pyro.tp.flaming_meteor.start",
                {
                    blendOutTime: 0.0,
                    nextState: 'animation.oct_eg.gauntlet.pyro.tp.flaming_meteor.ongoing',
                    stopExpression: 'q.all_animations_finished',
                    controller: `_octEgCont_FlamingMeteor`
                }
            );

        } catch (e) {
            //console.error(`§cOctovon: Flaming Meteor Attack Failed:§8\n${e}`);
            this.onEnd();
        }

    }

    whileCasting() {
        super.whileCasting();

        const animSeconds = (system.currentTick - this.attackCastTick) / 20;
        while (
            this.currentFrameIndex < this.animationFrames.length &&
            animSeconds >= this.animationFrames[this.currentFrameIndex].time
        ) {
            this.animationFrames[this.currentFrameIndex].fn();
            this.currentFrameIndex++;
        }
    }

    attackImpact() {
        this.player.runCommand('camerashake add @a[r=10] 0.3 1 positional');

        const attackDamage = 20 + (this.gauntlet.upgradeStages.skill3 >= 3 ? 10 : 0);
        const attackRange = 5 + (this.gauntlet.upgradeStages.skill3 >= 1 ? 2 : 0);
        const attackKnockback = { h: 2, v: 1 };

        //world.sendMessage(`§6Attack Damage: ${attackDamage} §6Attack Range: ${attackRange}`);

        for (const entity of attackUtils.getValidTargetsInRange(this.player, {
            startLocation: this.location,
            maxDistance: attackRange,
        })) {
            attackUtils.applyModifiedDamage(
                entity,
                attackDamage,
                {
                    cause: EntityDamageCause.entityExplosion,
                    damagingEntity: this.player
                }
            )

            const pointAwayVector = attackUtils.getPointAwayDirection(entity.location, this.location, true);
            try {
                entity.applyKnockback(pointAwayVector.x, pointAwayVector.z, attackKnockback.h, attackKnockback.v);
            } catch (e) { }

            const defaultHeat = this.player.gauntletManager.equippedGauntlet.defaultHeatGain
            this.player.gauntletManager.gauntlets.get('oct_eg:pyro_gauntlet')?.increaseHeat(defaultHeat * 0.5);
        }

        const shouldSpawnRocks = this.gauntlet.upgradeStages.skill3 >= 2 ? true : false;
        if (shouldSpawnRocks) {

            const rockDamage = 5 + (this.gauntlet.upgradeStages.skill3 >= 3 ? 5 : 0);
            const rockRange = 2;
            const rockAmount = 15;
            const rockSpawnRadius = 10;

            for (let i = 0; i < rockAmount; i++) {
                const rock = this.player.dimension.spawnEntity("oct_eg:molten_rock", this.location);
                rock.setDynamicProperty("oct_eg:rock_damage", rockDamage);
                rock.setDynamicProperty("oct_eg:rock_range", rockRange);

                rock.addTag(this.batchId);
                rock?.getComponent("minecraft:tameable")?.tame(this.player);

            }

            // Use a raycast to find the non-solid blocks above and add their heights to maxHeight
            let maxHeight = this.location.y + 5; // add 5 to maxHeight by default
            const heightCheck = this.player.dimension.getBlockFromRay(this.location, { x: 0, y: 1, z: 0 }, {
                includeLiquidBlocks: false,
                includePassableBlocks: false,
                maxDistance: 10,
            });

            // If ray hits a non-solid block, add its height to maxHeight
            if (heightCheck?.block) {
                maxHeight += heightCheck.block.location.y - this.location.y;
            }

            //world.sendMessage(`Height: ${maxHeight}`);

            system.runTimeout(() => {
                this.player.runCommand(`spreadplayers ${this.location.x} ${this.location.z} 2 ${rockSpawnRadius} @e[r=1, type=oct_eg:molten_rock, tag=${this.batchId}] ${maxHeight}`);
            }, 2);
        }

    }
}
export function moltenRockImpact(rock) {
    const owner = rock.getComponent("minecraft:tameable")?.tamedToPlayer;
    if (!owner) return;

    const rockDamage = rock.getDynamicProperty("oct_eg:rock_damage") ?? 5;
    const rockRange = rock.getDynamicProperty("oct_eg:rock_range") ?? 2;
    const nearbyEntities = attackUtils.getValidTargetsInRange(rock, {
        maxDistance: rockRange,
        excludedEntities: [owner],
    });

    for (const entity of nearbyEntities) {
        attackUtils.applyModifiedDamage(
            entity,
            rockDamage,
            {
                cause: EntityDamageCause.entityExplosion,
                damagingEntity: owner
            }
        );

        entity.setOnFire(5, true);
    }

    if (nearbyEntities.length <= 0) return;
    owner.gauntletManager.gauntlets.get('oct_eg:pyro_gauntlet')?.increaseHeat();

    //world.sendMessage(`§6Attack Damage: ${rockDamage} §6Attack Range: ${rockRange}`);
}
export class FireBreath extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 0
        const maxCharges = 1;
        const mode = "aimed";
        super(gauntlet, player, castTime, maxCharges, cooldownTime, mode);

        this.fireTickRate = 2;
        this.heatConsumption = 15;
        this.baseDamage = 2;
        this.baseAttackRange = 8;
        this.attackHeightOffset = 1.3
    }
    onStart() {
        super.onStart();

        this.player.dimension.playSound(
            "octovon.eg.gauntlet.pyro.fire_breath.start",
            this.player.location,
        )
        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.pyro.fire_breath.start',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_utilFireBreath`
            }
        );
    }

    executeAttack() {
        super.executeAttack();
        this.player.dimension.playSound(
            "octovon.eg.gauntlet.pyro.fire_breath.end",
            this.player.location,
        )
        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.pyro.fire_breath.end',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_utilFireBreath`
            }
        );
    }

    onCancel() {
        super.onCancel();
        this.player.dimension.playSound(
            "octovon.eg.gauntlet.pyro.fire_breath.end",
            this.player.location,
        )
        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.pyro.fire_breath.end',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_utilFireBreath`
            }
        );
    }

    whileAiming() {
        super.whileAiming();

        if (system.currentTick % this.fireTickRate !== 0 || this.isCasting) return;

        if (this.gauntlet.heatMeter === undefined) return;

        const direction = this.player.getViewDirection();
        const startLocation = { x: this.player.location.x, y: this.player.location.y + this.attackHeightOffset, z: this.player.location.z };

        const particleValuesMap = new MolangVariableMap();
        particleValuesMap.setFloat('direction_x', direction.x);
        particleValuesMap.setFloat('direction_y', direction.y);
        particleValuesMap.setFloat('direction_z', direction.z);


        if (this.gauntlet.heatMeter <= 0) {
            this.player.dimension.spawnParticle(
                "oct_eg:fire_breath.fire.sparks",
                { x: startLocation.x, y: startLocation.y, z: startLocation.z },
                particleValuesMap
            )
            this.player.dimension.spawnParticle(
                "oct_eg:fire_breath.smoke",
                { x: startLocation.x, y: startLocation.y, z: startLocation.z },
                particleValuesMap
            )
            return;
        }


        const damage = this.baseDamage + this.calculateBonusDamage();
        const burnDuration = 2 + (this.gauntlet.upgradeStages.skill2 >= 3 ? 3 : 0);

        const offsetDistance = 0.5;
        const perpendicular = {
            x: -direction.z,
            y: 0,
            z: direction.x,
        };
        const rays = [
            {
                location: startLocation,
                direction: direction
            },
            {
                location: {
                    x: startLocation.x + perpendicular.x * offsetDistance,
                    y: startLocation.y,
                    z: startLocation.z + perpendicular.z * offsetDistance
                },
                direction: direction
            },
            {
                location: {
                    x: startLocation.x - perpendicular.x * offsetDistance,
                    y: startLocation.y,
                    z: startLocation.z - perpendicular.z * offsetDistance
                },
                direction: direction
            }
        ];

        for (const entity of attackUtils.getValidTargetsFromRays(
            this.player,
            rays,
            { maxDistance: this.baseAttackRange }
        )) {
            attackUtils.applyModifiedDamage(
                entity,
                damage,
                {
                    cause: EntityDamageCause.fire,
                    damagingEntity: this.player
                }
            );
            entity?.setOnFire(burnDuration, true);
        }

        this.spawnParticles(startLocation, particleValuesMap);
        this.consumeHeat();

        //world.sendMessage(`§8Fire Breath -> §cDMG: ${damage} §8HM: ${heatMeter}`);
    }

    spawnParticles(location, particleValuesMap) {
        const fireparticle = this.gauntlet.upgradeStages.skill2 >= 3 ? "oct_eg:fire_breath.soulfire" : "oct_eg:fire_breath.fire";

        this.player.dimension.spawnParticle(
            fireparticle,
            { x: location.x, y: location.y, z: location.z },
            particleValuesMap
        )
        this.player.dimension.spawnParticle(
            "oct_eg:fire_breath.fire.sparks",
            { x: location.x, y: location.y, z: location.z },
            particleValuesMap
        )

        this.player.dimension.playSound(
            "octovon.eg.gauntlet.pyro.fire_breath",
            this.player.location,
        )
    }

    consumeHeat() {
        // Skill2 upgrade STAGE 1: Heat consumption is reduced by 20%
        const consumedHeat = this.heatConsumption * (this.gauntlet.upgradeStages.skill2 >= 1 ? 0.8 : 1);
        this.gauntlet?.decreaseHeat(consumedHeat);
        //world.sendMessage(`§8time passed: §c${((system.currentTick - this.attackStartTick) / 20).toFixed(1)}`);
    }

    calculateBonusDamage() {
        const timePassed = system.currentTick - this.attackStartTick;
        const u2AddedDamagePerSecond = 1;
        const u2MaxBonusSeconds = 5;
        const u3AddedDamageFlat = 1;

        // Skill2 upgrade STAGE 2: +2 damage per second
        const u2BonusDamage = this.gauntlet.upgradeStages.skill2 >= 2
            ? Math.min(Math.floor(timePassed / 20), u2MaxBonusSeconds) * u2AddedDamagePerSecond
            : 0;

        // Skill2 upgrade STAGE 3: +1 flat damage
        const u3BonusDamage = this.gauntlet.upgradeStages.skill2 >= 3
            ? u3AddedDamageFlat
            : 0;

        return u2BonusDamage + u3BonusDamage;
    }
}
export class FlameDash extends GauntletAttack {
    constructor(gauntlet, player, maxCharges, cooldownTime) {
        const castTime = 5;
        const mode = "aimed";
        super(gauntlet, player, castTime, maxCharges, cooldownTime, mode);

        this.dashDistance = 5;
        this.baseDamage = 8;
        this.trailLife = 60; // in ticks
        this.hitEntities = undefined;


        this.startLocation = null;
        this.endLocation = null;
        this.direction = null;
    }

    onStart() {
        super.onStart();
        this.hitEntities = new Set();
    }

    executeAttack() {
        super.executeAttack();

        const viewDir = this.player.getViewDirection();
        const horizontalLength = Math.sqrt(viewDir.x ** 2 + viewDir.z ** 2);
        this.direction = {
            x: viewDir.x / horizontalLength,
            z: viewDir.z / horizontalLength
        };

        this.startLocation = this.player.location;
        const dashDistance = this.dashDistance + (this.gauntlet.upgradeStages.skill1 >= 1 ? 2 : 0);
        this.endLocation = {
            x: this.startLocation.x + this.direction.x * dashDistance,
            y: this.startLocation.y,
            z: this.startLocation.z + this.direction.z * dashDistance
        };


        this.player.addEffect("minecraft:speed", 5, { amplifier: 2, showParticles: false });
        this.player.addEffect("minecraft:resistance", 5, { amplifier: 255, showParticles: false });

        //this.player.dimension.spawnParticle("oct_eg:double_jump.start", this.endLocation);
        this.player.dimension.playSound("octovon.eg.gauntlet.pyro.flame_dash", this.startLocation, { pitch: 1.2 });

        this.player.playAnimation(
            'animation.oct_eg.util.null_1',
            {
                blendOutTime: 0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_flameDash`
            }
        );
        this.player.playAnimation(
            'animation.oct_eg.gauntlet.pyro.tp.flame_dash',
            {
                blendOutTime: 0.1,
                nextState: "animation.oct_eg.util.null_1",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_flameDash`
            }
        );
    }

    whileCasting() {
        super.whileCasting();
        if (!this.startLocation || !this.endLocation || !this.direction) return;

        this.movePlayer();
        this.damageEntitiesInPath();
    }

    onEnd() {
        super.onEnd();
        system.runTimeout(() => this.leaveFireTrail(), 4);
    }

    movePlayer() {
        const dashDistance = this.dashDistance + (this.gauntlet.upgradeStages.skill1 >= 1 ? 2 : 0);
        const speed = dashDistance / this.castTime; // units per tick
        this.player.applyKnockback(this.direction.x, this.direction.z, speed, 0.1);
    }

    damageEntitiesInPath() {
        const damage = this.baseDamage + (this.gauntlet.upgradeStages.skill1 >= 1 ? 2 : 0);

        for (const entity of attackUtils.getValidTargetsFromRays(this.player,
            [
                {
                    location: {
                        x: this.player.location.x,
                        y: this.player.location.y + 1,
                        z: this.player.location.z
                    },
                    direction: { x: this.direction.x, y: 0, z: this.direction.z }
                }
            ],
            {
                maxDistance: 2,
                excludedEntities: Array.from(this.hitEntities),
            }
        )) {
            attackUtils.applyModifiedDamage(
                entity,
                damage,
                {
                    cause: EntityDamageCause.contact,
                    damagingEntity: this.player
                }
            );

            this.gauntlet?.increaseHeat();
            this.hitEntities.add(entity);
        }
    }

    leaveFireTrail() {
        const shouldSpawnTrail = this.gauntlet.upgradeStages.skill1 >= 2 ? true : false;
        if (!shouldSpawnTrail) return;

        const idUpgradedTrail = this.gauntlet.upgradeStages.skill1 >= 3 ? true : false;

        const spacing = 1; // Distance between fire points
        const distanceTraveled = Math.sqrt(
            (this.player.location.x - this.startLocation.x) ** 2 +
            (this.player.location.z - this.startLocation.z) ** 2
        );
        const steps = Math.floor(distanceTraveled / spacing);
        //world.sendMessage(`Trails To Spawn: ${steps + 1}`)

        for (let i = 0; i <= steps; i++) {
            const x = this.startLocation.x + this.direction.x * i * spacing;
            const z = this.startLocation.z + this.direction.z * i * spacing;
            const y = this.startLocation.y;

            const spawnPos = { x, y, z };


            const trail = this.player.dimension.spawnEntity("oct_eg:fire_trail", spawnPos);
            trail?.getComponent("minecraft:tameable")?.tame(this.player);

            trail.setDynamicProperty("oct_eg:time_created", system.currentTick);
            trail.setDynamicProperty("oct_eg:lifespan", this.trailLife);

            if (idUpgradedTrail) trail.setDynamicProperty("oct_eg:upgraded_trail", true);


            // Optional debug
            //world.sendMessage(`spawned trail at: ${x.toFixed(2)}, ${z.toFixed(2)}`);
        }
    }

}

export function fireTrailTick(trail) {
    const owner = trail.getComponent("minecraft:tameable")?.tamedToPlayer;
    if (!owner) {
        trail.remove();
        return;
    };

    const timeCreated = trail.getDynamicProperty("oct_eg:time_created");
    const lifespan = trail.getDynamicProperty("oct_eg:lifespan");
    const shouldExplode = trail.getDynamicProperty("oct_eg:upgraded_trail");
    if (system.currentTick - timeCreated >= lifespan) {
        if (shouldExplode) fireTrailExplode(trail, owner);
        trail.remove();
        return;
    }

    for (const entity of attackUtils.getValidTargetsInRange(trail, {
        maxDistance: 1.5,
        excludedEntities: [owner],
    })) {
        entity.setOnFire(5, true);
    }
}

function fireTrailExplode(trail, owner) {
    const nearbyEntities = attackUtils.getValidTargetsInRange(trail, {
        maxDistance: 3,
        excludedEntities: [owner],
    });
    for (const entity of nearbyEntities) {
        attackUtils.applyModifiedDamage(
            entity,
            10,
            {
                cause: EntityDamageCause.entityExplosion,
                damagingEntity: owner
            }
        );
        entity.setOnFire(5, true);
    }
    trail.dimension.spawnParticle("oct_eg:fiery_punch.stage2explosion", trail.location);
    trail.dimension.playSound("octovon.eg.gauntlet.pyro.fieryPunchExplosion", trail.location);

    if (nearbyEntities.length <= 0) return;
    owner.gauntletManager.gauntlets.get('oct_eg:pyro_gauntlet')?.increaseHeat();
}


// ENDER ATTACKS
export class EchoPunch extends GauntletPrimary {
    constructor(gauntlet, player) {
        super(gauntlet, player);
        this.maxPunchCount = 4;
        this.triggerEcho = false;
    }

    onAttack() {
        super.onAttack();

        if (this.punchCount !== this.maxPunchCount && !this.triggerEcho) return;
        this.triggerEcho = false;
        system.run(() => {
            const randomPitch = 0.9 + Math.random() * 0.2;
            this.player.dimension.playSound(
                "octovon.eg.gauntlet.ender.echo_punch",
                this.player.location,
                {
                    pitch: randomPitch
                }
            )
            const vewDir = this.player.getViewDirection();
            const proj = new projectiles.EchoPunchProjectile(this.player, vewDir, this.gauntlet);
            proj.shootProjectile();
        })
    }

    iteratePunchCount() {
        if (system.currentTick - this.lastPunchTime <= 8) {
            return;
        };

        if (this.punchCount >= this.maxPunchCount || system.currentTick - this.lastPunchTime >= this.punchResetTime) {
            this.punchCount = 0;
        }

        this.punchCount += 1;
        this.lastPunchTime = system.currentTick;

        const punchId = 2 - (this.punchCount % 2);
        this.player.playAnimation(`animation.oct_eg.util.gauntlet.common.punch_id.${punchId}`,
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: `0`,
                controller: `_octEgCont_punchCounter`
            }
        )

        //world.sendMessage(`Punch ID: §6${punchId}`);
        //world.sendMessage(`Punch Count: §6${this.punchCount}`);
    }

}
export class VoidStep extends GauntletAttack {
    constructor(gauntlet, player) {
        const castTime = 15;
        const cooldownTime = 3;
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime);

        this.dashDistance = 7;
        this.trailLife = 60;
    }

    executeAttack() {
        super.executeAttack();

        try {
            const viewDir = this.player.getViewDirection();
            const rayDir = { x: viewDir.x, y: 0, z: viewDir.z };
            const startLocation = this.player.location;
            const dashDistance = this.dashDistance + (this.gauntlet.upgradeStages.passive >= 1 ? 3 : 0);

            let endLocation = startLocation;

            const rayHit = this.player.dimension.getBlockFromRay(startLocation, rayDir, {
                maxDistance: dashDistance,
                includeLiquidBlocks: false,
                includePassableBlocks: false,
            });

            if (rayHit?.block) {
                switch (rayHit.face) {
                    case "Up":
                        endLocation = rayHit.block.above().bottomCenter();
                        break;
                    case "Down":
                        endLocation = rayHit.block.below(2).bottomCenter();
                        break;
                    case "North":
                        endLocation = rayHit.block.north().bottomCenter();
                        break;
                    case "South":
                        endLocation = rayHit.block.south().bottomCenter();
                        break;
                    case "West":
                        endLocation = rayHit.block.west().bottomCenter();
                        break;
                    case "East":
                        endLocation = rayHit.block.east().bottomCenter();
                        break;
                }
                //world.sendMessage(`Hit Face: ${rayHit.face}`);
                //world.sendMessage(`Block Location: ${endLocation.x.toFixed(2)}, ${endLocation.y.toFixed(2)}, ${endLocation.z.toFixed(2)}`);
            } else {
                endLocation = {
                    x: startLocation.x + rayDir.x * dashDistance,
                    y: startLocation.y,
                    z: startLocation.z + rayDir.z * dashDistance
                }
            }

            if (this.gauntlet.upgradeStages.passive >= 2) {
                const trail = this.player.dimension.spawnEntity("oct_eg:ender_trail", startLocation);
                trail?.getComponent("minecraft:tameable")?.tame(this.player);
                trail.setDynamicProperty("oct_eg:time_created", system.currentTick);
                trail.setDynamicProperty("oct_eg:lifespan", this.trailLife);
            }

            this.player.dimension.playSound(
                "mob.shulker.teleport",
                startLocation,
            );
            this.player.dimension.spawnParticle(
                "oct_eg:void_step.sparks_in",
                startLocation,
            );
            this.player.playAnimation(
                'animation.oct_eg.gauntlet.ender.tp.dash_start',
                {
                    blendOutTime: 0.0,
                    nextState: "animation.oct_eg.gauntlet.ender.tp.dash_end",
                    stopExpression: "q.all_animations_finished",
                    controller: `_octEgCont_voidStep`
                }
            );
            this.player.addEffect("minecraft:speed", 10, { amplifier: 3, showParticles: false });
            this.player.addEffect("minecraft:resistance", 10, { amplifier: 255, showParticles: false });
            this.player.addEffect("minecraft:slow_falling", 10, { amplifier: 255, showParticles: false });
            this.player.addEffect("minecraft:darkness", 40, { amplifier: 255, showParticles: false });
            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, false);

            system.runTimeout(() => {
                this.player.teleport(endLocation, { checkForBlocks: true });
                this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);

                this.player.playAnimation(
                    'animation.oct_eg.gauntlet.ender.tp.dash_end',
                    {
                        blendOutTime: 0,
                        nextState: "_null",
                        stopExpression: "0",
                        controller: `_octEgCont_voidStep`
                    }
                );
                this.player.dimension.playSound(
                    "mob.shulker.teleport",
                    endLocation,
                );
                this.player.dimension.spawnParticle(
                    "oct_eg:void_step.sparks_out",
                    endLocation,
                );

                if (this.gauntlet.fullSetBonus) {
                    this.gauntlet.primaryAttack.triggerEcho = true;
                }

                if (this.gauntlet.upgradeStages.passive >= 3) {
                    this.player.addEffect("minecraft:resistance", 40, { amplifier: 2, showParticles: false });
                }

            }, 10)

        } catch (e) {
            this.onCancel();
        }
    }
}
export function enderTrailTick(trail) {
    const owner = trail.getComponent("minecraft:tameable")?.tamedToPlayer;
    if (!owner) {
        trail.remove();
        return;
    };

    const timeCreated = trail.getDynamicProperty("oct_eg:time_created");
    const lifespan = trail.getDynamicProperty("oct_eg:lifespan");

    if (system.currentTick - timeCreated >= lifespan) {
        trail.remove();
        return;
    }

    for (const entity of attackUtils.getValidTargetsInRange(trail, {
        maxDistance: 2.5,
        excludedEntities: [owner],
    })) {
        attackUtils.applyModifiedDamage(
            entity,
            2,
            {
                cause: EntityDamageCause.magic,
                damagingEntity: owner
            }
        );
    }
}
export class ShulkerBarrage extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime);

        this.bulletsAmount = 3;
        this.baseBulletLife = 6; // in seconds
    }

    executeAttack() {
        super.executeAttack();
        try {

            // Remove all old targets
            for (const entity of this.player.dimension.getEntities({
                location: this.location,
                maxDistance: 32,
                tags: ["oct_eg.shulker_target"]
            })) {
                entity.removeTag("oct_eg.shulker_target");
            };

            // Add new targets
            for (const entity of attackUtils.getValidTargetsInRange(this.player, { maxDistance: 24 })) {
                entity.addTag("oct_eg.shulker_target");
            };

            const faceDir = this.player.getViewDirection();
            const spawnLock = {
                x: this.player.location.x + faceDir.x,
                y: this.player.location.y + 1.8,
                z: this.player.location.z + faceDir.z
            };

            this.player.playAnimation(
                'animation.oct_eg.util.gauntlet.ender.shulker_barrage',
                {
                    blendOutTime: 0.0,
                    nextState: "_null",
                    stopExpression: "q.all_animations_finished",
                    controller: `_octEgCont_utilShulkerBarrage`
                }
            );

            this.player.dimension.spawnParticle(
                "oct_eg:void_step.sparks_in",
                spawnLock,
            );
            this.player.dimension.spawnParticle(
                "oct_eg:void_step.flash",
                spawnLock,
            );
            this.player.dimension.playSound("mob.shulker.shoot", this.player.location);

            const randomSpread = 0.25;
            const shootSpeed = 1;

            const appliesLevitation = this.gauntlet.upgradeStages.skill1 >= 1 ? true : false;
            const explodesOnImpact = this.gauntlet.upgradeStages.skill1 >= 2 ? true : false;
            const bulletLife = this.baseBulletLife + (this.gauntlet.upgradeStages.skill1 >= 3 ? 4 : 0);
            const bulletsAmount = this.bulletsAmount + (this.gauntlet.upgradeStages.skill1 >= 3 ? 2 : 0);


            for (let i = 0; i < bulletsAmount; i++) {
                const bullet = this.player.dimension.spawnEntity("oct_eg:shulker_bullet", spawnLock);
                const shootDir = {
                    x: shootSpeed * Math.random() * randomSpread - randomSpread / 2,
                    y: shootSpeed,
                    z: shootSpeed * Math.random() * randomSpread - randomSpread / 2
                }
                bullet.setDynamicProperty("oct_eg:bullet_life", bulletLife);
                bullet.setDynamicProperty("oct_eg:time_created", system.currentTick);
                bullet.setDynamicProperty("oct_eg:explodes_on_impact", explodesOnImpact);
                bullet.setDynamicProperty("oct_eg:applies_levitation", appliesLevitation);
                bullet.getComponent("minecraft:tameable")?.tame(this.player);

                bullet.applyImpulse(shootDir);
            }

        } catch (e) {
            this.onCancel();
        }
    }
}
export function shulkerBulletTick(bullet) {
    if (!bullet.isValid()) {
        bullet?.remove();
        return;
    };

    const timeCreated = bullet.getDynamicProperty("oct_eg:time_created");
    const bulletLife = bullet.getDynamicProperty("oct_eg:bullet_life");

    if (system.currentTick - timeCreated >= bulletLife * 20) {
        bullet.remove();
        return;
    }
}
function resetShulkerTarget(target) {
    if (!target || !target?.isValid()) {
        //world.sendMessage("§cInvalid target");
        return;
    }
    target.removeTag("oct_eg.shulker_target.disabled");
}
world.afterEvents.entityHitEntity.subscribe(event => {
    const bullet = event?.damagingEntity;
    const target = event?.hitEntity;

    if (!bullet.isValid() || bullet?.typeId !== "oct_eg:shulker_bullet") return;

    bullet.dimension.spawnParticle("oct_eg:shulker_bullet.impact", bullet.location);
    bullet.dimension.playSound("mob.shulker.bullet.hit", bullet.location);

    const appliesLevitation = bullet.getDynamicProperty("oct_eg:applies_levitation");
    const explodesOnImpact = bullet.getDynamicProperty("oct_eg:explodes_on_impact");

    if (appliesLevitation) {
        target.addEffect("minecraft:levitation", 60, { showParticles: true, amplifier: 0 });
        target.dimension.spawnParticle("oct_eg:levitation", target.location);
    }

    const owner = bullet.getComponent("minecraft:tameable")?.tamedToPlayer;
    if (explodesOnImpact && owner) {
        for (const entity of attackUtils.getValidTargetsInRange(bullet, {
            maxDistance: 2.5,
            excludedEntities: [owner],
        })) {
            attackUtils.applyModifiedDamage(
                entity,
                4,
                {
                    cause: EntityDamageCause.entityExplosion,
                    damagingEntity: owner
                }
            );
        }
        bullet.dimension.spawnParticle("oct_eg:shulker_bullet.explosion", bullet.location);
        bullet.dimension.playSound("octovon.eg.gauntlet.pyro.fieryPunchExplosion", bullet.location);
    }

    target.addTag("oct_eg.shulker_target.disabled");
    system.runTimeout(() => {
        resetShulkerTarget(target);
    }, 40);

    bullet.remove();

}, { entityTypes: ["oct_eg:shulker_bullet"] });

export class EnderRift extends GauntletChargedAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime, []);
    }
    setAttackSpecificProperties() {
        this.hasAttackPreviewLogic = true;
        this.attackPreviewTypeId = "oct_eg:attack_cast_preview.rift";
        this.castingRange = 20;
        this.canCastOnLiquid = true;
        this.riftBaseDamage = 5;
        this.riftBaseLifeTime = 5;

    }
    whileAiming() {
        super.whileAiming();
        this.handleAttackPreview();
    }
    chargedAttackSuccess() {
        super.chargedAttackSuccess();

        const riftAppliesDamage = this.gauntlet.upgradeStages.skill2 >= 1 ? true : false;
        const riftPullRange = this.gauntlet.upgradeStages.skill2 >= 2 ? 10 : 7;

        const soundKey = this.gauntlet.upgradeStages.skill2 >= 3 ? 'upgraded' : 'normal';
        const riftLifeTime = this.riftBaseLifeTime + (this.gauntlet.upgradeStages.skill2 >= 3 ? 2 : 0);
        const riftExplosionDamage = this.riftBaseDamage + (this.gauntlet.upgradeStages.skill2 >= 3 ? 5 : 0);

        const loc = this.linkedAttackPreview?.location;
        const rift = this.player.dimension.spawnEntity("oct_eg:rift", loc);
        rift.setDynamicProperty("oct_eg:time_created", system.currentTick);
        rift.setDynamicProperty("oct_eg:life_time", riftLifeTime);
        rift.setDynamicProperty("oct_eg:sound_key", soundKey);
        rift.setDynamicProperty("oct_eg:pull_range", riftPullRange);
        rift.setDynamicProperty("oct_eg:apply_tick_damage", riftAppliesDamage);
        rift.setDynamicProperty("oct_eg:damage", riftExplosionDamage);

        rift.getComponent("minecraft:tameable")?.tame(this.player);


        rift.dimension.playSound("octovon.eg.gauntlet.ender.echo_punch", this.player.location);

        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.ender.ender_rift',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_utilEnderRift`
            }
        );

        this.removeAttackPreview();
    }
    updateAttackPreviewAnimations() {
        super.updateAttackPreviewAnimations();
        const animKey = this.gauntlet.upgradeStages.skill2 >= 2 ? 'upgraded' : 'normal';
        this.linkedAttackPreview?.playAnimation(`animation.oct_eg.rift_preview.charging.${animKey}`,
            {
                blendOutTime: 0,
                nextState: '_null',
                stopExpression: '0.0',
                controller: `_octEgCont_atPrevStage`
            }
        );
    }
}
export class VoidBeam extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime);

        this.fireTickRate = 8;
        this.baseDamage = 3;
        this.baseAttackRange = 16;
        this.attackHeightOffset = 1.3
        this.linkedLaserEntity = null;
        this.shouldApplySlowness = false;
        this.canMove = false;
    }
    onStart() {
        super.onStart();
        this.createLaserEntity();

        this.shouldApplySlowness = this.gauntlet.upgradeStages.skill3 >= 1 ? true : false;
        this.castTime = this.gauntlet.upgradeStages.skill3 >= 2 ? 100 : 80;
        this.canMove = this.gauntlet.upgradeStages.skill3 >= 3 ? true : false;

        this.player.dimension.playSound(
            "octovon.eg.gauntlet.ender.void_beam.start",
            this.player.location,
        )
        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.pyro.fire_breath.start',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_utilFireBreath`
            }
        );

        if (!this.canMove) {
            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, false);
        }

        this.player.addEffect("minecraft:slowness", 12, { amplifier: 2, showParticles: false });
        this.player.runCommand("camerashake add @s 0.3 0.6 positional");
    }

    onEnd() {
        super.onEnd();
        this.removeLaserEntity();

        this.player.dimension.playSound(
            "octovon.eg.gauntlet.ender.void_beam.active",
            this.player.location,
            {
                pitch: 2
            }
        )
        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.pyro.fire_breath.end',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_utilFireBreath`
            }
        );

        if (!this.player.inputPermissions.isPermissionCategoryEnabled(InputPermissionCategory.Movement)) {
            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);
        }
    }

    onCancel() {
        super.onCancel();
        this.removeLaserEntity();

        this.player.dimension.playSound(
            "octovon.eg.gauntlet.ender.void_beam.active",
            this.player.location,
            {
                pitch: 2
            }
        )
        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.pyro.fire_breath.end',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_utilFireBreath`
            }
        );

        if (!this.player.inputPermissions.isPermissionCategoryEnabled(InputPermissionCategory.Movement)) {
            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);
        }
    }

    whileCasting() {
        super.whileCasting();

        try {
            const direction = this.player.getViewDirection();
            const startLocation = {
                x: this.player.location.x,
                y: this.player.location.y + this.attackHeightOffset + (this.player.isSneaking ? -0.3 : 0),
                z: this.player.location.z
            };

            const damage = this.baseDamage + (this.gauntlet.upgradeStages.skill3 >= 2 ? 1 : 0);
            const range = this.baseAttackRange + (this.gauntlet.upgradeStages.skill3 >= 2 ? 4 : 0);

            for (const entity of attackUtils.getValidTargetsFromRays(this.player,
                [
                    {
                        location: startLocation,
                        direction: direction
                    }
                ],
                {
                    maxDistance: range
                }
            )) {
                attackUtils.applyModifiedDamage(
                    entity,
                    damage,
                    {
                        cause: EntityDamageCause.magic,
                        damagingEntity: this.player
                    }
                );
                if (this.shouldApplySlowness) entity?.addEffect("minecraft:slowness", 20, { amplifier: 1, showParticles: true });
                if (system.currentTick % 4 === 0) entity.dimension.playSound(
                    "octovon.eg.gauntlet.ender.void_beam.impact",
                    entity.location,
                    {
                        volume: 0.5,
                        pitch: 1
                    }
                );
            }

            //Get the length of the beam
            const blockRay = this.player.getBlockFromViewDirection({
                maxDistance: range,
                includeLiquidBlocks: false,
                includePassableBlocks: false
            })
            const blockHitLocation = blockRay?.block?.center();
            const longestDistance = blockHitLocation ? attackUtils.VOP.distance(startLocation, blockHitLocation) : 16;
            this.updateLaserEntity(startLocation, direction, longestDistance);

            if (blockHitLocation && system.currentTick % 2 === 0) this.player.dimension.playSound(
                "octovon.eg.gauntlet.pyro.fire_breath.end",
                blockHitLocation,
                {
                    volume: 0.7,
                    pitch: 2
                }
            );

            if ((system.currentTick - this.attackCastTick) % this.fireTickRate === 0) {
                this.player.addEffect("minecraft:slowness", 12, { amplifier: 2, showParticles: false });
                this.player.runCommand("camerashake add @s 0.1 0.6 positional");

                const randomPitch = 1 - Math.random() * 0.2;
                this.player.dimension.playSound(
                    "octovon.eg.gauntlet.ender.void_beam.active",
                    this.player.location,
                    {
                        pitch: randomPitch
                    }
                );
            }

        } catch (e) {
            console.error(e);
        }
    }


    createLaserEntity() {
        const direction = this.player.getViewDirection();
        const loc = {
            x: this.player.location.x,
            y: this.player.location.y + this.attackHeightOffset,
            z: this.player.location.z,
        }
        this.linkedLaserEntity = this.player.dimension.spawnEntity(
            "oct_eg:void_beam",
            loc,
        );

        if (!this.linkedLaserEntity || !this.linkedLaserEntity.isValid()) {
            this.onCancel();
        }

        // Set XY rotations
        const rotY = Math.atan2(direction.z, direction.x) * 180 / Math.PI - 90;
        const rotX = -Math.atan2(direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)) * 180 / Math.PI;

        // Clamp angles to [-360, 360] just in case
        this.linkedLaserEntity.setProperty('oct_eg:roty', Math.max(-360, Math.min(360, rotY)));
        this.linkedLaserEntity.setProperty('oct_eg:rotx', Math.max(-360, Math.min(360, rotX)));
    }

    updateLaserEntity(loc, direction, longestDistance) {
        if (!this.linkedLaserEntity) return;

        this.linkedLaserEntity?.teleport(loc);

        // Set XY rotations
        const rotY = Math.atan2(direction.z, direction.x) * 180 / Math.PI - 90;
        const rotX = -Math.atan2(direction.y, Math.sqrt(direction.x * direction.x + direction.z * direction.z)) * 180 / Math.PI;

        // Clamp angles to [-360, 360] just in case
        this.linkedLaserEntity.setProperty('oct_eg:roty', Math.max(-360, Math.min(360, rotY)));
        this.linkedLaserEntity.setProperty('oct_eg:rotx', Math.max(-360, Math.min(360, rotX)));

        this.linkedLaserEntity.setProperty('oct_eg:laser_length', Math.max(1, longestDistance));
    }

    removeLaserEntity() {
        if (this.linkedLaserEntity) {
            this.linkedLaserEntity.remove();
            this.linkedLaserEntity = null;
        }
    }

}


// ELECTRO ATTACKS
export class ThunderPunch extends GauntletPrimary {
    constructor(gauntlet, player) {
        super(gauntlet, player);
        this.bounceRange = 3;
        this.bounceAmount = 2;
        this.bounceDamagePercent = 0.2;
        this.molang = new MolangVariableMap();
    }

    onEntityAttack(hurtEntity, damage) {
        super.onEntityAttack(hurtEntity, damage);

        this.gauntlet?.applyStatic(1, hurtEntity);

        const bounceStartLocation = hurtEntity.getHeadLocation();
        const damagePercentBase = this.bounceDamagePercent + (this.gauntlet.upgradeStages.primary >= 2 ? 0.2 : 0);
        const bounceDistance = this.bounceRange + (this.gauntlet.upgradeStages.primary >= 1 ? 1 : 0);
        const bounceAmount = this.bounceAmount + (this.gauntlet.upgradeStages.primary >= 1 ? 1 : 0);

        let entitiesHit = 0;
        for (const entity of attackUtils.getValidTargetsInRange(
            hurtEntity,
            {
                maxDistance: bounceDistance,
                excludedEntities: [this.player]

            })
        ) {
            //world.sendMessage('Bounced to §c' + entity.id);
            const bounceEndLocation = entity.getHeadLocation();
            this.molang.setFloat("offset_x", bounceEndLocation.x - bounceStartLocation.x);
            this.molang.setFloat("offset_y", bounceEndLocation.y - bounceStartLocation.y + Math.random() - 0.5);
            this.molang.setFloat("offset_z", bounceEndLocation.z - bounceStartLocation.z);

            const hasStatic = entity?.getStatic() ?? false;
            const damagePercent = this.gauntlet.upgradeStages.primary >= 3 ? hasStatic ? 1 : damagePercentBase : damagePercentBase;

            attackUtils.applyModifiedDamage(entity, damage * damagePercent, {
                damagingEntity: this.player,
                cause: EntityDamageCause.lightning
            })

            hurtEntity.dimension.spawnParticle("oct_eg:thunder_punch.bounce", bounceStartLocation, this.molang);
            hurtEntity.dimension.playSound("octovon.eg.gauntlet.electro.thunder_punch.zap", entity.location);

            entitiesHit++;
            if (entitiesHit >= bounceAmount) break;
        }
        //world.sendMessage(`Bounced ${entitiesHit-1}`);
    }

}
export class ChargedDash extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 5;
        const mode = "aimed";
        super(gauntlet, player, castTime, 1, cooldownTime, mode);

        this.dashDistance = 7;
        this.baseDamage = 10;
        this.passedCooldownTime = Math.round(cooldownTime * 20);
        this.hitEntities = undefined;

        this.startLocation = null;
        this.endLocation = null;
        this.direction = null;

        this.molang = new MolangVariableMap();
    }

    onStart() {
        super.onStart();
        this.hitEntities = new Set();
    }
    executeAttack() {
        this.cooldownTime = this.gauntlet.upgradeStages.skill1 >= 3 ? this.passedCooldownTime - 60 : this.passedCooldownTime;
        super.executeAttack();

        const viewDir = this.player.getViewDirection();
        const horizontalLength = Math.sqrt(viewDir.x ** 2 + viewDir.z ** 2);
        this.direction = {
            x: viewDir.x / horizontalLength,
            z: viewDir.z / horizontalLength
        };

        this.startLocation = this.player.location;
        const dashDistance = this.dashDistance + (this.gauntlet.upgradeStages.skill1 >= 1 ? 2 : 0);
        this.endLocation = {
            x: this.startLocation.x + this.direction.x * dashDistance,
            y: this.startLocation.y,
            z: this.startLocation.z + this.direction.z * dashDistance
        };

        this.player.addEffect("minecraft:speed", 5, { amplifier: 4, showParticles: false });
        this.player.addEffect("minecraft:resistance", 5, { amplifier: 255, showParticles: false });

        this.player.dimension.playSound("octovon.eg.gauntlet.electro.charged_dash", this.startLocation);

        this.player.playAnimation(
            'animation.oct_eg.util.null_1',
            {
                blendOutTime: 0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_chargedDash`
            }
        );
        this.player.playAnimation(
            'animation.oct_eg.gauntlet.pyro.tp.flame_dash',
            {
                blendOutTime: 0.1,
                nextState: "animation.oct_eg.util.null_1",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_chargedDash`
            }
        );
    }
    onEnd() {
        super.onEnd();

        this.damageEntitiesInPath();
        this.player.dimension.spawnParticle("oct_eg:charged_dash", this.player.location, this.molang);
    }

    whileCasting() {
        super.whileCasting();
        if (!this.startLocation || !this.endLocation || !this.direction) return;

        this.damageEntitiesInPath();

        this.molang.setFloat("dir_x", -this.direction.x);
        this.molang.setFloat("dir_y", 0);
        this.molang.setFloat("dir_z", -this.direction.z);

        this.player.dimension.spawnParticle("oct_eg:charged_dash", this.player.location, this.molang);

        this.movePlayer();
    }

    movePlayer() {
        const dashDistance = this.dashDistance + (this.gauntlet.upgradeStages.skill1 >= 1 ? 2 : 0);
        const speed = dashDistance / this.castTime; // units per tick
        this.player.applyKnockback(this.direction.x, this.direction.z, speed, 0.1);
    }

    damageEntitiesInPath() {

        for (const entity of attackUtils.getValidTargetsFromRays(this.player,
            [
                {
                    location: {
                        x: this.player.location.x,
                        y: this.player.location.y + 1,
                        z: this.player.location.z
                    },
                    direction: { x: this.direction.x, y: 0, z: this.direction.z }
                }
            ],
            {
                maxDistance: 3,
                excludedEntities: Array.from(this.hitEntities)
            }
        )) {
            attackUtils.applyModifiedDamage(
                entity,
                this.baseDamage,
                {
                    cause: EntityDamageCause.contact,
                    damagingEntity: this.player
                }
            );
            entity.dimension.playSound("octovon.eg.gauntlet.electro.thunder_punch.zap", entity.location);
            if (this.gauntlet.upgradeStages.skill1 >= 2) this.gauntlet?.applyStatic(2, entity);
            this.hitEntities.add(entity);
        }
    }


}
export class Discharge extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime);

        this.searchRange = 16;
        this.baseDamage = 3;
        this.baseHealing = 1;
        this.molang = new MolangVariableMap();
    }

    executeAttack() {
        super.executeAttack();
        try {

            this.player.playAnimation(
                'animation.oct_eg.util.gauntlet.electro.discharge',
                {
                    blendOutTime: 0.0,
                    nextState: "_null",
                    stopExpression: "q.all_animations_finished",
                    controller: `_octEgCont_utilShulkerBarrage`
                }
            );

            this.player.dimension.spawnParticle(
                "oct_eg:discharge.sparks",
                this.player.location,
            );
            this.player.dimension.spawnParticle(
                "oct_eg:discharge.glow",
                this.player.location,
            );
            this.player.dimension.playSound("octovon.eg.gauntlet.electro.discharge", this.player.location);
            this.player.runCommand('camerashake add @a[r=5] 0.3 2 positional');
            this.player.runCommand('camerashake add @a[rm=5,r=10] 0.1 2 positional');

            for (const entity of attackUtils.getValidTargetsInRange(this.player, { maxDistance: this.searchRange })) {
                const staticStacks = entity?.getStatic();
                if (!staticStacks) continue;

                const removedStacks = entity?.removeStatic(staticStacks);
                const shouldApplySlowness = this.gauntlet.upgradeStages.skill2 >= 1 ? true : false;
                const damage = this.baseDamage + (this.gauntlet.upgradeStages.skill2 >= 2 ? 1 : 0);
                const shouldHeal = this.gauntlet.upgradeStages.skill2 >= 3 ? true : false;

                attackUtils.applyModifiedDamage(
                    entity,
                    damage * removedStacks,
                    {
                        cause: EntityDamageCause.lightning,
                        damagingEntity: this.player
                    }
                );

                if (shouldApplySlowness) {
                    const slownessLevel = Math.min(removedStacks, 3) - 1;
                    entity.addEffect("minecraft:slowness", 60, { amplifier: slownessLevel, showParticles: true });
                }

                if (shouldHeal) {
                    const healAmount = Math.floor(removedStacks / 2) * this.baseHealing;
                    const healthComp = this.player.getComponent("minecraft:health");
                    const maxHealth = healthComp.effectiveMax;
                    const currentHP = healthComp.currentValue;
                    healthComp.setCurrentValue(Math.min(currentHP + healAmount, maxHealth));
                }

                entity.dimension.playSound("octovon.eg.gauntlet.electro.thunder_punch.zap", entity.location);
                entity.dimension.spawnParticle("oct_eg:discharge.entity_sparks", entity.location)
                this.zapParticles(entity);

                system.runTimeout(() => {
                    this.zapParticles(entity);
                }, 20);

                //world.sendMessage(`Removed ${removedStacks}/${staticStacks} static from ${entity.id}`);
            };

        } catch (e) {
            // console.error(e);
            this.onCancel();
        }
    }

    zapParticles(entity) {
        if (!entity.isValid()) return;

        const bounceEndLocation = this.player.getHeadLocation();
        const bounceStartLocation = entity.getHeadLocation();
        this.molang.setFloat("offset_x", bounceEndLocation.x - bounceStartLocation.x);
        this.molang.setFloat("offset_y", bounceEndLocation.y - bounceStartLocation.y - 0.5);
        this.molang.setFloat("offset_z", bounceEndLocation.z - bounceStartLocation.z);
        entity.dimension.spawnParticle("oct_eg:thunder_punch.bounce", bounceStartLocation, this.molang);
    }
}
export class LightningStrike extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = Math.ceil(1.9 * 20); // The animation length
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime);

        this.animationFrames = [
            {
                time: 0.0, fn: () => {
                    this.updateCamera("minecraft:free", { x: 0, y: 4, z: -4 }, { x: 45, y: "~" }, 0.3, `in_sine`);
                    this.player.addEffect("minecraft:levitation", 30, { amplifier: 2, showParticles: false });
                }
            },
            { time: 0.3, fn: () => this.updateCamera("minecraft:free", { x: 0, y: 5, z: -4 }, { x: 30, y: "~" }, 0.7, `out_sine`) },
            { time: 1.0, fn: () => this.updateCamera("minecraft:free", { x: 0, y: 8, z: -6 }, { x: 35, y: "~" }, 0.4, `out_sine`) },
            { time: 1.3, fn: () => this.attackImpact() },
            {
                time: 1.5, fn: () => {
                    this.player.addEffect("minecraft:slow_falling", 20, { amplifier: 0, showParticles: false });
                }
            }
        ];

        this.currentFrameIndex = 0;
        this.yrot = 0;
        this.location = this.player.location;
        this.baseMaxTargets = 5;
        this.baseRange = 10;
        this.baseSparkDamage = 2;
        this.baseSparkTime = 160;
    }

    onEnd() {
        super.onEnd();

        // Reset attack values
        this.attackStartTick = null;
        this.attackCastTick = null;
        this.currentFrameIndex = 0;

        // Reset player
        this.player.camera.clear();
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, true);
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);
        this.player.playAnimation("animation.oct_eg.util.null_1",
            {
                blendOutTime: 0.2,
                nextState: '_null',
                stopExpression: '0.0',
                controller: `_octEgCont_lightningStrike`
            }
        );
        this.player.playAnimation("animation.oct_eg.util.gauntlet.electro.lightning_strike.end",
            {
                blendOutTime: 0.0,
                nextState: '_null',
                stopExpression: '0',
                controller: `_octEgCont_lightningStrikeUtil`
            }
        );
    }

    onCancel() {
        super.onCancel();

        // Reset attack values
        this.attackStartTick = null;
        this.attackCastTick = null;
        this.currentFrameIndex = 0;

        // Reset player
        this.player.camera.clear();
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, true);
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);

        if (this.player.getEffect("minecraft:levitation")) this.player.removeEffect("minecraft:levitation");
        this.player.playAnimation("animation.oct_eg.util.null_1",
            {
                blendOutTime: 0.2,
                nextState: '_null',
                stopExpression: '0.0',
                controller: `_octEgCont_lightningStrike`
            }
        );
        this.player.playAnimation("animation.oct_eg.util.gauntlet.electro.lightning_strike.end",
            {
                blendOutTime: 0.0,
                nextState: '_null',
                stopExpression: '0',
                controller: `_octEgCont_lightningStrikeUtil`
            }
        );
    }

    updateCamera(preset, pos, rot, eTime, eType) {
        const cmd = `execute positioned ${this.location.x} ${this.location.y} ${this.location.z} rotated ${this.yrot} 0 positioned ^${pos.x}^${pos.y}^${pos.z} run camera @s set ${preset} ease ${eTime} ${eType} pos ~~~ rot ${rot.x} ${rot.y}`;

        this.player.runCommand(cmd);
    }

    executeAttack() {
        super.executeAttack();

        try {
            // Setup the animation
            this.yrot = this.player.getRotation().y;
            this.location = this.player.location;
            this.currentFrameIndex = 0;

            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, false);
            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, false);

            // this.player.addEffect("minecraft:levitation", 40, { amplifier: 5, showParticles: false });
            this.player.addEffect("minecraft:resistance", 40, { amplifier: 255, showParticles: false });

            this.player.playAnimation("animation.oct_eg.gauntlet.electro.tp.lightning_strike",
                {
                    blendOutTime: 0.1,
                    nextState: 'animation.oct_eg.util.null_1',
                    stopExpression: 'q.all_animations_finished',
                    controller: `_octEgCont_lightningStrike`
                }
            );
            this.player.playAnimation("animation.oct_eg.util.gauntlet.electro.lightning_strike.start",
                {
                    blendOutTime: 0.0,
                    nextState: '_null',
                    stopExpression: '0',
                    controller: `_octEgCont_lightningStrikeUtil`
                }
            );
            this.player.dimension.playSound('octovon.eg.gauntlet.electro.lightning_strike', this.player.location);

        } catch (e) {
            //console.error(`§cOctovon: Flaming Meteor Attack Failed:§8\n${e}`);
            this.onEnd();
        }

    }

    whileCasting() {
        super.whileCasting();

        const animSeconds = (system.currentTick - this.attackCastTick) / 20;
        while (
            this.currentFrameIndex < this.animationFrames.length &&
            animSeconds >= this.animationFrames[this.currentFrameIndex].time
        ) {
            this.animationFrames[this.currentFrameIndex].fn();
            this.currentFrameIndex++;
        }

        if (system.currentTick % 4 === 0) {
            this.player.dimension.spawnParticle("oct_eg:discharge.entity_sparks", {
                x: this.player.location.x,
                y: this.player.location.y + 1,
                z: this.player.location.z
            });
        }
    }

    attackImpact() {
        this.player.runCommand('camerashake add @a[r=10] 0.4 2 positional');
        this.player.addEffect("minecraft:fire_resistance", 20, { amplifier: 255, showParticles: false });
        this.player.dimension.spawnParticle("oct_eg:meteor.radial_glow", this.player.location);

        const shouldApplySparks = this.gauntlet.upgradeStages.skill3 >= 1;
        const sparkDamage = this.baseSparkDamage + (this.gauntlet.upgradeStages.skill3 >= 2 ? 1 : 0);
        const attackRange = this.baseRange + (this.gauntlet.upgradeStages.skill3 >= 3 ? 10 : 0);
        const maxTargets = this.baseMaxTargets
            + (this.gauntlet.upgradeStages.skill3 >= 2 ? 2 : 0)
            + (this.gauntlet.upgradeStages.skill3 >= 3 ? 3 : 0);

        let entitiesHit = 0;
        for (const entity of attackUtils.getValidTargetsInRange(this.player, {
            startLocation: this.location,
            maxDistance: attackRange,
        })) {
            if (shouldApplySparks) entity?.addSparks(this.baseSparkTime, sparkDamage, this.player);

            const randomDelay = Math.round(Math.random() * 10);
            system.runTimeout(() => {
                if (!entity || !entity.isValid()) return;
                entity.dimension.spawnEntity('minecraft:lightning_bolt', entity.location);
                entity.dimension.spawnParticle("oct_eg:discharge.entity_sparks", entity.location);
            }, randomDelay);

            entitiesHit++;
            if (entitiesHit >= maxTargets) break;
        }

        if (entitiesHit < maxTargets) {
            const remaining = this.baseMaxTargets - entitiesHit;
            for (let i = 0; i < remaining; i++) {
                const minimumRange = 4;
                const randomRange = Math.random() * (attackRange - minimumRange) + minimumRange;
                const randomAngle = Math.random() * Math.PI * 2;
                const randomLocation = {
                    x: this.location.x + Math.cos(randomAngle) * randomRange,
                    y: this.location.y,
                    z: this.location.z + Math.sin(randomAngle) * randomRange,
                };

                const randomDelay = Math.round(Math.random() * 10);
                system.runTimeout(() => {
                    this.player.dimension.spawnEntity('minecraft:lightning_bolt', randomLocation);
                    this.player.dimension.spawnParticle("oct_eg:discharge.entity_sparks", randomLocation);
                }, randomDelay);
            }
        }

        system.runTimeout(() => this.player.extinguishFire(), 12);
    }
}


// ANEMO ATTACKS
export class QuickPunch extends GauntletPrimary {
    constructor(gauntlet, player) {
        super(gauntlet, player);
        this.maxPunchCount = 3;
        this.quickKnockbackStrength = 1.2;
        this.baseKnockbackStrength = 0.8;
        this.effectsTriggered = false;
        this.molang = new MolangVariableMap();
    }

    onAttack() {
        super.onAttack();

        if (this.punchCount === this.maxPunchCount && !this.effectsTriggered) {
            this.effectsTriggered = true; // Mark as triggered

            const viewDir = this.player.getViewDirection();
            this.molang.setFloat("direction_x", viewDir.x);
            this.molang.setFloat("direction_y", viewDir.y);
            this.molang.setFloat("direction_z", viewDir.z);
            this.player.dimension.playSound("octovon.eg.gauntlet.anemo.knockback_big", this.player.location);
            this.player.dimension.spawnParticle(
                "oct_eg:knockback_punch",
                this.player.getHeadLocation(),
                this.molang
            )
            this.player.dimension.spawnParticle(
                "oct_eg:knockback_punch.smoke",
                this.player.getHeadLocation(),
                this.molang
            )
        }

        if (this.punchCount !== this.maxPunchCount) {
            this.effectsTriggered = false;
        }

    }
    onEntityAttack(hurtEntity, damage) {
        super.onEntityAttack(hurtEntity, damage);

        const viewDir = this.player.getViewDirection();
        const upgradedKnockback = this.gauntlet.upgradeStages.primary >= 1 ? true : false;
        const shouldApplyLevitation = this.gauntlet.upgradeStages.primary >= 2 ? true : false;
        const shouldApplyWindCharged = this.gauntlet.upgradeStages.primary >= 3 ? true : false;

        if (this.punchCount !== this.maxPunchCount) {
            if (upgradedKnockback) this.applyKnockback(hurtEntity, viewDir, this.baseKnockbackStrength);

            this.player.dimension.playSound("octovon.eg.gauntlet.anemo.knockback_small", this.player.location);

        } else {
            this.applyKnockback(hurtEntity, viewDir, upgradedKnockback ? this.quickKnockbackStrength + 0.5 : this.quickKnockbackStrength);

            if (shouldApplyLevitation) {
                hurtEntity.addEffect("minecraft:levitation", 40, { amplifier: 0, showParticles: true });
                hurtEntity.dimension.spawnParticle("oct_eg:double_jump.start", hurtEntity.location);
            }
        }
        if (shouldApplyWindCharged) hurtEntity.addEffect("minecraft:wind_charged", 100, { amplifier: 0, showParticles: true });

    }

    applyKnockback(entity, direction, hstrength, vstrength = 0.3) {
        entity.applyKnockback(direction.x, direction.z, hstrength, vstrength);
    }

}
export class AirSlash extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 4;

        super(gauntlet, player, castTime, maxCharges, cooldownTime, "aimed");
        this.lastReplenishTime = 0;
        this.chargeReplenishTime = 100;
        this.passedCooldownTime = Math.round(cooldownTime * 20);
    }
    executeAttack() {
        this.cooldownTime = this.gauntlet.fullSetBonus ? Math.ceil(this.passedCooldownTime * 0.8) : this.passedCooldownTime;
        super.executeAttack();

        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.anemo.air_slash',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_utilAirSlash`
            }
        );

        system.run(() => {
            this.player.dimension.playSound(
                "octovon.eg.gauntlet.anemo.air_slash",
                this.player.location,
            )
            const vewDir = this.player.getViewDirection();
            const proj = new projectiles.AirSlashProjectile(this.player, vewDir, this.gauntlet);
            proj.shootProjectile();
        })

        this.lastReplenishTime = system.currentTick;
    }

    checkReplenishCharge() {
        if (this.isOnCooldown()) return;

        if (this.remainingCharges < this.maxCharges) {
            const ticksSinceLastAttack = system.currentTick - this.lastReplenishTime;

            if (ticksSinceLastAttack >= this.chargeReplenishTime) {
                this.remainingCharges++;
                this.lastReplenishTime = system.currentTick; // Reset timer for next replenish

                this.player.playSound(
                    "octovon.eg.gauntlet.anemo.knockback_small",
                    {
                        pitch: 0.5,
                        volume: 0.2
                    }
                )
                this.player.dimension.spawnParticle(
                    "oct_eg:double_jump.single",
                    this.player.location,
                )
                this.player.dimension.spawnParticle(
                    "oct_eg:double_jump.dots",
                    this.player.location,
                )
                //world.sendMessage(`§aAir Slash charge replenished!`);
            }
        }
    }

    triggerCooldown() {
        this.gauntlet.initializeAirSlashCharges();
        super.triggerCooldown();
    }
}
export class Tornado extends GauntletChargedAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 1;

        super(gauntlet, player, castTime, maxCharges, cooldownTime, []);
        this.passedCooldownTime = Math.round(cooldownTime * 20);
    }
    setAttackSpecificProperties() {
        this.hasAttackPreviewLogic = true;
        this.attackPreviewTypeId = "oct_eg:attack_cast_preview.tornado";
        this.castingRange = 20;
        this.canCastOnLiquid = true;
        this.baseLifetime = 10;
        this.basePullRange = 10;
        this.baseDamage = 1;
    }
    onStart() {
        this.chargeStages = this.gauntlet.upgradeStages.skill2 >= 2
            ? [1, this.gauntlet.upgradeStages.skill2 >= 3 ? 30 : 60]
            : [];
        super.onStart();
    }
    whileAiming() {
        super.whileAiming();
        this.handleAttackPreview();
    }
    chargedAttackSuccess() {
        this.cooldownTime = this.gauntlet.fullSetBonus ? Math.ceil(this.passedCooldownTime * 0.8) : this.passedCooldownTime;
        super.chargedAttackSuccess();

        const lifeTime = this.baseLifetime + (this.gauntlet.upgradeStages.skill2 >= 1 ? 5 : 0);

        const pullRange = this.basePullRange + (this.currentChargeStage >= 2 ? 5 : 0);
        const damage = this.baseDamage + (this.currentChargeStage >= 2 ? 1 : 0);

        const loc = this.linkedAttackPreview?.location;
        const tornado = this.player.dimension.spawnEntity('oct_eg:tornado', loc);

        tornado.setDynamicProperty("oct_eg:time_created", system.currentTick);
        tornado.setDynamicProperty("oct_eg:life_time", lifeTime);
        tornado.setDynamicProperty("oct_eg:pull_range", pullRange);
        tornado.setDynamicProperty("oct_eg:damage", damage);

        if (this.currentChargeStage >= 2) tornado.setProperty("oct_eg:upgraded", true);

        tornado.getComponent("minecraft:tameable")?.tame(this.player);

        //world.sendMessage(`§6Damage: ${damage} §6Range: ${pullRange} §6Life Time: ${lifeTime}`);

        this.player.dimension.spawnParticle(
            "oct_eg:tornado.spin",
            loc
        );
        this.player.dimension.playSound(
            "octovon.eg.gauntlet.anemo.tornado.cast",
            this.player.location,
        )
        const soundKey = this.gauntlet.upgradeStages.skill2 >= 1
            ? "octovon.eg.gauntlet.anemo.tornado.long"
            : "octovon.eg.gauntlet.anemo.tornado.short";
        this.player.dimension.playSound(
            soundKey,
            loc,
        )
        this.player.dimension.playSound(
            soundKey,
            {
                x: loc.x,
                y: loc.y + 5,
                z: loc.z
            },
            {
                pitch: 0.9
            }
        )

        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.anemo.tornado',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_utilTornado`
            }
        );

        this.removeAttackPreview();
    }
    updateAttackPreviewAnimations() {
        super.updateAttackPreviewAnimations();
        const chargeStage = this.gauntlet.upgradeStages.skill2 >= 2 ? this.currentChargeStage : 1;
        this.linkedAttackPreview?.playAnimation(`animation.oct_eg.tornado_preview.charging.stage_${chargeStage}`,
            {
                blendOutTime: 0,
                nextState: '_null',
                stopExpression: '0.0',
                controller: `_octEgCont_atPrevStage`
            }
        );
    }
}
export class Disengage extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 1;

        super(gauntlet, player, castTime, maxCharges, cooldownTime);
        this.passedCooldownTime = Math.round(cooldownTime * 20);
    }

    executeAttack() {
        this.cooldownTime = this.gauntlet.fullSetBonus ? Math.ceil(this.passedCooldownTime * 0.8) : this.passedCooldownTime;
        super.executeAttack();
        //world.sendMessage(`§6Disengage!`);

        this.player.runCommand('camerashake add @a[r=5] 0.3 0.5 positional');
        this.player.addEffect(
            "minecraft:slow_falling",
            100,
            {
                amplifier: 0,
                showParticles: false
            }
        )
        this.player.dimension.spawnParticle(
            "oct_eg:disengage",
            this.player.location
        );
        this.player.playAnimation(
            'animation.oct_eg.gauntlet.anemo.tp.jump',
            {
                blendOutTime: 0.1,
                nextState: "animation.oct_eg.util.null_1",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_disengage`
            }
        );
        this.player.dimension.playSound("octovon.eg.gauntlet.anemo.knockback_big", this.player.location, { volume: 3 });
        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);

        const shouldKnock = this.gauntlet.upgradeStages.skill3 >= 1 ? true : false;
        if (shouldKnock) disengageKnockback(this.player);

        attackUtils.addDirectionalKnockback(this.player, 0.0, 1.5);
        this.gauntlet.disengageInAirActive = true;
        this.gauntlet.disengageLastExecutedTime = system.currentTick;
    }
}
export function disengageKnockback(player) {
    for (const entity of attackUtils.getValidTargetsInRange(player, {
        maxDistance: 4
    })) {
        const delta = attackUtils.VOP.sub(entity.location, player.location);
        const distance = Math.sqrt(delta.x ** 2 + delta.y ** 2 + delta.z ** 2);

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

        entity.applyKnockback(pushDirection.x, pushDirection.z, 3.5, 0.6);
    }
}
export class DisengageLand extends GauntletAttack {
    constructor(gauntlet, player) {
        const castTime = 15;
        const cooldownTime = 1;
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime);
        this.disengageLandRunId = null;
    }

    onCancel() {
        super.onCancel();
        system.clearRun(this.disengageLandRunId);
        this.gauntlet.disengageInAirActive = false;
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);
        this.player.playAnimation(
            'animation.oct_eg.util.null_2',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_disengageFall`
            }
        );
    }

    onEnd() {
        super.onEnd();
        system.clearRun(this.disengageLandRunId);
        this.gauntlet.disengageInAirActive = false;
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);
        this.player.playAnimation(
            'animation.oct_eg.util.null_2',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_disengageFall`
            }
        );
    }

    executeAttack() {
        super.executeAttack();

        try {
            const rayDir = { x: 0, y: -1, z: 0 };
            const startLocation = this.player.location;
            const rayLength = 32;

            const rayHit = this.player.dimension.getBlockFromRay(startLocation, rayDir, {
                maxDistance: rayLength,
                includeLiquidBlocks: true,
                includePassableBlocks: false,
            });

            let groundLocation = undefined;
            if (rayHit?.block) {
                groundLocation = rayHit.block.above().bottomCenter();
            }

            this.player.dimension.spawnParticle(
                "oct_eg:disengage",
                this.player.location
            );
            this.player.dimension.playSound("octovon.eg.gauntlet.anemo.knockback_big", this.player.location, { volume: 3 });
            this.player.dimension.playSound("octovon.eg.gauntlet.anemo.disengage.fall", this.player.location, { volume: 1 });
            this.player.playAnimation(
                'animation.oct_eg.util.null_2',
                {
                    blendOutTime: 0.0,
                    nextState: "_null",
                    stopExpression: "0",
                    controller: `_octEgCont_disengageFall`
                }
            );
            this.player.playAnimation(
                'animation.oct_eg.gauntlet.anemo.tp.disengage.slam_start',
                {
                    blendOutTime: 0.1,
                    nextState: "animation.oct_eg.util.null_2",
                    stopExpression: "q.all_animations_finished || v.is_first_person",
                    controller: `_octEgCont_disengageFall`
                }
            );
            this.player.playAnimation(
                'animation.oct_eg.util.null_1',
                {
                    blendOutTime: 0.0,
                    nextState: "animation.oct_eg.gauntlet.anemo.tp.disengage.slam_start",
                    stopExpression: "!v.is_first_person",
                    controller: `_octEgCont_disengageFall`
                }
            );
            this.player.addEffect("minecraft:resistance", 10, { amplifier: 255, showParticles: false });
            this.player.addEffect("minecraft:slow_falling", 10, { amplifier: 255, showParticles: false });
            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, false);

            this.disengageLandRunId = system.runInterval(() => {
                try {
                    // knock player
                    this.player.applyKnockback(0, 0, 0, -1.5);

                    // check if reached the end
                    if (groundLocation && this.player.location.y <= groundLocation.y) {
                        this.hitGround(groundLocation);
                    };

                } catch {
                    this.onCancel();
                }

            }, 1);

        } catch (e) {
            //world.sendMessage(`§cDisengage Land Failed! ${e}`);
            this.onCancel();
        }
    }

    hitGround(groundLocation) {
        //world.sendMessage(`§aDisengage Land!`);
        this.player.playAnimation(
            'animation.oct_eg.gauntlet.anemo.tp.disengage.slam_end',
            {
                blendOutTime: 0.1,
                nextState: "animation.oct_eg.util.null_2",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_disengageSlam`
            }
        );
        this.player.dimension.playSound("octovon.eg.gauntlet.anemo.disengage.land", this.player.location);
        this.player.dimension.playSound("octovon.eg.gauntlet.anemo.knockback_big", this.player.location, { pitch: 0.7 });
        this.player.runCommand('camerashake add @a[r=6] 0.3 1.0 positional');
        this.player.dimension.spawnParticle(
            "oct_eg:disengage.slam",
            groundLocation
        )
        const shouldApplyDamage = this.gauntlet.upgradeStages.skill3 >= 3 ? true : false;
        for (const entity of attackUtils.getValidTargetsInRange(this.player, {
            maxDistance: 5
        })) {
            const delta = attackUtils.VOP.sub(entity.location, this.player.location);
            const distance = Math.sqrt(delta.x ** 2 + delta.y ** 2 + delta.z ** 2);

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

            entity.applyKnockback(pushDirection.x, pushDirection.z, 3.5, 0.8);

            if (!shouldApplyDamage) continue;
            attackUtils.applyModifiedDamage(entity, 5, { cause: EntityDamageCause.entityAttack, damagingEntity: this.player });

        }
        this.onEnd();
    }
}


// NECRO ATTACKS
export class LeachingPunch extends GauntletPrimary {
    constructor(gauntlet, player) {
        super(gauntlet, player);
        this.maxPunchCount = 2;
        this.molang = new MolangVariableMap();
        this.lifeStealPunchCount = 0;
        this.maxLifeStealPunchCount = 4;
        // this.lastLifeStealPunchTime = 0;
        this.effectsTriggered = false;
        this.baseLifeSteal = 1;
    }

    onEntityAttack(hurtEntity, damage) {
        super.onEntityAttack(hurtEntity, damage);

        if (this.lifeStealPunchCount !== this.maxLifeStealPunchCount) return;

        this.player.playAnimation(`animation.oct_eg.util.gauntlet.common.punch_id.3`,
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: `0`,
                controller: `_octEgCont_punchCounter`
            }
        );

        let lifeStealAmount = this.baseLifeSteal + (this.gauntlet.upgradeStages.primary >= 1 ? 1 : 0);
        const shouldApplyWithering = this.gauntlet.upgradeStages.primary >= 2 ? true : false;
        const shouldStealFromNearby = this.gauntlet.upgradeStages.primary >= 3 ? true : false;

        this.lifeSteaParticleTrail(hurtEntity);

        if (shouldApplyWithering) {
            hurtEntity.addEffect("minecraft:wither", 100, { amplifier: 1, showParticles: true });
        }

        if (shouldStealFromNearby) {
            const maxEntities = 3;
            let entityCount = 0;

            hurtEntity.dimension.spawnParticle("oct_eg:life_teal_burst.smoke", hurtEntity.location);

            for (const entity of attackUtils.getValidTargetsInRange(this.player, {
                maxDistance: 2,
                startLocation: hurtEntity.location,
                excludedEntities: [hurtEntity],
                excludePetsOfExcludedEntities: false
            })) {
                lifeStealAmount += 1;
                attackUtils.applyModifiedDamage(entity, 1, { cause: EntityDamageCause.wither, damagingEntity: this.player });
                this.lifeSteaParticleTrail(entity);
                //world.sendMessage(`Entity: ${entity.typeId}`);

                entityCount += 1;
                if (entityCount >= maxEntities) break;
            }
        }


        this.gauntlet.lifeSteal(lifeStealAmount);
    }

    iteratePunchCount() {
        super.iteratePunchCount();

        if (this.lifeStealPunchCount >= this.maxLifeStealPunchCount) {
            this.lifeStealPunchCount = 0;
        }
        this.lifeStealPunchCount += 1;
    }

    lifeSteaParticleTrail(hurtEntity) {
        const startLocation = this.player.getHeadLocation();
        const endLocation = hurtEntity.getHeadLocation();
        this.molang.setFloat("offset_x", startLocation.x - endLocation.x);
        this.molang.setFloat("offset_y", startLocation.y - endLocation.y);
        this.molang.setFloat("offset_z", startLocation.z - endLocation.z);
        this.player.dimension.spawnParticle("oct_eg:life_steal.trail", endLocation, this.molang);
    }

}
export class UndeadScythes extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 1;

        super(gauntlet, player, castTime, maxCharges, cooldownTime);
        this.baseDamageRange = 6;
        this.baseDamage = 15;
        this.passedCooldownTime = Math.round(cooldownTime * 20);
    }
    executeAttack() {
        this.cooldownTime = this.gauntlet.upgradeStages.skill3 >= 3 ? this.passedCooldownTime - 100 : this.passedCooldownTime;
        super.executeAttack();

        const scytheLoc = {
            x: this.player.location.x,
            y: this.player.location.y + 1.0,
            z: this.player.location.z
        };
        const scythe = this.player.dimension.spawnEntity(
            "oct_eg:undead_scythe",
            scytheLoc
        )

        const upgradedRange = this.gauntlet.upgradeStages.skill3 >= 1 ? true : false;
        if (upgradedRange) scythe.setProperty('oct_eg:size', 2);
        const damageRange = this.baseDamageRange + (upgradedRange ? 2 : 0);

        const shouldHeal = this.gauntlet.upgradeStages.skill3 >= 2 ? true : false;
        const yRange = 2;
        let healAmount = 0;

        for (const entity of attackUtils.getValidTargetsInRange(this.player, {
            maxDistance: damageRange,
            startLocation: this.player.location
        })) {
            if (entity.location.y < scytheLoc.y - yRange || entity.location.y > scytheLoc.y + yRange) continue;

            attackUtils.applyModifiedDamage(entity,
                this.baseDamage,
                { cause: EntityDamageCause.entityAttack, damagingEntity: this.player }
            );
            healAmount += 1;
        }

        if (shouldHeal && healAmount) this.gauntlet.lifeSteal(healAmount);

        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.necro.undead_scythe',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_utilUndeadScythe`
            }
        );
        this.player.dimension.playSound(
            "octovon.eg.gauntlet.necro.undead_scythe",
            this.player.location,
        )

    }

}
export class PutridRibs extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 1;

        super(gauntlet, player, castTime, maxCharges, cooldownTime, "aimed");
        this.baseRibDamage = 5;
        this.baseRibDamageRange = 2;
        this.baseStunDuration = 60;

        this.baseAttackRange = 10;
        this.ribSpacing = 2;
        this.spawnDelayTicks = 4;
        this.hitEntities = null;
    }
    executeAttack() {
        super.executeAttack();
        this.hitEntities = new Set();
        const attackRange = this.baseAttackRange + (this.gauntlet.upgradeStages.skill2 >= 2 ? 2 : 0);

        const startLocation = this.player.location;
        const viewDir = this.player.getViewDirection();
        const initRotation = this.player.getRotation();

        const horizontalLength = Math.sqrt(viewDir.x ** 2 + viewDir.z ** 2);
        const direction = {
            x: viewDir.x / horizontalLength,
            z: viewDir.z / horizontalLength
        };

        const spawnedRibs = Math.floor(attackRange / this.ribSpacing);
        let previousY = null;

        for (let i = 0; i < spawnedRibs; i++) {
            const offsetX = direction.x * i * this.ribSpacing;
            const offsetZ = direction.z * i * this.ribSpacing;

            const rayOrigin = {
                x: startLocation.x + offsetX,
                y: startLocation.y + 5, // Start raycast from above
                z: startLocation.z + offsetZ
            };

            const rayDirection = { x: 0, y: -1, z: 0 };

            let surfaceY;

            try {
                const hit = this.player.dimension.getBlockFromRay(rayOrigin, rayDirection, { maxDistance: 10, includePassableBlocks: false });
                if (!hit?.block) break;

                surfaceY = hit.block.above().bottomCenter().y;
            } catch (e) {
                //world.sendMessage(`§cPutrid Ribs Raycast Failed! ${e}`);
                break; // Stop if ray fails
            }

            if (previousY !== null) {
                const incline = Math.abs(surfaceY - previousY);
                if (incline > 2) break; // Stop propagation if incline too steep
            }

            previousY = surfaceY;

            const spawnPosition = {
                x: rayOrigin.x,
                y: surfaceY,
                z: rayOrigin.z
            };

            const spawnDelay = i * this.spawnDelayTicks;
            const flipped = i % 2 === 1;

            system.runTimeout(() => {
                this.spawnRib(spawnPosition, initRotation, flipped);
            }, spawnDelay);
        }

        system.runTimeout(() => {
            this.countHits();
        }, (spawnedRibs * this.spawnDelayTicks) + 1);

        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.necro.putrid_ribs',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_utilPutridRibs`
            }
        );
    }

    spawnRib(spawnPosition, initRotation, flipped = false) {
        const rib = this.player.dimension.spawnEntity(
            "oct_eg:putrid_ribs",
            spawnPosition
        );
        const randomRot = initRotation.y + (Math.random() * 20 - 10);
        rib.setProperty("oct_eg:ryinit", parseInt(randomRot));
        rib.setProperty("oct_eg:flipped", flipped);

        const ribDamage = this.baseRibDamage + (this.gauntlet.upgradeStages.skill2 >= 2 ? 2 : 0);
        const stunDur = this.baseStunDuration + (this.gauntlet.upgradeStages.skill2 >= 1 ? 40 : 0);

        for (const entity of attackUtils.getValidTargetsInRange(this.player, {
            maxDistance: this.baseRibDamageRange,
            startLocation: spawnPosition
        })) {
            attackUtils.applyModifiedDamage(entity,
                ribDamage,
                { cause: EntityDamageCause.contact, damagingEntity: this.player }
            );
            entity?.addStun(stunDur);
            this.hitEntities.add(entity);
        }


    }

    countHits() {
        //world.sendMessage(`§6Putrid Ribs Hit Count: ${this.hitEntities.size}`);
        if (this.hitEntities.size >= 3 && this.gauntlet.upgradeStages.skill2 >= 3) {
            this.gauntlet.addSouls(1);
        }
    }

}
export class SoulSnipe extends GauntletChargedAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 1;

        super(gauntlet, player, castTime, maxCharges, cooldownTime, []);
        this.passedCooldownTime = Math.round(cooldownTime * 20);
    }
    onStart() {
        if (this.gauntlet.soulMeter < 1) {
            // world.sendMessage(`§cNot enough souls!`);
            this.player.playSound(
                "extinguish.candle",
                this.player.location,
            );
            return;
        }

        this.chargeStages = this.gauntlet.upgradeStages.skill1 >= 1
            ? [1, 20, 20]
            : [];
        super.onStart();
    }
    chargedAttackSuccess() {
        if (this.gauntlet.soulMeter < 1) {
            this.onCancel();
            return;
        }

        this.cooldownTime = this.passedCooldownTime;
        super.chargedAttackSuccess();

        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.necro.putrid_ribs',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_utilSoulSnipe`
            }
        );

        const viewDir = this.player.getViewDirection();
        const molang = new MolangVariableMap();
        molang.setFloat("direction_x", viewDir.x);
        molang.setFloat("direction_y", viewDir.y);
        molang.setFloat("direction_z", viewDir.z);
        this.player.dimension.spawnParticle(
            "oct_eg:soul_snipe.shoot",
            this.player.getHeadLocation(),
            molang
        )

        system.run(() => {
            this.player.dimension.playSound(
                "octovon.eg.gauntlet.necro.soul_snipe.cast",
                this.player.location,
            )
            const vewDir = this.player.getViewDirection();
            const proj = new projectiles.SoulSnipeProjectile(this.player, vewDir, this.gauntlet, this.currentChargeStage);
            proj.shootProjectile();
        })

        this.gauntlet.removeSouls(1);

    }
}

// HYDRO ATTACKS
export class AquaticDash extends GauntletAttack {
    constructor(gauntlet, player) {
        const castTime = 10;
        const cooldownTime = 3;
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime);

        this.molang = new MolangVariableMap();
        this.direction = { x: 0, y: 0, z: 0 };
        this.baseDashDamage = 10;
        this.dashStrength = 1.2;
        this.hitEntities = null;
    }

    onEnd() {
        super.onEnd();
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);

    }
    onCancel() {
        super.onCancel();
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);

    }
    executeAttack() {
        super.executeAttack();

        this.hitEntities = new Set();
        this.direction = this.player.getViewDirection();

        try {
            this.player.dimension.playSound(
                "item.trident.riptide_3",
                this.player.location,
            );

            this.player.playAnimation(
                'animation.oct_eg.util.gauntlet.hydro.aquatic_dash',
                {
                    blendOutTime: 0.0,
                    nextState: "_null",
                    stopExpression: "q.all_animations_finished",
                    controller: `_octEgCont_utilAquaticDash`
                }
            );
            this.player.addEffect("minecraft:resistance", 10, { amplifier: 255, showParticles: false });
            this.player.addEffect("minecraft:slow_falling", 10, { amplifier: 255, showParticles: false });
            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, false);

        } catch (e) {
            this.onCancel();
        }
    }
    whileCasting() {
        super.whileCasting();
        const shouldDamage = this.gauntlet.upgradeStages.passive >= 2;

        // knockback towards the direction
        const length = Math.sqrt(this.direction.x * this.direction.x + this.direction.y * this.direction.y + this.direction.z * this.direction.z);
        const normDir = {
            x: this.direction.x / length,
            y: this.direction.y / length,
            z: this.direction.z / length
        };
        const horizontalStrength = this.dashStrength;
        const verticalStrength = normDir.y * this.dashStrength * 1.0;
        this.player.applyKnockback(normDir.x, normDir.z, horizontalStrength, verticalStrength);

        // Damage entities
        if (shouldDamage) this.damageEntitiesInPath();

        // Particle effects
        this.molang.setFloat("direction_x", -normDir.x);
        this.molang.setFloat("direction_y", -normDir.y);
        this.molang.setFloat("direction_z", -normDir.z);
        this.player.dimension.spawnParticle(
            "oct_eg:knockback_punch",
            this.player.getHeadLocation(),
            this.molang
        )

    }
    damageEntitiesInPath() {
        for (const entity of attackUtils.getValidTargetsFromRays(this.player,
            [
                {
                    location: {
                        x: this.player.location.x,
                        y: this.player.location.y + 1,
                        z: this.player.location.z
                    },
                    direction: this.direction
                }
            ],
            {
                maxDistance: 2,
                excludedEntities: Array.from(this.hitEntities),
            }
        )) {
            attackUtils.applyModifiedDamage(
                entity,
                this.baseDashDamage,
                {
                    cause: EntityDamageCause.contact,
                    damagingEntity: this.player
                }
            );
            this.hitEntities.add(entity);
        }
    }
}
export class FrozenPunch extends GauntletPrimary {
    constructor(gauntlet, player) {
        super(gauntlet, player);
        this.maxPunchCount = 2;
        this.baseSlownessDuration = 20;
    }

    onEntityAttack(hurtEntity, damage) {
        super.onEntityAttack(hurtEntity, damage);

        // Full set bonus
        const slownessModifier = this.gauntlet.fullSetBonus ? 1.5 : 1.0;

        let slownessDuration = this.baseSlownessDuration
            + (this.gauntlet.upgradeStages.primary >= 1 ? 20 : 0);
        slownessDuration = Math.round(slownessDuration * slownessModifier);

        const slownessLevel = this.gauntlet.upgradeStages.primary >= 2 ? 1 : 0;
        const enemyIsAlreadySlowed = hurtEntity?.getEffect("minecraft:slowness");

        hurtEntity.addEffect("minecraft:slowness", slownessDuration, { amplifier: slownessLevel, showParticles: true });
        if (enemyIsAlreadySlowed && this.gauntlet.upgradeStages.primary >= 3 ? 1 : 0) hurtEntity.addEffect("minecraft:weakness", 40, { amplifier: 0, showParticles: true });

        const loc = hurtEntity.getHeadLocation();
        hurtEntity.dimension.spawnParticle(
            "oct_eg:ice_damage",
            {
                x: loc.x,
                y: loc.y - 0.5,
                z: loc.z
            },
        )
        hurtEntity.dimension.playSound(
            "octovon.eg.gauntlet.hydro.liquid_punch.impact",
            hurtEntity.location,
            { volume: 0.5 }
        )
        hurtEntity.dimension.playSound(
            "octovon.eg.gauntlet.hydro.liquid_punch.ice",
            hurtEntity.location,
            { volume: 0.5 + slownessLevel * 0.2 }
        )

    }

}
export class IceSpikes extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 3;

        super(gauntlet, player, castTime, maxCharges, cooldownTime, "aimed");
        this.passedCooldownTime = Math.round(cooldownTime * 20);
    }
    executeAttack() {
        // Passive upgrade 3 reduces cooldown by 20%
        this.cooldownTime = this.gauntlet.upgradeStages.passive >= 3
            ? Math.ceil(this.passedCooldownTime * 0.8)
            : this.passedCooldownTime;

        super.executeAttack();


        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.anemo.air_slash',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_utilAirSlash`
            }
        );

        system.run(() => {
            this.player.dimension.playSound(
                "octovon.eg.gauntlet.hydro.ice_spike.cast",
                this.player.location,
                {
                    volume: 0.8
                }
            )
            const vewDir = this.player.getViewDirection();
            const proj = new projectiles.IceSpikeProjectile(this.player, vewDir, this.gauntlet);
            proj.shootProjectile();
        })
    }

    triggerCooldown() {
        this.gauntlet.initializeIceSpikeCharges();
        super.triggerCooldown();
    }
}
export class SlipNSlide extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 100;
        const maxCharges = 1;

        super(gauntlet, player, castTime, maxCharges, cooldownTime);
        this.passedCooldownTime = Math.round(cooldownTime * 20);

        this.emergeKnockbackRange = 4;
        this.castKnockbackRange = 3;
        this.emergeDamage = 10;
    }

    whileCasting() {
        super.whileCasting();
        this.player.addEffect("minecraft:speed", 12, { amplifier: 2, showParticles: false });
        this.player.addEffect("minecraft:resistance", 12, { amplifier: 254, showParticles: false });
        this.player.addEffect("minecraft:weakness", 12, { amplifier: 254, showParticles: false });

        const moveVec = this.player.inputInfo.getMovementVector();
        const isMoving = Math.abs(moveVec.x) > 0.01 || Math.abs(moveVec.y) > 0.01;
        if (system.currentTick % 10 === 0 && isMoving) this.player.dimension.playSound("liquid.water", this.player.location);
    }

    onEnd() {
        super.onEnd();
        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.hydro.sns.inactive',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_utilSNS`
            }
        );
        this.player.playAnimation("animation.oct_eg.gauntlet.hydro.tp.slip_n_slide.end",
            {
                blendOutTime: 0.1,
                nextState: 'animation.oct_eg.util.null_1',
                stopExpression: 'q.all_animations_finished',
                controller: `_octEgCont_SlipNSlide`
            }
        );
        this.player.dimension.spawnParticle("oct_eg:sns_attack.splash", {
            x: this.player.location.x,
            y: this.player.location.y + 0.1,
            z: this.player.location.z
        });
        this.player.dimension.playSound("bubble.upinside", this.player.location);

        const shouldDamage = this.gauntlet.upgradeStages.skill3 >= 1 ? true : false;
        if (shouldDamage) this.impact();

        system.runTimeout(() => {
            this.player.camera.clear();
        }, 10);
    }
    onCancel() {
        super.onCancel();
        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.hydro.sns.inactive',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_utilSNS`
            }
        );
        this.player.playAnimation("animation.oct_eg.gauntlet.hydro.tp.slip_n_slide.end",
            {
                blendOutTime: 0.1,
                nextState: 'animation.oct_eg.util.null_1',
                stopExpression: 'q.all_animations_finished',
                controller: `_octEgCont_SlipNSlide`
            }
        );
        this.player.dimension.spawnParticle("oct_eg:sns_attack.splash", this.player.location);
        this.player.dimension.playSound("bubble.upinside", this.player.location);

        system.runTimeout(() => {
            this.player.camera.clear();
        }, 10);
    }

    executeAttack() {
        this.cooldownTime = this.gauntlet.fullSetBonus ? Math.ceil(this.passedCooldownTime * 0.8) : this.passedCooldownTime;
        this.castTime = this.gauntlet.upgradeStages.skill3 >= 3 ? 200 : 100;
        super.executeAttack();

        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.hydro.sns.active',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "0",
                controller: `_octEgCont_utilSNS`
            }
        );
        this.player.playAnimation("animation.oct_eg.gauntlet.hydro.tp.slip_n_slide.hidden",
            {
                blendOutTime: 0.0,
                nextState: '_null',
                stopExpression: '0',
                controller: `_octEgCont_SlipNSlide`
            }
        );
        this.player.playAnimation("animation.oct_eg.gauntlet.hydro.tp.slip_n_slide.start",
            {
                blendOutTime: 0.0,
                nextState: 'animation.oct_eg.gauntlet.hydro.tp.slip_n_slide.hidden',
                stopExpression: 'q.all_animations_finished',
                controller: `_octEgCont_SlipNSlide`
            }
        );

        this.player.dimension.playSound("bubble.downinside", this.player.location);
        this.player.dimension.playSound("bubble.down", this.player.location);
        system.runTimeout(() => {
            this.player.dimension.spawnParticle("oct_eg:sns_attack.splash", {
                x: this.player.location.x,
                y: this.player.location.y + 0.1,
                z: this.player.location.z
            });
        }, 8);

        this.player.camera.setCamera('oct_eg:follow_orbit_sns');

        const shouldKnock = this.gauntlet.upgradeStages.skill3 >= 2 ? true : false;
        if (shouldKnock) this.snsKnockback();

    }

    impact() {
        //world.sendMessage(`§aSlip N Slide Impact!`);
        this.player.dimension.playSound("octovon.eg.gauntlet.hydro.sns.impact", this.player.location);
        for (const entity of attackUtils.getValidTargetsInRange(this.player, {
            maxDistance: this.emergeKnockbackRange,
        })) {
            attackUtils.applyModifiedDamage(entity, this.emergeDamage, { cause: EntityDamageCause.contact, damagingEntity: this.player });
        }
    }

    snsKnockback() {
        for (const entity of attackUtils.getValidTargetsInRange(this.player, {
            maxDistance: 2
        })) {
            const delta = attackUtils.VOP.sub(entity.location, this.player.location);
            const distance = Math.sqrt(delta.x ** 2 + delta.y ** 2 + delta.z ** 2);

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

            entity.applyKnockback(pushDirection.x, pushDirection.z, 3.0, 0.5);
        }
    }
}
export class Tsunami extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 1;

        super(gauntlet, player, castTime, maxCharges, cooldownTime, "aimed");
        this.passedCooldownTime = Math.round(cooldownTime * 20);
    }
    executeAttack() {
        // Passive upgrade 3 reduces cooldown by 20%
        this.cooldownTime = this.gauntlet.upgradeStages.passive >= 3
            ? Math.ceil(this.passedCooldownTime * 0.8)
            : this.passedCooldownTime;

        super.executeAttack();

        this.player.playAnimation(
            'animation.oct_eg.util.gauntlet.necro.putrid_ribs',
            {
                blendOutTime: 0.0,
                nextState: "_null",
                stopExpression: "q.all_animations_finished",
                controller: `_octEgCont_utilPutridRibs`
            }
        );

        system.run(() => {
            this.player.dimension.playSound(
                "octovon.eg.gauntlet.hydro.tsunami.cast",
                this.player.location,
                {
                    volume: 1.0
                }
            )
            const vewDir = this.player.getViewDirection();
            const castDir = {
                x: vewDir.x,
                y: 0,
                z: vewDir.z
            }
            const proj = new projectiles.TsunamiProjectile(this.player, castDir, this.gauntlet);
            proj.shootProjectile();
        })
    }
}

// GEO ATTACKS
export class HeavyPunch extends GauntletPrimary {
    constructor(gauntlet, player) {
        super(gauntlet, player);
        this.maxPunchCount = 3;
        this.effectsTriggered = false;

        this.baseKnockbackStrength = 1;
        this.baseKnockbackRadius = 3;

        this.baseStunDuration = 20;
        this.baseStunRadius = 6;
    }

    onAttack() {
        super.onAttack();

        if (this.punchCount === this.maxPunchCount && !this.effectsTriggered) {
            this.effectsTriggered = true;

            const knockbackStrength = this.baseKnockbackStrength + (this.gauntlet.upgradeStages.primary >= 1 ? 1 : 0);
            const knockbackRadius = this.baseKnockbackRadius + (this.gauntlet.upgradeStages.primary >= 2 ? 1 : 0);
            const shouldStun = this.gauntlet.upgradeStages.primary >= 3 ? true : false;

            const viewDir = this.player.getViewDirection();
            const magnitude = Math.sqrt(viewDir.x ** 2 + viewDir.z ** 2);
            const offset = 1.0;
            const startLocation = {
                x: this.player.location.x + (viewDir.x / magnitude) * offset,
                y: this.player.location.y,
                z: this.player.location.z + (viewDir.z / magnitude) * offset
            };

            for (const target of attackUtils.getValidTargetsInRange(this.player, {
                maxDistance: knockbackRadius,
                startLocation: startLocation,
            })) {
                const delta = attackUtils.VOP.sub(target.location, startLocation);
                const distance = Math.sqrt(delta.x ** 2 + delta.y ** 2 + delta.z ** 2);
                const pushDirection = {
                    x: delta.x / distance,
                    y: delta.y / distance,
                    z: delta.z / distance,
                };
                target.applyDamage(7, { cause: EntityDamageCause.contact });
                target.applyKnockback(pushDirection.x, pushDirection.z, knockbackStrength, knockbackStrength / 2);
            }

            if (!shouldStun) {
                this.player.dimension.spawnParticle("oct_eg:heavy_punch.dirt_small", startLocation);
                this.player.dimension.playSound("octovon.eg.gauntlet.geo.heavy_punch.normal", this.player.location, { volume: 0.8 });
                this.player.runCommand('camerashake add @a[r=4] 0.1 0.5 positional');
            };

            if (shouldStun) {
                for (const target of attackUtils.getValidTargetsInRange(this.player, {
                    maxDistance: this.baseStunRadius,
                    startLocation: startLocation,
                })) {
                    target?.addStun(this.baseStunDuration);
                }

                this.player.dimension.spawnParticle("oct_eg:heavy_punch.dirt_big", startLocation);
                this.player.dimension.playSound("octovon.eg.gauntlet.geo.heavy_punch.stun", this.player.location, { volume: 0.8 });
                this.player.runCommand('camerashake add @a[r=6] 0.2 0.7 positional');
            }


        }

        if (this.punchCount !== this.maxPunchCount) {
            this.effectsTriggered = false;
        }

    }

}
export class SeismicSmash extends GauntletAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 20;
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime);

        this.baseDamage = 15;
        this.baseRadius = 4;
    }

    onEnd() {
        super.onEnd();
        // Reset attack values
        this.attackStartTick = null;
        this.attackCastTick = null;
        // Reset player
        this.player.camera.clear();
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, true);
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);
    }

    onCancel() {
        super.onCancel();
        // Reset attack values
        this.attackStartTick = null;
        this.attackCastTick = null;
        // Reset player
        this.player.camera.clear();
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, true);
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);
    }
    updateCamera(preset, pos, rot, eTime, eType) {
        const cmd = `execute positioned ${this.location.x} ${this.location.y} ${this.location.z} rotated ${this.yrot} 0 positioned ^${pos.x}^${pos.y}^${pos.z} run camera @s set ${preset} ease ${eTime} ${eType} pos ~~~ rot ${rot.x} ${rot.y}`;

        this.player.runCommand(cmd);
    }

    executeAttack() {
        super.executeAttack();
        try {

            this.player.playAnimation("animation.oct_eg.gauntlet.geo.tp.seismic_smash",
                {
                    blendOutTime: 0.1,
                    nextState: 'animation.oct_eg.util.null_1',
                    stopExpression: 'q.all_animations_finished',
                    controller: `_octEgCont_seismicSmash`
                }
            );
            this.player.playAnimation("animation.oct_eg.util.gauntlet.geo.seismic_smash",
                {
                    blendOutTime: 0.0,
                    nextState: '_null',
                    stopExpression: '0',
                    controller: `_octEgCont_seismicSmashUtil`
                }
            );

            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, false);
            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, false);

            this.location = this.player.location;
            this.yrot = this.player.getRotation().y;
            this.updateCamera("minecraft:free", { x: 0, y: 7, z: -5 }, { x: 45, y: "~" }, 0.4, `in_out_sine`);

            this.player.dimension.playSound("octovon.eg.gauntlet.geo.seismic_smash.impact", this.player.location);
            this.player.dimension.playSound("octovon.eg.gauntlet.geo.seismic_smash.start", this.player.location);

            system.runTimeout(() => {
                this.impact();
            }, 0.6 * 20);


        } catch (e) {
            console.error(e);
            this.onCancel();
        }
    }

    impact() {
        if (!this.isCasting) return;

        const damage = this.baseDamage + (this.gauntlet.upgradeStages.skill3 >= 3 ? 5 : 0);

        for (const entity of attackUtils.getValidTargetsInRange(this.player, {
            maxDistance: this.baseRadius,
        })) {
            attackUtils.applyModifiedDamage(entity, damage, { cause: EntityDamageCause.contact, damagingEntity: this.player });
        }

        const seismicSmashEntity = this.player.dimension.spawnEntity("oct_eg:seismic_smash", this.player.location);
        seismicSmashEntity.setDynamicProperty("oct_eg:upgrade_level", this.gauntlet.upgradeStages.skill3);
        seismicSmashEntity.setDynamicProperty("oct_eg:time_created", system.currentTick);
        seismicSmashEntity.getComponent("minecraft:tameable")?.tame(this.player);

        this.player.runCommand('camerashake add @a[r=5] 0.4 2 positional');
        this.player.dimension.spawnParticle("oct_eg:meteor.impact_area", this.player.location);
    }
}
export function seismicSmashTick(entity) {
    if (!entity.isValid()) {
        entity?.remove();
        return;
    };

    const owner = entity.getComponent("minecraft:tameable")?.tamedToPlayer;
    if (!owner) {
        entity.remove();
        return;
    };

    const ungradeLevel = entity.getDynamicProperty("oct_eg:upgrade_level");
    const timeCreated = entity.getDynamicProperty("oct_eg:time_created");

    const lifespan = 5 + (ungradeLevel >= 2 ? 3 : 0);
    if (system.currentTick - timeCreated >= lifespan * 20) {
        entity.dimension.playSound("octovon.eg.gauntlet.geo.seismic_smash.end", entity.location);
        entity.remove();
        return;
    }

    const shouldApplySlowness = ungradeLevel >= 1 ? true : false;
    const radius = 12 + (ungradeLevel >= 2 ? 4 : 0);
    const damageMin = 1 + (ungradeLevel >= 3 ? 1 : 0);
    const damageMax = damageMin + 1;
    const minDamageDistance = 4;

    const applyDamage = entity.getDynamicProperty("oct_eg:has_applied_damage");
    if (!applyDamage) {
        for (const target of attackUtils.getValidTargetsInRange(entity, {
            maxDistance: radius,
            excludedEntities: [owner],
        })) {
            if (!target.isOnGround) continue;

            const dx = target.location.x - entity.location.x;
            const dy = target.location.y - entity.location.y;
            const dz = target.location.z - entity.location.z;
            const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);

            let damage;
            if (distance <= minDamageDistance) {
                damage = damageMax;
            } else {
                const t = Math.min((distance - minDamageDistance) / (radius - minDamageDistance), 1);
                damage = damageMax - t * (damageMax - damageMin);
            }

            attackUtils.applyModifiedDamage(target, damage, { cause: EntityDamageCause.contact });
            if (shouldApplySlowness) {
                target.addEffect("minecraft:slowness", 20, { amplifier: 0, showParticles: true });
            }
        }

        entity.setDynamicProperty("oct_eg:has_applied_damage", true);
    } else {
        entity?.setDynamicProperty("oct_eg:has_applied_damage", false);
    }


    const molang = new MolangVariableMap();
    molang.setFloat("radius", radius);
    entity.dimension.spawnParticle("oct_eg:seismic_smash.smoke", entity.location, molang);
    entity.dimension.spawnParticle("oct_eg:seismic_smash.radius", entity.location, molang);
    entity.dimension.spawnParticle("oct_eg:seismic_smash.dirt", entity.location, molang);

    const randomPitch = 1 + Math.random() * 0.2 - 0.1;
    entity.dimension.playSound("octovon.eg.gauntlet.geo.seismic_smash.loop", entity.location, { pitch: randomPitch });

    entity.runCommand(`camerashake add @a[r=${radius}] 0.1 1 positional`);

}
export class LeapSlam extends GauntletChargedAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 30;
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime, []);

        this.currentFrameIndex = 0;
        this.startLocation = this.player.location;
        this.endLocation = this.player.location;
        this.yrot = 0;

        this.animationFrames = [
            {
                time: 0.0, fn: () => {
                    this.updateCamera("minecraft:free", { x: 0, y: 7, z: -6 }, { x: 30, y: "~" }, 0.3, `out_sine`, this.startLocation);
                }
            },
            { time: 0.3, fn: () => this.updateCamera("minecraft:free", { x: 0, y: 7, z: -7 }, { x: 45, y: "~" }, 0.4, `in_out_sine`, this.endLocation) },
            {
                time: 0.7, fn: () => {
                    this.player.teleport(this.endLocation, { rotation: { x: 0, y: this.yrot } });
                }
            },
            {
                time: 0.8, fn: () => {
                    this.player.teleport(this.endLocation, { rotation: { x: 0, y: this.yrot } });
                    this.player.playAnimation("animation.oct_eg.gauntlet.geo.tp.leap_slam.end",
                        {
                            blendOutTime: 0.0,
                            nextState: 'animation.oct_eg.util.null_1',
                            stopExpression: 'q.all_animations_finished',
                            controller: `_octEgCont_LeapSlam`
                        }
                    );
                }
            },
            {
                time: 0.9, fn: () => {
                    this.attackImpact();
                    this.player.camera.setCamera("minecraft:free", {
                        location: this.player.getHeadLocation(),
                        rotation: this.player.getRotation(),
                        easeOptions: {
                            easeTime: 0.6,
                            easeType: EasingType.InOutSine,
                        },
                    });
                }
            },
        ];

        this.baseDamage = 10;
        this.baseRadius = 3;
    }
    setAttackSpecificProperties() {
        this.hasAttackPreviewLogic = true;
        this.attackPreviewTypeId = "oct_eg:attack_cast_preview.rift";
        this.castingRange = 22;
        this.canCastOnLiquid = false;
    }

    whileAiming() {
        super.whileAiming();
        this.handleAttackPreview();
    }

    onEnd() {
        super.onEnd();
        // Reset attack values
        this.attackStartTick = null;
        this.attackCastTick = null;
        this.currentFrameIndex = 0;
        // Reset player
        this.player.camera.clear();
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, true);
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);
    }

    onCancel() {
        super.onCancel();
        // Reset attack values
        this.attackStartTick = null;
        this.attackCastTick = null;
        this.currentFrameIndex = 0;
        // Reset player
        this.player.camera.clear();
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, true);
        this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);

        this.player.playAnimation("animation.oct_eg.util.null_1",
            {
                blendOutTime: 0.0,
                nextState: '_null',
                stopExpression: '0.0',
                controller: `_octEgCont_LeapSlam`
            }
        );
    }

    updateCamera(preset, pos, rot, eTime, eType, location) {
        const cmd = `execute positioned ${location.x} ${location.y} ${location.z} rotated ${this.yrot} 0 positioned ^${pos.x}^${pos.y}^${pos.z} run camera @s set ${preset} ease ${eTime} ${eType} pos ~~~ rot ${rot.x} ${rot.y}`;

        this.player.runCommand(cmd);
    }

    chargedAttackSuccess() {
        super.chargedAttackSuccess();

        try {

            this.startLocation = this.player.location
            this.endLocation = this.linkedAttackPreview?.location;

            // Get the attack rotation
            const dx = this.endLocation.x - this.startLocation.x;
            const dz = this.endLocation.z - this.startLocation.z;
            const yrot = (Math.atan2(-dx, dz) * (180 / Math.PI) + 360) % 360;

            this.yrot = yrot
            this.player.teleport(this.startLocation, { rotation: { x: 0, y: yrot } });

            // Setup the animation
            this.currentFrameIndex = 0;
            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, false);
            this.player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, false);

            this.player.addEffect("minecraft:resistance", 20, { amplifier: 254, showParticles: false });
            this.player.addEffect("minecraft:slow_falling", 20, { amplifier: 254, showParticles: false });

            this.player.playAnimation("animation.oct_eg.gauntlet.pyro.tp.flaming_meteor.ongoing",
                {
                    blendOutTime: 0.0,
                    nextState: '_null',
                    stopExpression: '0',
                    controller: `_octEgCont_LeapSlam`
                }
            );
            this.player.playAnimation("animation.oct_eg.gauntlet.geo.tp.leap_slam.start",
                {
                    blendOutTime: 0.0,
                    nextState: 'animation.oct_eg.gauntlet.pyro.tp.flaming_meteor.ongoing',
                    stopExpression: 'q.all_animations_finished',
                    controller: `_octEgCont_LeapSlam`
                }
            );

            this.player.dimension.playSound("octovon.eg.gauntlet.geo.leap_slam.start", this.startLocation);
            this.player.dimension.spawnParticle("oct_eg:leap_slam.start", this.startLocation);


            this.removeAttackPreview();

        } catch (e) {
            //console.error(`§cOctovon: Flaming Meteor Attack Failed:§8\n${e}`);
            this.onEnd();
        }

    }

    whileCasting() {
        super.whileCasting();

        const animSeconds = (system.currentTick - this.attackCastTick) / 20;
        while (
            this.currentFrameIndex < this.animationFrames.length &&
            animSeconds >= this.animationFrames[this.currentFrameIndex].time
        ) {
            this.animationFrames[this.currentFrameIndex].fn();
            this.currentFrameIndex++;
        }
    }

    attackImpact() {
        this.player.runCommand('camerashake add @a[r=5] 0.3 1 positional');
        this.player.runCommand('camerashake add @a[rm=5,r=10] 0.1 1 positional');

        const attackRange = this.baseRadius + (this.gauntlet.upgradeStages.skill1 >= 1 ? 2 : 0);
        const attackDamage = this.baseDamage + (this.gauntlet.upgradeStages.skill1 >= 2 ? 5 : 0);
        const shouldAddEffects = this.gauntlet.upgradeStages.skill1 >= 3 ? true : false;

        const attackKnockback = { h: 0.5, v: 1.5 };

        //world.sendMessage(`§6Attack Damage: ${attackDamage} §6Attack Range: ${attackRange}`);

        for (const entity of attackUtils.getValidTargetsInRange(this.player, {
            startLocation: this.endLocation,
            maxDistance: attackRange,
        })) {
            attackUtils.applyModifiedDamage(
                entity,
                attackDamage,
                {
                    cause: EntityDamageCause.entityExplosion,
                    damagingEntity: this.player
                }
            )

            const pointAwayVector = attackUtils.getPointAwayDirection(entity.location, this.endLocation, true);
            try {
                entity.applyKnockback(pointAwayVector.x, pointAwayVector.z, attackKnockback.h, attackKnockback.v);
            } catch (e) { }

        }

        const visualEntity = this.player.dimension.spawnEntity('oct_eg:leap_slam', this.endLocation);
        visualEntity.setProperty("oct_eg:ryinit", this.yrot);
        visualEntity.setProperty("oct_eg:ult_stage1", this.gauntlet.upgradeStages.skill1 >= 1 ? true : false);

        this.player.dimension.spawnParticle("oct_eg:leap_slam.area", this.endLocation);
        this.player.dimension.playSound("octovon.eg.gauntlet.pyro.flaming_meteor.impact", this.endLocation);
        this.player.dimension.playSound("octovon.eg.gauntlet.geo.seismic_smash.start", this.endLocation);

        if (shouldAddEffects) {
            this.player.removeEffect("minecraft:resistance");
            this.player.addEffect("minecraft:resistance", 100, { amplifier: 2, showParticles: true });
            this.player.addEffect("minecraft:speed", 100, { amplifier: 1, showParticles: true });
        }

    }

    updateAttackPreviewAnimations() {
        super.updateAttackPreviewAnimations();
        const animKey = this.gauntlet.upgradeStages.skill1 >= 1 ? 'upgraded' : 'normal';
        this.linkedAttackPreview?.playAnimation(`animation.oct_eg.leap_preview.charging.${animKey}`,
            {
                blendOutTime: 0,
                nextState: '_null',
                stopExpression: '0.0',
                controller: `_octEgCont_atPrevStage`
            }
        );
    }

}
export class StoneWall extends GauntletChargedAttack {
    constructor(gauntlet, player, cooldownTime) {
        const castTime = 10;
        const maxCharges = 1;
        super(gauntlet, player, castTime, maxCharges, cooldownTime, []);
        this.passedCooldownTime = Math.round(cooldownTime * 20);

        this.currentFrameIndex = 0;
        this.location = this.player.location;
        this.yrot = 0;
        this.baseSize = 2;
    }
    setAttackSpecificProperties() {
        this.hasAttackPreviewLogic = true;
        this.attackPreviewTypeId = "oct_eg:attack_cast_preview.stone_wall";
        this.castingRange = 14;
        this.canCastOnLiquid = true;
    }

    whileAiming() {
        super.whileAiming();
        this.handleAttackPreview();
    }

    chargedAttackSuccess() {
        this.cooldownTime = this.gauntlet.upgradeStages.skill2 >= 3 ? this.passedCooldownTime - 100 : this.passedCooldownTime;
        super.chargedAttackSuccess();

        try {
            // world.sendMessage('Casted Stone Wall');

            const wallEntity = this.player.dimension.spawnEntity("oct_eg:stone_wall", this.location);

            // Store wall positions as dynamic properties
            const size = this.gauntlet.upgradeStages.skill2 >= 1 ? 1 : 0;
            const height = this.gauntlet.upgradeStages.skill2 >= 1 ? 3 : 2;
            const isMagma = this.gauntlet.upgradeStages.skill2 >= 2 ? true : false;
            const damage = 3 + (this.gauntlet.upgradeStages.skill2 >= 3 ? 2 : 0);

            this.wallPositions.forEach((pos, index) => {
                wallEntity.setDynamicProperty(`oct_eg:wall_pos_${index}`, JSON.stringify(pos));
                this.player.dimension.spawnParticle("oct_eg:undead_minion.dirt", pos);
                this.player.dimension.spawnParticle("oct_eg:heavy_punch.smoke", pos);
            });

            this.player.dimension.playSound("octovon.eg.gauntlet.geo.stone_wall.spawn", this.location);
            this.player.dimension.playSound("octovon.eg.gauntlet.geo.seismic_smash.end", this.location);
            wallEntity.runCommand('camerashake add @a[r=7] 0.1 2 positional');

            wallEntity.setDynamicProperty("oct_eg:wall_height", height);
            wallEntity.setDynamicProperty("oct_eg:does_damage", isMagma);
            wallEntity.setDynamicProperty("oct_eg:wall_damage", damage);

            wallEntity?.setProperty("oct_eg:size", size);
            wallEntity?.setProperty("oct_eg:rot", parseInt(this.yrot));
            wallEntity?.setProperty("oct_eg:magma", isMagma);
            wallEntity.getComponent("minecraft:tameable")?.tame(this.player);

            this.player.playAnimation(
                'animation.oct_eg.util.gauntlet.anemo.air_slash',
                {
                    blendOutTime: 0.0,
                    nextState: "_null",
                    stopExpression: "q.all_animations_finished",
                    controller: `_octEgCont_utilAirSlash`
                }
            );

            this.removeAttackPreview();

        } catch (e) {
            this.onCancel();
        }

    }

    updateAttackPreviewAnimations() {
        super.updateAttackPreviewAnimations();

        if (!this.linkedAttackPreview) return;

        const size = this.gauntlet.upgradeStages.skill2 >= 1 ? 1 : 0;
        const rotation = this.player.getRotation().y;
        const roundedRotation = Math.round(rotation / 45) * 45;

        this.linkedAttackPreview?.setProperty("oct_eg:rot", parseInt(roundedRotation));
        this.yrot = roundedRotation;
        this.location = this.linkedAttackPreview.location;

        this.linkedAttackPreview?.setProperty("oct_eg:size", size);

        this.wallPositions = this.getWallPositions();
    }

    getWallPositions() {
        const center = this.location;
        const rotRad = (this.yrot) * (Math.PI / 180);

        // Rounded direction based on rotation
        const dx = Math.round(Math.cos(rotRad));
        const dz = Math.round(Math.sin(rotRad));

        const wallPositions = [];

        const size = this.baseSize + (this.gauntlet.upgradeStages.skill2 >= 1 ? 1 : 0);
        for (let offset = -size; offset <= size; offset++) {

            const x = center.x + dx * offset;
            const y = center.y;
            const z = center.z + dz * offset;

            wallPositions.push({ x, y, z });
        }

        return wallPositions;
    }

}

