// script file for Breath Attacks
// made by Inner Tri

// Imports
// Important Global Variables
import {
    system
} from "@minecraft/server";



// Breath Attack Types
// These specify the type, radius, distance, knockback, and damage of each Breath Attack
import {
	breath_skeleton,
	breath_spider,
	breath_dragon
} from "./breathAttackDefs"





// Global Variables
// Most Breath Attack variables are stored in each breath attack definition in breathAttackDefs.js
const numPoints = 25;

const drawDebug = false;








// Breath Attacks are called via ScriptEvents,
// ran via BP anims when certain entites are attempting to attack a target
system.afterEvents.scriptEventReceive.subscribe(
    event => {
        // first, check if the message is for us
        if (event.id === "cyn_hao:breathAttack") {
            // if it is, next check if it's a call for a breath attack
            switch (event.message) {
                case "wild_skeleton":
                    // debug is determined by world value
                    breathAttack(event.sourceEntity, breath_skeleton, true);
                return;

                case "tamed_skeleton":
                    // debug is determined by world value
                    breathAttack(event.sourceEntity, breath_skeleton, false);
                return;

				case "wild_spider":
                    // debug is determined by world value
                    breathAttack(event.sourceEntity, breath_spider, true);
                return;

                case "tamed_spider":
                    // debug is determined by world value
                    breathAttack(event.sourceEntity, breath_spider, false);
                return;

				case "wild_dragon":
                    // debug is determined by world value
                    breathAttack(event.sourceEntity, breath_dragon, true);
                return;

                case "tamed_dragon":
                    // debug is determined by world value
                    breathAttack(event.sourceEntity, breath_dragon, false);
                return;
            }
        }
    }
);



// NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE 
// if you ever have players saying they're immune to breath attacks,
// try seeing if having them run the command to delete the breathattack tag fixes it!!!
// sometimes it got "stuck" where for some reason or another, the breath attack didn't remove the tag once it was finished,
// so then successive breath attacks weren't able to impact them!!!



// Breath Attack, finally converted to script!
// This method currently has the small fringe case of being able to attack through / in walls sometimes,
// as the traces seem to go through
function breathAttack(sourceEntity, breathAttackDef, isWild = false) {

	// since there only seemed to be ways to do spheres (via maxDistance) or volumes that wouldn't rotate (via volume)
	// within EntityQueryOptions, we have to do our own approach to get an appropriately-rotating "breath volume"

	// 1. Block trace up to max distance
	// 2. set end point (either hit block before max dist, or max dist if no block was hit)
	// 3. get random points within a (ROTATED TO MATCH LOOKDIR) circle of X radius, centered on end point
	// 3.a. Additional block traces for each of those points seem to be NOT NEEDED since EntityRaycastOptions seems to check for block collision anyway?
	// 4. do entity ray traces with the above points, applying damage and knockback as appropriate



	// initialize the start and end locations of the volume
	// NOTE that getHeadLocation isn't *actually* getting the location of the "head" bone or anything like that
	// it seems to actually calculate the point that is ~10% down from the top of the entity's collision box
	// so the ray isn't actually coming from the horse's head for tamed attacks, 
	// since their collision box gets increased when players ride them (to make activating the attack easier)
	let entityHead = sourceEntity.getHeadLocation();
	let endLoc = {x:0, y:0, z:0}


	

	// NOTE ON VIEWDIR
	// horse's flying/swimming/falling animations mess with the horse's head angle,
	// and therefore their viewDir, resulting in the laser only ever going straight forward
	// instead of being angled
	// therefore, if you're going to change the below back to using the HORSE'S VIEWDIR,
	// you may need to keep the case of using the rider's when the horse is NOT on the ground
	// which is easy cuz you just need to encapsulate the block AFTER
	// let viewDir = sourceEntity.getViewDirection();
	// to be in an if (!sourceEntity.isOnGround) {} statement

	// determine viewDir
	let viewDir = sourceEntity.getViewDirection();

	// if horse has a rider, use their viewDir
	// otherwise, use the horse's
	let rideableComp = sourceEntity.getComponent("minecraft:rideable")
	// I don't really like nesting if's,
	//  but in this case it seems like it makes sense since we need to check for validity first
	if (rideableComp != undefined) {
		// if the entity is able to have riders, next check if it does
		let riders = rideableComp.getRiders()
		if (rideableComp.getRiders().length > 0) {
			// finally, if there is a rider, set viewDir to be theirs
			// console.warn(`Horse has rider, using ${riders[rideableComp.controllingSeat].name}'s viewDir instead.`)
			viewDir = riders[rideableComp.controllingSeat].getViewDirection()
		}
	}
	


	// do a ray (line) trace to see if there is a block "in the way of" the volume
	let blockRay = sourceEntity.getBlockFromViewDirection({includeLiquidBlocks: false, includePassableBlocks: false, maxDistance: breathAttackDef.distance});
	
	// if a block is hit, that's where the end point should be
	// note that we add faceLocation to the block's location to get the ACTUAL hit coords
	// (as compared to the coords of the block that was hit)
	if (blockRay != undefined) {
		endLoc = vectorAdd(blockRay.block.location, blockRay.faceLocation);
	}

	// if no block was hit, then the end point is X blocks in front of the sourceEntity
	else {
		endLoc = vectorAdd(entityHead, vectorScale(viewDir, breathAttackDef.distance))
	}



	// get random points within a (ROTATED TO MATCH LOOKDIR) circle of X radius, centered on end point
	let breathPoints = pointsInCircle3D(endLoc, sourceEntity.getViewDirection(), breathAttackDef.radius)



    // show debug if appropriate
    // this should better allow us to help players that have problems,
    // and / or troubleshoot said problems via player videos
    if (drawDebug) {
        sourceEntity.dimension.spawnParticle("minecraft:blue_flame_particle", endLoc);
        for(let point of breathPoints){
        	sourceEntity.dimension.spawnParticle("minecraft:basic_flame_particle", point)
        	// DEBUG - show traces FROM head TO point
        	// NOTE that you can bump i to 100 or 1000 and do (0.01 * i) or (0.001 * i) respectively 
        	// to visually SEE (via lag lol) the direction the rays are going!
        	for(let i = 0; i < 10; i++) {
        		sourceEntity.dimension.spawnParticle("minecraft:basic_flame_particle",
        			vectorAdd(entityHead, vectorScale(vectorSubtract(point, entityHead), (0.1 * i)))
        		)
        	}
        }
    }
	
    



	// Again, additional block traces for each of those points seem to be NOT NEEDED 
	// since EntityRaycastOptions seems to check for block collision anyway?





	let exclusionIds = [ "minecraft:item", "minecraft:xp_orb", "minecraft:armor_stand" ];
	let hitEntities = [];

	// make the attacker immune to it's own damage
	// this has the side effect of if any horse attacks another attacking horse,
	// they won't take damage
	// but hopefully that is a rare enough occasion
	sourceEntity.addTag("cyn_hao:breathSelf")

	// set whether players should be hit or not
	let excludeFamilies = [];
	if(!isWild) {
		exclusionIds.push("minecraft:player");
		excludeFamilies.push("cyn_hao:tamed_horse");
	}

	for(let point of breathPoints) {
		let rayResults = sourceEntity.dimension.getEntitiesFromRay(entityHead, vectorSubtract(point, entityHead), {
		// do not hit entities with the breathHit tag, as they have already been hit by another ray!
		excludeTags: ["cyn_hao:breathHit", "cyn_hao:breathSelf" ],
		// ignore players (conditionally), items, xp orbs, and armor stands
		excludeTypes: exclusionIds,
		// ignore tamed horses
		excludeFamilies: excludeFamilies
		// tame check is done below
		});

		if(rayResults.length > 0) {
			for(let result of rayResults) {
				// entity validity checks

				// first, IF THE ENTITY IS NOT WILD, check if it is tamed
				// while this might look redundant since the ray already excludes tamed horses,
				// it's only that: tamed horses of *OURS* - other Add-Ons obviously won't use cyn_hao:tamed_horse!
				if(!isWild && (result.entity.hasComponent("minecraft:is_tamed"))) {
					// if so, do nothing
					// console.warn("Breath Ray hit tamed entity, returning.")
					return;
				}



				// if the entity passed all validation checks, tag them and add them to the list
				result.entity.addTag("cyn_hao:breathHit");
				hitEntities.push(result.entity)
			}
		}
	}

	// if nothing was hit, return
	if (hitEntities.length <= 0) {
		// console.warn(`Breath Attack from ${sourceEntity.typeId} failed to hit any targets.`)
	}

	// otherwise, if entities were hit, 
	// go through the list and deal damage (noting that this entity is the damage source),
	// apply effects,
	// apply knockback,
	// and remove their tag for the next "round" of breath attacks
	for(let hitEntity of hitEntities) {
		// first, remove the tag from the entity
		// this was last, but now that we have a potential "continue" statement,
		// it needs to be first
		// shouldn't ultimately matter, though
		hitEntity.removeTag("cyn_hao:breathHit");

		// call the function that will check for shields
		// this function also handles player shield hit sound and damaging the shield, if applicable
		if (playerUsingShield(hitEntity)) {
			// if the hit entity was a player who was using their shield,
			// do not deal damage to them and simply continue
			// console.warn("Player had shield. Skipped damaging player.")
			continue;
		}
		else {
			// otherwise, if the hit entity was NOT a player who was using their shield,
			// deal damage
			hitEntity.applyDamage(breathAttackDef.damage, {cause: "magic", damagingEntity: sourceEntity});
			if(breathAttackDef.effectType != "") {
				hitEntity.addEffect(breathAttackDef.effectType, breathAttackDef.durationTicks, {amplifier: breathAttackDef.amplifier, showParticles: breathAttackDef.showParticles})
			}

			// knockback is in the direction the sourceEntity is looking, with force defined at the top of the file
			// knockback function was changed in scripting 2.0, so this might not work as intended anymore
			// specifically, all breath attacks might use the same knockback now since breathAttackDef.knockback.x isn't being used anymore
			hitEntity.applyKnockback({x: viewDir.x, z: viewDir.z}, breathAttackDef.knockback.y);
		}
	}
	
	// after all attacks are finished, remove the self tag
	sourceEntity.removeTag("cyn_hao:breathSelf")

}



function playerUsingShield(entity) {
	// if the entity in question IS a player
	if (entity.typeId === "minecraft:player") {
		// if that player IS sneaking
		if (entity.isSneaking) {

			// if that player does not have an invalid equippable component (*should* never happen)
			const equippableComponent = entity.getComponent("minecraft:equippable");
			if (!equippableComponent) {
				// console.warn("Player does not have an 'minecraft:equippable' component. They will be damaged.");
			}



			else {
				// if the player DOES have a valid equippable component,
				// check their main and off hands for a shield
				// done in a for loop to condense the code
				
				let checkSlots = ["Mainhand", "Offhand"]
				for (let slot of checkSlots) {
					let currentEquipment = equippableComponent.getEquipment(slot);
					// if there is anything in this slot, and it is a shield
					if((currentEquipment != undefined) && (currentEquipment.typeId === "minecraft:shield")) {
						// if the player IS holding a shield in this hand,
						// play the block sound
						entity.dimension.playSound("item.shield.block", entity.location)
						
						

						// Below code is for damaging the shield, 
						// but since Minecraft doesn't inherently / automatically handle the breaking of items
						// when damage == maxDurability, and I didn't feel like re-creating the mechanic here
						// (cuz imo it really should handle that itself...),
						// we opted to just have shields not take damage
						// benefits the players anyway, so not too big of a deal
						// maybe in the future if they ever add a "breakItem" or "applyDamage" function to ItemStack or some such,
						// we can come back and re-enable durability damage
						// this also has the benefit of not having to worry about unbreaking and other enchantments

						// // damage the shield, if it has durability
						// // not really sure how it WOULDN'T have durability, but. you know.
						// const shieldDurComp = currentEquipment.getComponent("minecraft:durability")
						// if (!shieldDurComp) {
						// 	console.warn("Shield somehow has no durability component. No damage will be dealt to it.");
						// }
						// else {
						// 	// otherwise, if there is a valid durability component, 
						// 	// damage the shield by the breath attack's damage
						// 	// technically, shields only take damage if the attack's damage was >= 3,
						// 	// and most of our breath attacks only deal 1, but breath attacks are special, so.
						//	// NOTE that this does NOT take enchantments into account!!!
						// 	shieldDurComp.damage += 1;

						// 	// as always, to update the equipment we need to use setEquipment
						// 	equippableComponent.setEquipment(slot, currentEquipment)
						// 	console.warn(shieldDurComp.damage)
						// }



						// if there was a shield that was hit, regardless of if damage was dealt or not,
						// return that this WAS a player who was using their shield
						return true;
					}
				}
			}
		}
	}

	// So, to recap, the above code will return false to NOT damage the entity if...
	// Entity is a Player,
	// Player is Sneaking (since that's how you use a shield in Bedrock),
	// Sneaking Player has an EquippableComponent,
	// EquippableComponent has a Shield in EITHER Mainhand or Offhand

	// additionally, if there was a shield in mainhand or offhand,
	// the block sound will be played and, if possible, the shield will be damaged

	// I don't like to do nested if statements, but in this case I think it's neccessary since the early returns are needed


	

}






// FULL DISCLOSURE, pointsInCircle3D WAS COPIED FROM GOOGLE SEARCH AI RESPONSE
// I'm sure it ripped it from StackExchange or something similar itself...



// Generate random points in the circle centered at endLoc
// a better approach would probably generating normal vectors that randomly deviate
// from forward (entity.getViewDirection()), and then scaling them equally to make them all go the same distance,
// but I can't quite wrap my head around how to do that 
// since we'd want a circle, not square, and adding random values between +N and -N would result in the square of N, N to -N, -N
function pointsInCircle3D(center, normal, radius) {
  // Normalize the normal vector
  let normalizedNormal = vectorNormalize(normal);

  // Create two orthogonal vectors to the normal vector
  let [vectorU, vectorV] = createOrthogonalVectors(normalizedNormal);

  let points = [];
  for (let i = 0; i < numPoints; i++) {
    // Generate random angle and radius
    let angle = Math.random() * 2 * Math.PI;
    let randomRadius = Math.random() * radius;

    // Calculate the point in 3D space
    points.push({
		x:center.x + randomRadius * (Math.cos(angle) * vectorU.x + Math.sin(angle) * vectorV.x),
		y:center.y + randomRadius * (Math.cos(angle) * vectorU.y + Math.sin(angle) * vectorV.y),
		z:center.z + randomRadius * (Math.cos(angle) * vectorU.z + Math.sin(angle) * vectorV.z)
	});
  }
  return points;
}

// Helper function for pointsInCircle3D
// it's been WAYYY too long since I've done this stuff to remember / understand how it works
// but it does work though!
function createOrthogonalVectors(normal) {
    let vectorU, vectorV;

    if (Math.abs(normal[0]) < 0.9) {
        vectorU = vectorCross({x: 1, y: 0, z: 0}, normal);
    } else {
        vectorU = vectorCross({x: 0, y: 1, z: 0}, normal);
    }

    let normalizedVectorU = vectorNormalize(vectorU);

    vectorV = vectorCross(normal, normalizedVectorU);

    return [normalizedVectorU, vectorNormalize(vectorV)];
}





// Functions duplicated from minecraft-math,
// since that's WAY easier than going through the whole process for just a few vector operations!

function vectorAdd(v1, v2) {
	return { x: v1.x + v2.x, y: v1.y + v2.y, z: v1.z + v2.z };
}

function vectorSubtract(v1, v2) {
	return { x: v1.x - v2.x, y: v1.y - v2.y, z: v1.z - v2.z };
}

function vectorCross(a, b) {
	return {
		x: a.y * b.z - a.z * b.y,
		y: a.z * b.x - a.x * b.z,
		z: a.x * b.y - a.y * b.x
	};
}

function vectorMagnitude(v) {
	return Math.sqrt(v.x ** 2 + v.y ** 2 + v.z ** 2);
}

function vectorNormalize(v) {
	const mag = vectorMagnitude(v);
	return { x: v.x / mag, y: v.y / mag, z: v.z / mag };
}

function vectorScale(v1, scale) {
	return { x: v1.x * scale, y: v1.y * scale, z: v1.z * scale };
}