import { world, system, ItemStack, Player, Block, BlockVolumeBase, BlockVolume, Entity, BlockPermutation, Dimension, EntityComponentTypes, InputPermissionCategory, EntityDamageCause } from "@minecraft/server";

import { Mathm } from "../modules/mathm.js";
import { Vec3 } from "../utils/vec.js";
import { ScriptEventListener } from "../utils/scriptevent";

import { Debugger } from "../utils/debug.js";
const debug = new Debugger('mythicus_tw:watcher.events');
debug.enabled = false;
debug.priority = 0;

//const ACTIVE_WATCHERS = {};

//system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
//    if (id !== "mythicus_tw:addActiveWatcher" || sourceEntity?.typeId !== "mythicus_tw:watcher") return;

//    ACTIVE_WATCHERS[sourceEntity.id] = sourceEntity;
//});

//system.runInterval(() => {

//    for (const [watcherId, watcher] of Object.entries(ACTIVE_WATCHERS)) {
//        if (watcher?.isValid() !== true) delete ACTIVE_WATCHERS[watcherId];

//        world.sendMessage(`entity: ${watcherId} | ${watcher.dimension}`)
//        checkAvailableHeight(watcher);
//    }

//}, 2);

/**
 * 
 *
 * @param {Dimension} dim - Dimension.
*/
function randomNearbyGroundN(dim, loc, rw, rh = rw) {
    const locations = [];
    const h = loc.y;
    const [xL, zL] = [(loc.x >> 0), (loc.z >> 0)];

    for (let x = xL - rw; x <= xL + rw; x++) {
        for (let z = zL - rw; z <= zL + rw; z++) {

            let block = undefined;
            try {
                const air = dim.getBlockFromRay({ x: x, y: h + 3, z: z }, { x: 0, y: -1, z: 0 }, { maxDistance: 4, includePassableBlocks: true, includeTypes: ["minecraft:air"] });
                if (air && air.block.below().isAir) {
                    block = dim.getBlockFromRay(air.block.location, { x: 0, y: -1, z: 0 }, { maxDistance: 4, includeLiquidBlocks: false, includePassableBlocks: false })?.block;
                }
            } finally {
                if (block) {
                    locations.push({ x: block.x, y: block.y + 1, z: block.z });
                }
            }

        }
    }

    if (locations.length === 0) return undefined;

    return locations[Mathm.randomRangeInt(0, locations.length - 1)];
}

/**
 * 
 *
 * @param {Dimension} dim - Dimension.
*/
function randomNearbyGround(dim, loc, rw, rh = rw) {
    const locations = [];
    const [xL, yL, zL] = [(loc.x >> 0), (loc.y >> 0), (loc.z >> 0)];
    const { min: yMin, max: yMax } = dim.heightRange;

    for (let x = xL - rw; x <= xL + rw; x++) {
        for (let z = zL - rw; z <= zL + rw; z++) {
            for (let y = yL - rh; y <= yL + rh; y++) {
                if (y < yMin || yMax < y) continue;

                const block = dim.getBlock({ x: x, y: y, z: z });
                if (block?.isAir !== false) continue;
                if (y < (yMax - 1) && block.above(2)?.isAir === false) {
                    y++;
                    continue;
                } else if (y < yMax && block.above(1)?.isAir === false) {
                    continue;
                }

                locations.push({ x: x, y: y + 1, z: z });
            }
        }
    }

    if (locations.length === 0) return undefined;

    return locations[Mathm.randomRangeInt(0, locations.length - 1)];
}


/**
 * 
 *
 * @param {Dimension} dim - Dimension.
*/
function checkLevelGround(dim, loc, distance, ang) {

    const pos = { x: loc.x, z: loc.z };
    const h = loc.y;
    const xa = Math.sin(ang), za = Math.cos(ang);

    for (let i = 1; i <= distance; i++) {
        let height = h;
        pos.x -= xa;
        pos.z += za; // negative?
        try {
            height = dim.getTopmostBlock(pos, h + 10)?.y;
        } finally {
            //world.sendMessage(`height: ${height} | ${JSON.stringify(pos)}`)
            if (Math.abs(height - h) > 4) {
                return false;
            }
        }
    }

     return true;
}

/**
 * 
 *
 * @param {Dimension} dim - Dimension.
*/
function isOpenPath(dim, loc, distance, ang) {

    const pos = { x: loc.x, y: loc.y, z: loc.z };
    const xa = Math.sin(ang), za = Math.cos(ang);

    for (let i = 1; i <= distance; i++) {
        
        pos.x -= xa;
        pos.z += za;

        try {

            let h = pos.y;

            // get highest block on ground (up to 2 blocks in air)
            const groundBlock = dim.getBlockFromRay({ x: pos.x, y: pos.y + 2, z: pos.z }, { x: 0, y: -1, z: 0 }, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 2 });
            if (groundBlock?.block) h = groundBlock.block.y + 1;

            if (dim.getBlockFromRay({ x: pos.x, y: h, z: pos.z }, { x: 0, y: 1, z: 0 }, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 2 })?.block !== undefined) {
                return false;
            }

        } finally {
        }
    }

    return true;
}

export let AGGRESSION_LEVEL = world.getDynamicProperty("mythicus_tw:watcherAggressionLevel") ?? 0;

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (id !== "mythicus_tw:getAggressionLevel" || sourceEntity?.typeId !== "minecraft:player") return;

    if (AGGRESSION_LEVEL === 0) {
        sourceEntity.sendMessage("Aggression: LOW");
    } else if (AGGRESSION_LEVEL === 1) {
        sourceEntity.sendMessage("Aggression: MEDIUM");
    } else {
        sourceEntity.sendMessage("Aggression: HIGH");
    }
});

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (id !== "mythicus_tw:getFear" || sourceEntity?.typeId !== "minecraft:player") return;

    sourceEntity.sendMessage(`Fear: ${sourceEntity.getDynamicProperty("mythicus_tw:fear.amount") ?? 0}`);
    //sourceEntity.sendMessage(`Fear: ${world.getDynamicProperty("mythicus_tw:illusion.noLimit")}`);
});

system.runInterval(() => {

    for (const player of world.getAllPlayers()) {
        checkSleep(player);
        if (player.getDynamicProperty("mythicus_tw:chasedByWatcher")) chaseFog(player);
    }

}, 20);

system.runInterval(() => {

    parseDayInfo();

    for (const player of world.getAllPlayers()) {
        checkFear(player);
    }

}, 200);

world.afterEvents.playerBreakBlock.subscribe(data => {

    breakBlock(data.player, data.block);

});

let CRAWLER_SPAWN_TIMER = 0;
ScriptEventListener.subscribe("mythicus_tw:illusion.crawlerSpawnTimer", ({ sourceEntity: entity }) => {

    // check if another entity has already been placed
    if (entity?.isValid() === true && CRAWLER_SPAWN_TIMER >= system.currentTick) {
        entity.remove();
        return;
    }

    CRAWLER_SPAWN_TIMER = system.currentTick + 1000;

});

/**
 * Runs when a block breaks.
 *
 * @param {Player} player - Player.
 * @param {Block} block - Block.
*/
function breakBlock(player, block) {
    
    if (Mathm.randomChance(0.015) && player.dimension.getTopmostBlock({ x: player.location.x, z: player.location.z })?.y - 5 > player.location.y && CRAWLER_SPAWN_TIMER < system.currentTick) {
        
        // check if block broken is at head height
        if (Math.floor(player.location.y) + 1 !== block.y) return;
          
        // check block is walled in
        let isWalled = false;
        try {
            isWalled = (block.west().isAir + block.east().isAir + block.north().isAir + block.south().isAir + block.above().isAir + block.below().isAir) <= 1;
        } finally {
            if (!isWalled) return;
        }

        // check no nearby watcher
        if (player.dimension.getEntities({ location: player.location, maxDistance: 30, type: "mythicus_tw:watcher" }).length > 0) return;
        
        // get direction of line of breakage
        const xDist = block.bottomCenter().x - player.location.x;
        const zDist = block.bottomCenter().z - player.location.z;
        if (Math.abs(xDist) > 1 && Math.abs(zDist) > 1) return;
        const isXAxis = Math.abs(xDist) > Math.abs(zDist);
        const solidDir = isXAxis ? xDist / Math.abs(xDist) : zDist / Math.abs(zDist);
        
        // check if area in line of breakage is solid or open (air)
        let isSolid = false;
        try {
            const offVec = { x: 0, y: 0, z: 0 };
            let solidCount = 0;
            for (let i = 0; i < 3; i++) {
                if (isXAxis) offVec.x += solidDir;
                else offVec.z += solidDir;

                const offVecBelow = { ...offVec };
                offVecBelow.y = -1;

                if (block.offset(offVec)?.isAir === false) solidCount += 1;
                if (block.offset(offVecBelow)?.isAir === false) solidCount += 1;
            }
            isSolid = (solidCount >= 6);
        } finally {
            if (!isSolid) return;
        }

        

        try {
            const volumeEnd = block.center();
            if (isXAxis) volumeEnd.x += 2 * solidDir;
            else volumeEnd.z += 2 * solidDir;

            const solidArea = new BlockVolume(block.center(), volumeEnd);
            block.dimension.fillBlocks(solidArea, "minecraft:air");
        } catch {
            return;
        }

        //let summonLocation = { ...block.center() };
        //if (isXAxis) summonLocation.x += 1.55 * solidDir;
        //else summonLocation.z += 1.55 * solidDir;
        //player.dimension.spawnEntity("mythicus_tw:illusion<mythicus_tw:crawler>", summonLocation);

        const blockCenter = block.center();
        block.dimension.runCommand(
            `summon mythicus_tw:illusion ${blockCenter.x + 1.65 * (isXAxis ? solidDir : 0)} ${blockCenter.y - 0.6} ${blockCenter.z + 1.65 * (isXAxis ? 0 : solidDir)} ${(solidDir < 1 ? 0 : 180) - (isXAxis ? 90 : 0)} ~ mythicus_tw:crawler`
        );

        //player.runCommand("effect @a[r=5] blindness 5 0 true");
        player.runCommand("playsound mythicus_tw:watcher.run_whisper @s ~ ~ ~ 1 1 ");

    }

}



system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (id !== "mythicus_tw:watcher.scratchNearbyTree" || sourceEntity?.typeId !== "mythicus_tw:watcher") return;

    if (Mathm.randomChance(0.2 + (0.1 * AGGRESSION_LEVEL))) {
        scratchNearbyTree(sourceEntity);
    }

});

/**
 * 
 *
 * @param {Entity} entity - Entity.
*/
function scratchNearbyTree(entity) {

    const dim = entity.dimension;
    const loc = entity.location;

    // randomize scratch height
    loc.y += Mathm.randomRangeInt(1, 3);

    const volume = new BlockVolume({ x: loc.x - 5, y: loc.y, z: loc.z - 5 }, { x: loc.x + 5, y: loc.y, z: loc.z + 5 });
    for (const location of volume.getBlockLocationIterator()) {

        // check block type
        const block = dim.getBlock(location);
        if (block?.isAir !== false || !block.typeId.endsWith("_log") || block.typeId.includes("stripped")) continue;

        // determine facing direction of scratch mark
        const xDist = block.bottomCenter().x - loc.x;
        const zDist = block.bottomCenter().z - loc.z;
        const dir = Math.abs(xDist) > Math.abs(zDist) ? (xDist > 0 ? "west" : "east") : (zDist > 0 ? "north" : "south");

        // set block
        let scratchedPerm;
        let scratchedBlock;
        try {
            scratchedPerm = BlockPermutation.resolve("mythicus_tw:scratch", { "minecraft:block_face": dir, "mythicus_tw:variant": Mathm.randomRangeInt(0, 6) });
            scratchedBlock = dir === "west" ? block.west() : (dir === "east" ? block.east() : (dir === "north" ? block.north() : block.south()));
        } finally {
            if (scratchedPerm && scratchedBlock?.isAir === true) scratchedBlock.setPermutation(scratchedPerm);
        }

    }
}

/**
 *
 * @param {Player} player - Player.
*/
function checkSleep(player) {
    if (player.isSleeping) {
        if (player.dimension.getEntities({ location: player.location, maxDistance: 60, type: "mythicus_tw:watcher" }).length > 0) {
            //player.runCommand("tp @s ~ ~ ~");
            player.teleport(player.location);
            //player.runCommand("title @s actionbar You are being watched...")
            player.onScreenDisplay.setActionBar({ rawtext: [{ translate: "mythicus_tw:actionbar.cantSleep" }] });
        }
    }
}

/**
 *
 * @param {Player} player - Player.
*/
function chaseFog(player) {
    if (player.isOnGround) {
        const loc = player.location;
        const groundLoc = { x: Math.floor(loc.x), y: Math.floor(loc.y - 1), z: Math.floor(loc.z) };
        player.dimension.spawnParticle("mythcius_tw:ground_fog", groundLoc);
    }
}

function parseDayInfo() {

    // get current info
    const day = world.getDay();
    const timeOfDay = world.getTimeOfDay();

    // trigger change of day events
    if (world.getDynamicProperty("mythicus_tw:day") !== day) {
        world.setDynamicProperty("mythicus_tw:day", day);
        changeOfDay();
    }

    // update properties
    world.setDynamicProperty("mythicus_tw:timeOfDay", timeOfDay);

}

function changeOfDay() {

    // update watcher aggression level
    const aggressionLevel = Mathm.randomRangeInt(0, 2);
    world.setDynamicProperty("mythicus_tw:watcherAggressionLevel", aggressionLevel);
    AGGRESSION_LEVEL = aggressionLevel;

}


let blockPlaceSub;
let itemUseSub;

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity: entity }) => {
    if (!entity || id !== "mythicus_tw:chase.start") return;

    // get nearest player
    const player = entity.dimension.getPlayers({ location: entity.location, closest: 1, maxDistance: 128 })[0];
    if (!player) {
        return;
    }

    // set dynamic property
    entity.setDynamicProperty("mythicus_tw:visibilityCount", 99);

    //player.playMusic("mythicus_tw:watcher.heart_beat", { loop: false });
    player.runCommand("playsound mythicus_tw:watcher.heart_beat");
    player.setDynamicProperty("mythicus_tw:chasedByWatcher", true);
    player.setDynamicProperty("mythicus_tw:chasedByWatcherTimer", system.currentTick + 45 * 20);
    player.addEffect("minecraft:darkness", 45 * 20, { showParticles: false });
    player.addEffect("minecraft:mining_fatigue", 45 * 20, { showParticles: false });

    // remove previous block place event
    if (blockPlaceSub) {
        world.afterEvents.playerBreakBlock.unsubscribe(blockPlaceSub);
        blockPlaceSub = undefined;
    }
    // remove previous block place event
    if (itemUseSub) {
        world.beforeEvents.itemUseOn.unsubscribe(itemUseSub);
        itemUseSub = undefined;
    }

    // add block place event
    blockPlaceSub = world.afterEvents.playerPlaceBlock.subscribe((data) => {
        if (data.player.getDynamicProperty("mythicus_tw:chasedByWatcher")) {
            world.sendMessage("place water")
            data.dimension.spawnItem(data.block.getItemStack(1, true), data.block.bottomCenter());
            data.block.setType("minecraft:air");
        }
    });
    itemUseSub = world.beforeEvents.itemUseOn.subscribe((data) => {
        if (data?.source?.getDynamicProperty("mythicus_tw:chasedByWatcher") === true) {
            if (data.itemStack.typeId === "minecraft:water_bucket") {
                data.cancel = true;
            }
        }
    });
    //blockPlaceSub = world.afterEvents.playerPlaceBlock.subscribe((data) => {
    //    if (data.player.getDynamicProperty("mythicus_tw:chasedByWatcher")) {
    //        const loc = data.block.bottomCenter();
    //        data.dimension.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air destroy`);
    //    }
    //});

    system.runTimeout(() => {
        if (player?.isValid() && system.currentTick > (player.getDynamicProperty("chasedByWatcherTimer") ?? 0)) {
            player.setDynamicProperty("mythicus_tw:chasedByWatcher", false);
        }

        if (world.getAllPlayers().some((player) => player.getDynamicProperty("mythicus_tw:chasedByWatcher") === true)) return;
        if (blockPlaceSub) {
            world.afterEvents.playerBreakBlock.unsubscribe(blockPlaceSub);
            blockPlaceSub = undefined;
        }
        if (itemUseSub) {
            world.beforeEvents.itemUseOn.unsubscribe(itemUseSub);
            itemUseSub = undefined;
        }
    }, 50 * 20);

});

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity: entity }) => {
    if (!entity || id !== "mythicus_tw:chase.stop") return;

    // get nearest player
    const players = entity.dimension.getPlayers({ location: entity.location, maxDistance: 64 }).filter(plr => plr.getDynamicProperty("mythicus_tw:chasedByWatcher"));

    //player.stopMusic();
    for (const player of players) {
        player.runCommand("stopsound @s mythicus_tw:watcher.heart_beat");
        player.setDynamicProperty("mythicus_tw:chasedByWatcher", false);
        player.removeEffect("minecraft:darkness");
        player.removeEffect("minecraft:mining_fatigue");
    }

    // remove place event
    if (blockPlaceSub) {
        world.afterEvents.playerBreakBlock.unsubscribe(blockPlaceSub);
        blockPlaceSub = undefined;
    }
    if (itemUseSub) {
        world.beforeEvents.itemUseOn.unsubscribe(itemUseSub);
        itemUseSub = undefined;
    }

    // remove watcher
    debug.log("remove from chase");
    entity.remove();

});

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity: entity }) => {
    if (!entity || id !== "mythicus_tw:behavior.baseSelector") return;

    // get behavior
    const behavior = entity.getProperty("mythicus_tw:behavior") ?? 0;

    // get distance to nearest player
    let distance = -1;
    const closestPlayer = entity.dimension.getPlayers({ location: entity.location, closest: 1, maxDistance: 128 })[0];
    if (closestPlayer) {
        distance = Mathm.clamp(Vec3.distance(entity.location, closestPlayer.location), 0, 128);
    }

    // get visibility count
    const visibilityCount = entity.getDynamicProperty("mythicus_tw:visibilityCount") ?? 0;



    // --- despawn mechanics --- //

    // increment life timer
    const lifeTimer = entity.getDynamicProperty("mythicus_tw:lifeTimer") ?? 0;
    entity.setDynamicProperty("mythicus_tw:lifeTimer", lifeTimer + 1);

    // random immobile despawn chance
    if (visibilityCount === 0 && behavior !== 2) {
        const lastPos = entity.getDynamicProperty("mythicus_tw:lastPos") ?? { x: 0, y: -1000, z: 0 };
        entity.setDynamicProperty("mythicus_tw:lastPos", entity.location);

        const lastInvis = entity.getDynamicProperty("mythicus_tw:lastInvis") ?? 0;
        entity.setDynamicProperty("mythicus_tw:lastInvis", lastInvis + 1);

        // add height difference to chance?

        if (lastInvis > 1 && distance > 20 && Vec3.distance(lastPos, entity.location) < 0.5 && Math.random() < 0.4) {
            debug.log("remove from immobile despawn");
            entity.remove();
            return;
        }
    } else {
        entity.setDynamicProperty("mythicus_tw:lastInvis", 0);
    }

    // random lifetime despawn chance
    if (behavior !== 2 && lifeTimer > 30 && distance > 8 && Math.random() < 0.05) {
        debug.log("remove from lifetime chance");
        entity.remove();
        return;
    }



    // --- basic mechanics --- //

    // update fear
    for (const player of entity.dimension.getPlayers({ location: entity.location, maxDistance: 96 })) {
        updateFear(player);
    }

    // activate nearby selector if invisible next to player
    if (visibilityCount === 0 && behavior !== 2 && distance < 5) {
        nearbySelector(entity);
        return;
    }

    // random constant behaviors
    if (visibilityCount === 0 || behavior === 2) {
        if (Mathm.randomChance(0.1 + 0.1 * AGGRESSION_LEVEL)) {
            scratchNearbyTree(entity);
        }

        if (Mathm.randomChance(0.05 + 0.05 * AGGRESSION_LEVEL)) {
            placeFootstep(entity);
        }
    }

    // selector behavior
    switch (behavior) {

        case 0:
            behaviorDefaultSelector(entity, distance, visibilityCount);
            break;

        case 1:
            behaviorStopStareSelector(entity, distance, visibilityCount);
            break;

        case 2:
            behaviorChaseSelector(entity, distance, visibilityCount);
            break;

        case 10:
            break;

        default:
            behaviorDefaultSelector(entity, distance, visibilityCount);
            //world.sendMessage("default selector")
            break;
    }

});

function behaviorDefaultSelector(entity, distance, visibilityCount) {

    // default distance
    if (distance === -1) distance = 16;

    // get height
    const height = checkAvailableHeight(entity);

    // get visibility chance
    const visibilityChance = Math.random() < (0.7 - 0.00781 * Mathm.clamp(distance, 0, 64) - 0.2 * (height <= 1)); // 0.5 * (1 / 64)

    // change pose/visibility and related movement
    if (visibilityChance && !entity.isInWater) {
        entity.setDynamicProperty("mythicus_tw:visibilityCount", visibilityCount + 1);
        entity.triggerEvent("mythicus_tw:movement_behaviors.deactivate");
        
        // check if pose is new
        const pose = Mathm.randomRangeInt(0, 4);
        const isNewPose = pose !== (entity.getProperty("mythicus_tw:pose") ?? 0);

        // high chance to delay change of pose if going from invisible to visible to prevent movement seen
        if (visibilityCount === 0 && (distance > 8 ? true : Math.random() < 0.8)) {
            system.runTimeout(() => {
                if (!entity?.isValid()) return;
                const heightCheck = checkAvailableHeight(entity);
                if (heightCheck > 1) entity.setProperty("mythicus_tw:pose", pose);
                if (isNewPose && heightCheck > 1) entity.runCommand("playsound mythicus_tw:watcher.bones_breaking @a ~ ~ ~ 1");
            }, 15);
        } else {
            const heightCheck = checkAvailableHeight(entity);
            if (heightCheck > 1) entity.setProperty("mythicus_tw:pose", pose);
            if (isNewPose && heightCheck > 1) entity.runCommand("playsound mythicus_tw:watcher.bones_breaking @a ~ ~ ~ 1");
        }
    } else {
        entity.setProperty("mythicus_tw:pose", 10);
        entity.setDynamicProperty("mythicus_tw:visibilityCount", 0);
        
        if (distance >= 5 && visibilityCount > 0) {
            entity.triggerEvent("mythicus_tw:movement_behaviors.activate");
            entity.triggerEvent("mythicus_tw:speed.set");
        }
    }

    // determine behavior chances
    const teleportChance = 0.005 * AGGRESSION_LEVEL;
    const stopStareChance = teleportChance + 0.010 + 0.0075 * (2 - AGGRESSION_LEVEL);

    // check if a random behavior should be selected
    const random = Math.random();
    if (random < teleportChance) {
        entity.runCommand("scriptevent mythicus_tw:teleportBehindPlayer2");
    } else if (random < stopStareChance && entity.dimension.getTopmostBlock({ x: entity.location.x, z: entity.location.z })?.y - 2 < entity.location.y) {
        debug.log("watcher entering stop and stare mode");
        entity.triggerEvent("mythicus_tw:stop_and_stare.activate");
    }

}

function behaviorStopStareSelector(entity, distance, visibilityCount) {

    // default distance
    if (distance === -1) distance = 128;

    const isStaringDist = distance < 64;

    // get height
    const height = checkAvailableHeight(entity);

    // chance to do nothing
    if (isStaringDist && Math.random() < (visibilityCount === 0 ? 0.30 : 0.60)) {
        return;
    }

    // get visibility chance
    const visibilityChance = Math.random() < (0.95 - (isStaringDist ? 0.0025 : 0.0065) * distance - 0.3 * (height <= 1));

    // change pose/visibility and related movement
    if (visibilityChance && !entity.isInWater) {
        entity.setDynamicProperty("mythicus_tw:visibilityCount", visibilityCount + 1);
        if (visibilityCount === 0) {
            entity.triggerEvent("mythicus_tw:stop_and_stare.stare");
        }

        // check if pose is new
        const pose = Mathm.randomRangeInt(0, 4);
        const isNewPose = pose !== (entity.getProperty("mythicus_tw:pose") ?? 0);

        // high chance to delay change of pose if going from invisible to visible to prevent movement seen
        if (visibilityCount === 0 && (distance > 8 ? true : Math.random() < 0.8)) {
            system.runTimeout(() => {
                if (!entity?.isValid()) return;
                const heightCheck = checkAvailableHeight(entity);
                if (heightCheck > 1) entity.setProperty("mythicus_tw:pose", pose);
                if (isNewPose && heightCheck > 1) entity.runCommand("playsound mythicus_tw:watcher.bones_breaking @a ~ ~ ~ 1");
            }, 15);
        } else {
            const heightCheck = checkAvailableHeight(entity);
            if (heightCheck > 1) entity.setProperty("mythicus_tw:pose", pose);
            if (isNewPose && heightCheck > 1) entity.runCommand("playsound mythicus_tw:watcher.bones_breaking @a ~ ~ ~ 1");
        }
    } else {
        entity.setProperty("mythicus_tw:pose", 10);
        entity.setDynamicProperty("mythicus_tw:visibilityCount", 0);
        if (!isStaringDist) {
            entity.triggerEvent("mythicus_tw:stop_and_stare.track");
            entity.triggerEvent("mythicus_tw:speed.set");
        }
    }

}

function behaviorChaseSelector(entity, distance, visibilityCount) {

    // default distance
    if (distance === -1) distance = 32;

    // get height
    const height = checkAvailableHeight(entity);

    // get position
    const lastPos = entity.getDynamicProperty("mythicus_tw:lastPos") ?? { x: 0, y: -1000, z: 0 };
    entity.setDynamicProperty("mythicus_tw:lastPos", entity.location);

    // teleport if far
    if (distance > 8 || entity.isInWater) {
        if (Math.random() < (Vec3.distance(lastPos, entity.location) < 1 ? 0.90 : 0.70)) {
            teleportBehindChase(entity);
        }
    }

}

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity: entity }) => {
    if (!entity || id !== "mythicus_tw:behavior.lookAtSelector") return;
    
    // return if invisible
    if ((entity.getDynamicProperty("mythicus_tw:visibilityCount") ?? 0) === 0) {
        return;
    }
    
    // get distance to nearest player
    let distance = 16;
    const closestPlayer = entity.dimension.getPlayers({ location: entity.location, closest: 1, maxDistance: 128 })[0];
    if (closestPlayer) {
        distance = Mathm.clamp(Vec3.distance(entity.location, closestPlayer.location), 0, 128);
    }

    if ((entity.getProperty('mythicus_tw:behavior') ?? 0) === 2) {
        if (distance > 3 && Math.random() < 0.75 && entity.getProperty("mythicus_tw:is_under_door") !== true && system.currentTick > (entity.getDynamicProperty("mythicus_tw:chaseTpCd") ?? 0)) {
            entity.setDynamicProperty("mythicus_tw:chaseTpCd", system.currentTick + Mathm.randomRangeInt(80, 100));
            teleportBehindChase(entity);
        }
        return;
    }

    // check selector cooldown
    if (system.currentTick < (entity.getDynamicProperty("mythicus_tw:lookAtSelectorCd") ?? 0)) return;
    entity.setDynamicProperty("mythicus_tw:lookAtSelectorCd", system.currentTick + 50);

    // actions if approached by or towards watcher
    let approachState = entity.getDynamicProperty("mythicus_tw:approachState") ?? 0;
    if (approachState > 0 && distance > 8) {
        approachState = 0;
        entity.setDynamicProperty("mythicus_tw:approachState", 0);
    }

    if (approachState === 2) {
        if (Math.random() < (0.7 - 0.15 * AGGRESSION_LEVEL)) {
            entity.dimension.playSound("mythicus_tw:watcher.dont_move", entity.location);
            entity.runCommand("effect @a[r=8] blindness 3 0 true");
            entity.setProperty("mythicus_tw:despawning", Mathm.randomRangeInt(1, 2));
            entity.triggerEvent("mythicus_tw:quick_look.remove");
            system.runTimeout(() => {
                if (entity?.isValid()) {
                    entity.remove();
                }
            }, 50);
        } else {
            entity.triggerEvent("mythicus_tw:chase.activate");
            entity.setDynamicProperty("mythicus_tw:approachState", 0);
        }
    } else if (approachState === 1) {
        const rand = Math.random();
        const randChase = 0.15 + 0.1 * AGGRESSION_LEVEL;
        if (rand < randChase) {
            entity.triggerEvent("mythicus_tw:chase.activate");
            entity.setDynamicProperty("mythicus_tw:approachState", 0);
        } else if (randChase + 0.3) {
            entity.dimension.playSound("mythicus_tw:watcher.dont_move", entity.location);
            entity.runCommand("effect @a[r=8] blindness 3 0 true");
            entity.setProperty("mythicus_tw:despawning", Mathm.randomRangeInt(1, 2));
            entity.triggerEvent("mythicus_tw:quick_look.remove");
            system.runTimeout(() => {
                if (entity?.isValid()) {
                    entity.remove();
                }
            }, 50);
        }
    }

    // determine behavior chances
    const teleportChance = (distance > 16 ? 0.01 : 0.02) + 0.02 * AGGRESSION_LEVEL;
    const chaseChance = distance > 16 ? 0 : 0.01 * AGGRESSION_LEVEL;

    // check if a random behavior should be selected
    const random = Math.random();
    if (random < teleportChance) {
        entity.runCommand("scriptevent mythicus_tw:teleportBehindPlayer2");
    } else if (random < chaseChance) {
        entity.triggerEvent("mythicus_tw:chase.activate");
        entity.setDynamicProperty("mythicus_tw:approachState", 0);
    }

});

//system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity: entity }) => {
//    if (!entity || !id.startsWith("mythicus_tw:behavior.defaultSelector")) return;

//    let distance = 0;
//    if (id.endsWith("mid")) {
//        distance = 16;
//    } else if (id.endsWith("far")) {
//        distance = 64;
//    }

//    const random = Math.random();
//    const visibilityCount = entity.getDynamicProperty("mythicus_tw:visibilityCount") ?? 0;

//    if (random < (distance >= 64 ? 0.1 : (distance >= 16 ? 0.35 : 0.65))) {
//        entity.setProperty("mythicus_tw:pose", Mathm.randomRangeInt(0, 2));
//        entity.setDynamicProperty("mythicus_tw:visibilityCount", visibilityCount + 1);
//        entity.triggerEvent("mythicus_tw:movement_behaviors.deactivate");
//    } else {
//        entity.setProperty("mythicus_tw:pose", 10);
//        entity.setDynamicProperty("mythicus_tw:visibilityCount", 0);
//        entity.triggerEvent("mythicus_tw:movement_behaviors.activate");
//    }

//});

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (!sourceEntity || id !== "mythicus_tw:selectRandomBehavior") return;

    const random = Math.random();

    const teleportChance = 0.05 + 0.05 * AGGRESSION_LEVEL;
    const stopStareChance = teleportChance + 0.20;

    if (random < teleportChance) {
        sourceEntity.runCommand("scriptevent mythicus_tw:teleportBehindPlayer");
    } else if (random < stopStareChance) {
        sourceEntity.triggerEvent("mythicus_tw:stop_and_stare");
    }

});

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (id === "mythicus_tw:behavior.nearbySelector" && sourceEntity?.typeId === "mythicus_tw:watcher") {
        nearbySelector(sourceEntity);
    }
});

/**
 * @param {Entity} entity - Entity.
*/
function nearbySelector(entity) {

    // check height and behavior
    const height = checkAvailableHeight(entity);
    if (height <= 1 || (entity.getProperty("mythicus_tw:behavior") ?? 0) === 2) return;

    // check water
    if (entity.isInWater) return;

    // check selector cooldown
    if (system.currentTick < (entity.getDynamicProperty("mythicus_tw:nearbySelectorCd") ?? 0)) return;
    entity.setDynamicProperty("mythicus_tw:nearbySelectorCd", system.currentTick + 40);

    const visibilityCount = entity.getDynamicProperty("mythicus_tw:visibilityCount") ?? 0;
    if (visibilityCount !== 0) {
        entity.setDynamicProperty("mythicus_tw:approachState", 1);
    } else {
        entity.setDynamicProperty("mythicus_tw:visibilityCount", 100);
        entity.setDynamicProperty("mythicus_tw:approachState", 2);
        entity.triggerEvent("mythicus_tw:movement_behaviors.deactivate");

        const pose = Mathm.randomRangeInt(0, 4);
        system.runTimeout(() => {
            if (!entity?.isValid()) return;
            const heightCheck = checkAvailableHeight(entity);
            if (heightCheck > 1) entity.setProperty("mythicus_tw:pose", pose);
        }, 15);

    }

    entity.triggerEvent("mythicus_tw:quick_look.add");   // don't need to remove so watcher will remain more aggressive if player escapes

    system.runTimeout(() => {
        if (!entity?.isValid()) return;

        const heightCheck = checkAvailableHeight(entity);

        if (entity.getProperty("mythicus_tw:behavior") !== 2 && heightCheck > 1 && Math.random() < 0.7) {
            entity.runCommand("scriptevent mythicus_tw:behavior.lookAtSelector");
        }
    }, Mathm.randomRangeInt(30, 40));

}

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (id !== "mythicus_tw:checkAvailableHeight" || sourceEntity?.typeId !== "mythicus_tw:watcher") return;
    
    checkAvailableHeight(sourceEntity);
});

/**
 * 
 *
 * @param {Entity} entity - Entity.
*/
function checkAvailableHeight(entity) {
    
    // check current height
    const dim = entity.dimension;
    const loc = entity.location;
    loc.y = Math.floor(loc.y);
    const curHeight = entity.getProperty("mythicus_tw:height") ?? 1;

    const isChasing = (entity.getProperty("mythicus_tw:behavior") ?? 0) === 2;

    // speed boost when going up/down blocks while chasing
    if (isChasing) {
        if (loc.y >> 0 !== entity.getDynamicProperty("mythicus_tw:yLevel")) {
            //entity.addEffect("minecraft:speed", 40, { amplifier: 10, showParticles: true });  // not working??
            entity.runCommand("effect @s speed 1 1 true");
        }
        entity.setDynamicProperty("mythicus_tw:yLevel", loc.y >> 0);
    }

    // 
    let height = 0;
    try {
        const block = dim.getBlockFromRay(loc, { x: 0, y: 1, z: 0 }, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 2, excludeTags: ["one_way_collidable"] });
        if (block?.block == undefined) height = 3;
        else height = block.block.y - loc.y;
    } finally {
    }


    // 
    let forwardHeight = 0;
    try {
        const velocity = entity.getVelocity();
        const speed = Vec3.magnitude(velocity);
        const offVec = speed < 0.3 ? entity.getViewDirection() : Vec3.multiply(velocity, 1 / speed);
        const blockLoc = Vec3.add(loc, { x: offVec.x, y: 0, z: offVec.z });

        // add 1 to base forwardHeight if block in front is solid (jump up)
        if (dim.getBlockFromRay(blockLoc, { x: 0, y: 1, z: 0 }, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 1, excludeTags: ["one_way_collidable"] })?.block?.y === loc.y) {
            blockLoc.y += 1;
        }

        const block = dim.getBlockFromRay(blockLoc, { x: 0, y: 1, z: 0 }, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 2, excludeTags: [ "one_way_collidable" ] });
        if (block?.block == undefined) forwardHeight = 3;
        else forwardHeight = block.block.y - blockLoc.y;
    } finally {
    }


    if (isChasing) {
        let lastForwardHeight = entity.getDynamicProperty("mythicus_tw:lastForwardHeight") ?? forwardHeight;
        entity.setDynamicProperty("mythicus_tw:lastForwardHeight", forwardHeight);
        if (forwardHeight !== lastForwardHeight) {
            forwardHeight = lastForwardHeight;
        }

        if (height < 2) {
            entity.setDynamicProperty("mythicus_tw:lastHeightTime", system.currentTick + 2);
        } else if (system.currentTick < (entity.getDynamicProperty("mythicus_tw:lastHeightTime") ?? 0)) {
            height = 1;
        }
    }
    

    //if (height < 2) world.sendMessage("height < 2");
    //if (forwardHeight < 2) {
    //    world.sendMessage("forwardHeight < 2");
    //    if (dim.getBlock({ x: 79, y: 103, z: 157 })?.typeId === "minecraft:acacia_door") {
    //        world.sendMessage(dim.getBlock({ x: 79, y: 103, z: 157 }).getTags());
    //    }
    //}

    const minHeight = Math.min(height, forwardHeight);
    if (curHeight !== minHeight) {
        entity.setProperty("mythicus_tw:height", minHeight);
        if (minHeight < 2) entity.setProperty("mythicus_tw:pose", 10);

        if (isChasing && minHeight > 1 && (entity.getProperty("mythicus_tw:pose") ?? 0) !== 0) {
            entity.setProperty("mythicus_tw:pose", 0);
        }
    }

    if (isChasing && height > 2 && forwardHeight === 2) {
        checkDoorway(entity, 2);
    } else if (isChasing && height === 2 && forwardHeight > 2) {
        checkDoorway(entity, 1);
    } else if (isChasing && entity.getProperty("mythicus_tw:is_under_door") === true && system.currentTick >= (entity.getDynamicProperty("mythicus_tw:is_under_door_timer") ?? 0) && height !== 2) {
        entity.setProperty("mythicus_tw:is_under_door", false);
    }

    return minHeight;

}

/**
 * 
 *
 * @param {Entity} entity - Entity.
*/
function checkDoorway(entity, mode = 2) {
    
    const dim = entity.dimension;
    const loc = entity.location;
    loc.y = Math.floor(loc.y);

    //
    const velocity = entity.getVelocity();
    const speed = Vec3.magnitude(velocity);
    const offVec = speed < 0.3 ? entity.getViewDirection() : Vec3.multiply(velocity, 1 / speed);
    let oldBlockLoc = Vec3.add(loc, { x: offVec.x, y: 0, z: offVec.z });
    const blockLoc = Vec3.add(loc, { x: mode * offVec.x, y: 0, z: mode * offVec.z });

    if (mode === 1) {
        oldBlockLoc = loc;
    }

    const isXAxis = Math.abs(offVec.x) > Math.abs(offVec.z);

    // 
    let prevHeight = 0;
    let forwardHeight = 0;
    try {

        // cancel if the path in front isn't straight
        if (dim.getBlockFromRay(oldBlockLoc, { x: 0, y: 1, z: 0 }, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 1, excludeTags: ["one_way_collidable"] })?.block?.y === loc.y) {
            return;
        }

        // check previous block if directly under doorway
        if (mode === 1) {
            const prevBlockLoc = Vec3.subtract(loc, { x: offVec.x, y: 0, z: offVec.z });
            const prevBlock = dim.getBlockFromRay(prevBlockLoc, { x: 0, y: 1, z: 0 }, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 2, excludeTags: ["one_way_collidable"] });
            if (prevBlock?.block == undefined) prevHeight = 3;
            else prevHeight = prevBlock.block.y - prevBlockLoc.y;
        }

        const block = dim.getBlockFromRay(blockLoc, { x: 0, y: 1, z: 0 }, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 2, excludeTags: ["one_way_collidable"] });
        if (block?.block == undefined) forwardHeight = 3;
        else forwardHeight = block.block.y - blockLoc.y;
    } finally {
    }

    
    if (forwardHeight < 3) return false;
    if (mode === 1 && prevHeight < 3) return false;

    let leftHeight = 0;
    let rightHeight = 0;
    try {

        // isXAxis indicates the direction of travel--the blocks checked are opposite to this axis

        const leftBlock = dim.getBlockFromRay({ x: blockLoc.x + (isXAxis ? 0 : 1), y: blockLoc.y, z: blockLoc.z + (isXAxis ? 1 : 0) }, { x: 0, y: 1, z: 0 }, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 2 });
        if (leftBlock?.block == undefined) leftHeight = 2;
        else leftHeight = leftBlock.block.y - blockLoc.y;

        const rightBlock = dim.getBlockFromRay({ x: blockLoc.x - (isXAxis ? 0 : 1), y: blockLoc.y, z: blockLoc.z - (isXAxis ? 1 : 0) }, { x: 0, y: 1, z: 0 }, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 2 });
        if (rightBlock?.block == undefined) rightHeight = 2;
        else rightHeight = rightBlock.block.y - blockLoc.y;

    } finally {

    }

    if (leftHeight >= 2 && rightHeight >= 2 && !entity.getProperty("mythicus_tw:is_under_door")) {
        //world.sendMessage("under door");
        entity.addEffect("minecraft:slowness", 20, { amplifier: 0, showParticles: false });
        entity.setProperty("mythicus_tw:is_under_door", true);
        entity.setDynamicProperty("mythicus_tw:is_under_door_timer", system.currentTick + 3);

        return true;
    }

    return false;

}



system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (!sourceEntity || id !== "mythicus_tw:teleportBehindPlayer") return;

    const players = sourceEntity.dimension.getPlayers({ location: sourceEntity.location, maxDistance: 64 });
    if (players.length === 0) return;

    teleportBehindPlayer(players[Math.floor(Math.random() * players.length)], sourceEntity, 10, false);
});

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (!sourceEntity || id !== "mythicus_tw:teleportBehindPlayer2") return;

    const players = sourceEntity.dimension.getPlayers({ location: sourceEntity.location, maxDistance: 64 });
    if (players.length === 0) return;

    teleportBehindPlayer(players[Math.floor(Math.random() * players.length)], sourceEntity, 10, true);
});

/**
 * 
 * @param {Player} player - Player.
 * @param {Entity} entity - Entity.
*/
function teleportBehindPlayer(player, entity, dist = 10, hasSound = false) {
    
    const playerRot = player.getRotation().y * (Math.PI / 180);
    const playerLoc = player.location;
    const teleportLoc = { x: playerLoc.x + dist * Math.sin(playerRot), y: playerLoc.y, z: playerLoc.z - dist * Math.cos(playerRot) };

    //if (checkLevelGround(player.dimension, playerLoc, dist, playerRot - 180)) {
    //    world.sendMessage("level ground")
    //}

    if (!isOpenPath(entity.dimension, playerLoc, dist, playerRot - Math.PI)) {
        return;
    }

    const groundLoc = randomNearbyGround(player.dimension, teleportLoc, 2, 3);
    if (groundLoc) {
        entity.teleport(groundLoc, { facingLocation: playerLoc });
        checkAvailableHeight(entity);
        if (hasSound) entity.dimension.playSound("mythicus_tw:watcher.behind_you", groundLoc, { volume: 10 });
    }

}

/**
 * 
 * @param {Entity} entity - Entity.
*/
function teleportBehindChase(entity) {

    const player = entity.dimension.getPlayers({ location: entity.location, maxDistance: 10, closest: 1 })?.[0];
    if (!player) return;

    const dist = Mathm.randomRangeInt(3, 5);

    const playerRot = player.getRotation().y * (Math.PI / 180);
    const playerLoc = player.location;
    const teleportLoc = { x: playerLoc.x + dist * Math.sin(playerRot), y: playerLoc.y, z: playerLoc.z - dist * Math.cos(playerRot) };

    if (!isOpenPath(entity.dimension, playerLoc, dist + 1, playerRot - Math.PI)) {
        return;
    }

    const groundLoc = randomNearbyGround(player.dimension, teleportLoc, 1, 2);
    if (groundLoc) {
        entity.teleport(groundLoc, { facingLocation: playerLoc });
        checkAvailableHeight(entity);
    }

}

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (!sourceEntity || id !== "mythicus_tw:summonIllusionBehind") return;

    const players = sourceEntity.dimension.getPlayers({ location: sourceEntity.location, maxDistance: 64 });
    if (players.length === 0) return;

    summonIllusionBehind(players[Math.floor(Math.random() * players.length)]);
});

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (!sourceEntity || id !== "mythicus_tw:summonIllusionBehindCrawl") return;

    const players = sourceEntity.dimension.getPlayers({ location: sourceEntity.location, maxDistance: 8 });
    if (players.length === 0) return;

    summonIllusionBehind(players[Math.floor(Math.random() * players.length)]);
});

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (!sourceEntity || id !== "mythicus_tw:summonIllusionBehindPlayer") return;

    summonIllusionBehind(sourceEntity);
});

/**
 * 
 * @param {Player} player - Player.
 * @param {Entity} entity - Entity.
*/
function summonIllusionBehind(player, dist=4) {

    const playerRot = player.getRotation().y * (Math.PI / 180);
    const playerLoc = player.location;
    const teleportLoc = { x: playerLoc.x + dist * Math.sin(playerRot), y: playerLoc.y, z: playerLoc.z - dist * Math.cos(playerRot) };

    //if (checkLevelGround(player.dimension, playerLoc, dist, playerRot - Math.PI)) {
    //    world.sendMessage("level ground")
    //}

    const groundLoc = randomNearbyGround(player.dimension, teleportLoc, 2, 3);
    if (groundLoc) {
        player.dimension.runCommand(`summon mythicus_tw:illusion ${groundLoc.x} ${groundLoc.y} ${groundLoc.z} facing ${playerLoc.x} ${playerLoc.x} ${playerLoc.x} mythicus_tw:behind`);
        player.dimension.playSound("mythicus_tw:watcher.behind_you", groundLoc, { volume: 10 });
    }

}


system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (!sourceEntity || id !== "mythicus_tw:watcher.placeRandomFootstep") return;

    if (Mathm.randomChance(0.8 + 0.1 * AGGRESSION_LEVEL)) {
        placeFootstep(sourceEntity);
    }
});

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (!sourceEntity || id !== "mythicus_tw:placeFootstep") return;

    placeFootstep(sourceEntity);
});

/**
 * 
 * @param {Entity} entity - Entity.
*/
function placeFootstep(entity) {

    const rotation = 90 * Math.round(entity.getRotation().y / 90);
    const dir = rotation === -90 || rotation === 90 ? (rotation === -90 ? "east" : "west") : (rotation === 0 ? "south" : "north");

    //world.sendMessage(`rotation: ${rotation}, dir: ${dir}`);
    const block = entity.dimension.getBlockFromRay(entity.location, { x: 0, y: -1, z: 0 }, { includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: 1, excludeTags: ["one_way_collidable"] })?.block;
    if (block && block.y !== Math.floor(entity.location.y) && block.above()?.isAir) {
        block.above().setPermutation(BlockPermutation.resolve("mythicus_tw:footstep", { "minecraft:cardinal_direction": dir }));
    }
}

ScriptEventListener.subscribe("mythicus_tw:watcher.updateFear", ({ sourceEntity }) => {
    if (sourceEntity?.isValid() !== true) return;

    for (const player of sourceEntity.dimension.getPlayers({ location: sourceEntity.location, maxDistance: 96 })) {
        updateFear(player);
    }

});


/**
 * 
 * @param {Player} player - Player.
*/
function updateFear(player) {

    let fear = player.getDynamicProperty("mythicus_tw:fear.amount") ?? 0;

    if (fear < 140) {
        fear += 1.4;
    } else {
        fear = 0;
    }

    player.setDynamicProperty("mythicus_tw:fear.amount", fear);
    player.setDynamicProperty("mythicus_tw:fear.timer", system.currentTick + 20 * 120);

}

/**
* 
* @param {Player} player - Player.
*/
function checkFear(player) {

    if (system.currentTick > (player.getDynamicProperty("mythicus_tw:fear.timer") ?? 0)) {
        player.setDynamicProperty("mythicus_tw:fear.amount", 0);
        player.setDynamicProperty("mythicus_tw:fear.timer", system.currentTick + 20 * 120);
    }

}

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (!sourceEntity || id !== "mythicus_tw:illusion.lookAtCeil") return;

    const random = Math.random();
    if (random < 0.4) {
        return;
    } else if (random < (0.8 - 0.05 * AGGRESSION_LEVEL)) {
        sourceEntity.runCommand("scriptevent mythicus_tw:illusion.ceilingSpawnReset");
        sourceEntity.remove();
    } else {
        sourceEntity.runCommand("scriptevent mythicus_tw:illusion.ceilingSpawnReset");
        sourceEntity.runCommand("execute as @r[r=64] run scriptevent mythicus_tw:summonIllusionBehindPlayer");
        sourceEntity.remove();
    }
});

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (!sourceEntity || id !== "mythicus_tw:illusion.lookAtCrawl") return;

    system.runTimeout(() => {
        if (!sourceEntity) return;
        const random = Math.random();
        if (random < (0.7 - 0.05 * AGGRESSION_LEVEL)) {
            //sourceEntity.runCommand("scriptevent mythicus_tw:illusion.ceilingSpawnReset");
            sourceEntity.remove();
        } else {
            //sourceEntity.runCommand("scriptevent mythicus_tw:illusion.ceilingSpawnReset");
            sourceEntity.runCommand("execute as @r[r=8] run scriptevent mythicus_tw:summonIllusionBehindPlayer");
            sourceEntity.remove();
        }
    }, Mathm.randomRangeInt(25, 35));
});

system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity }) => {
    if (!sourceEntity || id !== "mythicus_tw:illusion.lookAtDefault") return;

    const lookCount = sourceEntity.getDynamicProperty("mythicus_tw:lookCount") ?? 0;

    if (Math.random() < (0.4 + 0.25 * lookCount)) {
        sourceEntity.triggerEvent("mythicus_tw:disappear");
        return;
    }

    sourceEntity.setDynamicProperty("mythicus_tw:lookCount", lookCount + 1);
});


system.afterEvents.scriptEventReceive.subscribe(({ id, sourceEntity: entity }) => {
    if (!entity || id !== "mythicus_tw:watcherChaseDamage") return;

    const watcherLoc = entity.location;
    const watcherDir = entity.getViewDirection();
    const watcherHeadLoc = { x: watcherLoc.x, y: watcherLoc.y + 1.15, z: watcherLoc.z };
    const watcherCrouchHeadLoc = { x: watcherLoc.x, y: watcherLoc.y - 0.7, z: watcherLoc.z };

    const height = checkAvailableHeight(entity);

    let isKill = false;
    for (const player of entity.dimension.getPlayers({ location: entity.location, maxDistance: 4 })) {
        const health = player.getComponent(EntityComponentTypes.Health).currentValue;
        if (health > 8) {
            //player.runCommand("/damage @s 8 override");
            player.applyDamage(8, { cause: EntityDamageCause.override, damagingEntity: entity });
            continue;
        }

        isKill = true;
        player.setDynamicProperty("mythicus_tw:grabbedByWatcher", true);

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

        player.addEffect("minecraft:slow_falling", 100, { amplifier: 5, showParticles: false });

        system.runTimeout(() => {

            if (player?.isValid() !== true && entity?.isValid() !== true) return;

            const killPose = entity.getProperty("mythicus_tw:pose") ?? 0;
            if (killPose == 0) {
                player.teleport({ x: watcherLoc.x + 2 * watcherDir.x, y: watcherLoc.y, z: watcherLoc.z + 2 * watcherDir.z }, { facingLocation: (height < 3 ? watcherCrouchHeadLoc : watcherHeadLoc) });
            } else if (killPose == 1) {
                player.teleport({ x: watcherLoc.x + 2.5 * watcherDir.x, y: watcherLoc.y, z: watcherLoc.z + 2.5 * watcherDir.z }, { facingLocation: (height < 3 ? watcherCrouchHeadLoc : { x: watcherLoc.x, y: watcherLoc.y + 0.95, z: watcherLoc.z }) });
            } else {
                if (height < 3) player.teleport({ x: watcherLoc.x + 2.4 * watcherDir.x, y: watcherLoc.y, z: watcherLoc.z + 2.4 * watcherDir.z }, { facingLocation: watcherCrouchHeadLoc });
                else player.teleport({ x: watcherLoc.x + 2.0 * watcherDir.x, y: watcherLoc.y, z: watcherLoc.z + 2.0 * watcherDir.z }, { facingLocation: { x: watcherLoc.x, y: watcherLoc.y + 0.25, z: watcherLoc.z } });

            }

            player.runCommand("camerashake add @s 0.18 2.5 positional");
            player.playSound("mythicus_tw:watcher.death_start");

        }, 5);

        system.runTimeout(() => {

            if (player?.isValid() !== true || player.getDynamicProperty("mythicus_tw:grabbedByWatcher") !== true) return;

            player.setDynamicProperty("mythicus_tw:grabbedByWatcher", false);
            player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, true);
            player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);

            player.runCommand("stopsound @s mythicus_tw:watcher.heart_beat");

            player.playSound("mythicus_tw:watcher.death_end");
            player.kill();

        }, 50);
    }

    if (isKill) {
        entity.addEffect("minecraft:slowness", 1000, { amplifier: 10, showParticles: false });
        entity.setProperty("mythicus_tw:pose", Mathm.randomRangeInt(0, 2));
        entity.triggerEvent("mythicus_tw:chase.kill");
    }
});

world.afterEvents.playerSpawn.subscribe((data) => {

    const player = data.player;

    // input permissions from watcher kill
    if (player.getDynamicProperty("mythicus_tw:grabbedByWatcher")) {
        player.setDynamicProperty("mythicus_tw:grabbedByWatcher", false);
        player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, true);
        player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);
    }

    // generic input permissions
    if (!player.inputPermissions.isPermissionCategoryEnabled(InputPermissionCategory.Camera) || !player.inputPermissions.isPermissionCategoryEnabled(InputPermissionCategory.Movement)) {
        player.inputPermissions.setPermissionCategory(InputPermissionCategory.Camera, true);
        player.inputPermissions.setPermissionCategory(InputPermissionCategory.Movement, true);
    }

    // chase effects
    if (player.getDynamicProperty("mythicus_tw:chasedByWatcher")) {
        player.setDynamicProperty("mythicus_tw:chasedByWatcher", false);
        player.stopMusic();
        player.runCommand("stopsound @s mythicus_tw:watcher.heart_beat");

    }

});
