::ZAPPER_REACTION_TIME <- 0.2 //When we see a new target, take this long before we're legally allowed to react
::ZAPPER_FORGET_TIME <- 5 //When we don't see anyone after seeing a new target, stay on guard for this long before needing ZAPPER_REACTION_TIME seconds to react again
::ZAPPER_CHARGEUP_LENGTH <- 2 //After we get mad at someone, charge up a shot for this long
::ZAPPER_DAMAGE_TYPE <- DMG_ACID + DMG_BLAST //Acid = crits
::ZAPPER_BASE_DAMAGE <- 100 //Base damage on direct hit. Blast type, always crits. Players with no resistances take 450 damage due to crits.
::ZAPPER_BUILDING_DAMAGE_MULTIPLIER <- 6
::ZAPPER_BUILDING_STUN_DURATION <- 2.5 //On direct hits only, disable buildings for this long. Prevents wrangler tanking from being too good.
::ZAPPER_EXPLOSION_RADIUS <- 146 //Radius of collateral explosion from hitting walls or other players. Players that are already hit by a direct attack are exempt from their own collateral explosion damage
::ZAPPER_EXPLOSION_DAMAGE <- 50 //Maximum damage from collateral explosion. Likely will never hit this exact number but gotta start somewhere.
::ZAPPER_EXPLOSION_FALLOFF_FACTOR <- 2.88 //It's the same explosive falloff as rockets dont ask further
::ZAPPER_MAXIMUM_EXPLOSION_MULTIPLIER <- 5 //Knockback strength is defined as distance to explosion / player pos. This caps that number in each axis.
::ZAPPER_EXPLOSION_KNOCKBACK_FACTOR <- -75 //Multiply explosive knockback power by this much. Make it negative.
::ZAPPER_COOLDOWN_TIME <- 2 //After firing a shot, must wait this long before being able to fire another
::ZAPPER_FIRING_INTERVAL <- ZAPPER_CHARGEUP_LENGTH + ZAPPER_COOLDOWN_TIME

::playerThink <- function() {
	foreach(name, func in thinkTable) {
		func()
	}
	return -1
}

::playSound <- function(soundName, originEntity, flags=0, singlePlayer=false) {
	local filter = singlePlayer ? RECIPIENT_FILTER_SINGLE_PLAYER : RECIPIENT_FILTER_GLOBAL
	local soundTable = {
		sound_name = soundName,
		channel = 6,
		entity = originEntity,
		filter_type = filter
	}
	if(flags != 0) {
		soundTable.flags <- flags
	}
	EmitSoundEx(soundTable)
}

::evaluateTargetValidity <- function(target) {
	if(target.GetTeam() != TF_TEAM_RED) {
		return false
	}
	if(target.IsPlayer()) {
		if(!target.InCond(TF_COND_DISGUISED) && !target.InCond(TF_COND_STEALTHED)) {
			return true
		}
	}
	if(target.GetClassname() in {"obj_sentrygun": true, "obj_dispenser": true}) {
		return true
	}
	return false
}

::zapperCallbacks <- {
	function cleanup() {
		delete ::zapperCallbacks;
		// EntFire("zapper*", "Kill")
    }

	function OnGameEvent_player_spawn(params) {
		local player = GetPlayerFromUserID(params.userid)
		if(player == null) return
		
		if(params.team == 0) { //both humans and bots
			player.ValidateScriptScope()
			local scope = player.GetScriptScope()
			scope.thinkTable <- {}
			scope.IsDeployingBomb <- false
			AddThinkToEnt(player, "playerThink")
		}

		if(!IsPlayerABot(player) || player.GetTeam() != TF_TEAM_BLUE) {
			return;
		}

		//Delay tag add since robots dont have cond 51 for a split second when they spawn
		EntFireByHandle(player, "RunScriptCode", "zapperCallbacks.addThink(self)", 0.2, null, null);
	}

	function calculatePushPower(playerOrigin, explosionPos) {
		local delta = playerOrigin - explosionPos

		local pushPower = (ZAPPER_EXPLOSION_RADIUS / delta)

		//Cap to ZAPPER_MAXIMUM_EXPLOSION_MULTIPLIER in both directions
		pushPower = pushPower < (ZAPPER_MAXIMUM_EXPLOSION_MULTIPLIER * -1) ? (ZAPPER_MAXIMUM_EXPLOSION_MULTIPLIER * -1) : pushPower > ZAPPER_MAXIMUM_EXPLOSION_MULTIPLIER ? ZAPPER_MAXIMUM_EXPLOSION_MULTIPLIER : pushPower

		pushPower = pushPower * ZAPPER_EXPLOSION_KNOCKBACK_FACTOR
		return pushPower
	}

	//Handle zapper explosions. Buildings are not affected by explosion dmg but take multiplied damage from direct hits
	function doZapperExplosion(explosionPos, explosionPosEnt, attacker, attackerWeapon, zappedPlayers) {

		DispatchParticleEffect("zapper_explosion", explosionPos, Vector())

		// playSound("misc/halloween/spell_mirv_explode_secondary.wav", explosionPosEnt)

		// DebugDrawCircle(explosionPos, Vector(255, 0, 0), 127, 146, true, 5)

		local victim = null

		while(victim = Entities.FindByClassnameWithin(victim, "player", explosionPos, ZAPPER_EXPLOSION_RADIUS)) {
			if(victim.GetTeam() == TF_TEAM_RED) {
				//Don't double hit players that are already hit by the direct
				if(victim in zappedPlayers) {
					// ClientPrint(null, 3, "Skipping zapped victim")
					continue
				}

				local distance = (victim.GetCenter() - explosionPos).Length()

				local explosionTrace = {
					start = explosionPos
					end = victim.GetCenter()
				}
				TraceLineEx(explosionTrace)

				if(explosionTrace.hit && explosionTrace.enthit != victim) {
					// ClientPrint(null, 3, "No LOS, skip victim")
					//not los, don't damage them
					continue
				}

				local splash = distance / ZAPPER_EXPLOSION_FALLOFF_FACTOR
				local explosionDamage = ZAPPER_EXPLOSION_DAMAGE * (1 - splash / 100)

				victim.TakeDamageEx(attacker, attacker, attackerWeapon, Vector(1, 1, 1), explosionPos, explosionDamage, ZAPPER_DAMAGE_TYPE)

				zappedPlayers[victim] <- true

				//Maths for knockback
				local playerOrigin = victim.GetOrigin()

				local pushPowerX = zapperCallbacks.calculatePushPower(playerOrigin.x, explosionPos.x)
				local pushPowerY = zapperCallbacks.calculatePushPower(playerOrigin.y, explosionPos.y)
				local pushPowerZ = zapperCallbacks.calculatePushPower(playerOrigin.z, explosionPos.z)

				victim.ApplyAbsVelocityImpulse(Vector(pushPowerX, pushPowerY, pushPowerZ))

				playSound("MVM.ZapperSoldierSplashHit", victim, 0, true)
			}
		}
	}
	
	function deleteRocket(player, rocketlauncher) {
		StopSoundOn("Weapon_RPG_DirectHit.SingleCrit", player)
		rocketlauncher.AcceptInput("DispatchEffect", "ParticleEffectStop", null, null)
		local rocket = null
		while(rocket = Entities.FindByClassname(rocket, "tf_projectile_rocket")) {
			if(rocket.GetOwner() == player) {
				rocket.Kill()
			}
			break;
		}
	}

	function addThink(player) {
		if(!player.IsMiniBoss() && player.GetPlayerClass() != TF_CLASS_SOLDIER) {
			return;
		}

		if(!player.HasBotTag("zapper_soldier")) {
			return;
		}

		local scope = player.GetScriptScope();
		IncludeScript("thunderstorm/commonlaserfuncs.nut", scope)

		local weapon = null;
		for(local i = 0; i < 8; i++) {
			weapon = NetProps.GetPropEntityArray(player, "m_hMyWeapons", i);
			if(weapon == null || weapon.GetClassname() != "tf_weapon_rocketlauncher_directhit") {
				continue;
			}
			break;
		}

		scope.rocketlauncher <- weapon;
		NetProps.SetPropFloat(weapon, "m_flNextPrimaryAttack", Time() + 100) 
		
		scope.zapperProp <- createCosmetic("models/props_thematic_event/c_zapper.mdl", player)
		NetProps.SetPropString(scope.zapperProp, "m_iName", "zapper_ornament" + player.entindex().tostring())

		NetProps.SetPropInt(scope.rocketlauncher, "m_nRenderMode", kRenderTransColor);
		NetProps.SetPropInt(scope.rocketlauncher, "m_clrRender", 0);

		// scope.rocketlauncher.SetModelScale(0.0001, 0)

		scope.triggerParticle <- SpawnEntityFromTable("trigger_particle", {
			particle_name = "zapper_chargeup",
			attachment_type = 4,
			attachment_name = "muzzle",
			spawnflags = 64
		});

		scope.cdParticle <- SpawnEntityFromTable("info_particle_system", {
			targetname = "zapper_cd" + player.entindex().tostring(),
			effect_name = "zapper_cd_left",
			start_active = 1
		});

		scope.laserSightEnd <- SpawnEntityFromTable("info_particle_system", {
			targetname = "zapper_sightend" + player.entindex().tostring()
		});

		scope.laserSightParticle <- SpawnEntityFromTable("info_particle_system", {
			targetname = "zapper_laser_sight" + player.entindex().tostring(),
			effect_name = "zapper_laser_sight_beam",
			start_active = 0
		});

		scope.lightningParticle <- SpawnEntityFromTable("info_particle_system", {
			targetname = "zapper_lightning" + player.entindex().tostring(),
			effect_name = "zapper_lightning",
			start_active = 0
		});

		scope.laserSightParticle.AcceptInput("SetParent", "zapper_ornament" + player.entindex().tostring(), null, null)
		scope.laserSightParticle.AcceptInput("SetParentAttachment", "muzzle", null, null)
		NetProps.SetPropEntityArray(scope.laserSightParticle, "m_hControlPointEnts", scope.laserSightEnd, 0)

		scope.cdParticle.AcceptInput("SetParent", "!activator", player, player)
		scope.cdParticle.AcceptInput("SetParentAttachment", "eye_boss_1", null, null)

		scope.lightningParticle.AcceptInput("SetParent", "zapper_ornament" + player.entindex().tostring(), null, null)
		scope.lightningParticle.AcceptInput("SetParentAttachment", "muzzle", null, null)
		NetProps.SetPropEntityArray(scope.lightningParticle, "m_hControlPointEnts", scope.laserSightEnd, 0)

		scope.startedTracking <- -1
		scope.stoppedTracking <- -1
		scope.chargeupTime <- -1
		scope.cooldownTime <- -1
		scope.vulnerableFirstTime <- false
		scope.lastTarget <- null
		scope.visionState <- VISION_STATE_NOTARGET
		scope.laserState <- LASER_STATE_INACTIVE
		scope.potentiallyReactablePlayers <- {}
		scope.spottedPlayersForgetTimes <- {}
		scope.zappedPlayers <- {}

		//cleans up when bot dies or is otherwise no longer active
		scope.thinkTable.zapperThink <- function() {
			if(NetProps.GetPropInt(self, "m_lifeState") != 0) {
				delete thinkTable.zapperThink;

				if(triggerParticle.IsValid()) {
					triggerParticle.Kill();
				}

				if(cdParticle.IsValid()) {
					cdParticle.Kill();
				}

				if(laserSightParticle.IsValid()) {
					laserSightParticle.Kill();
				}

				if(laserSightEnd.IsValid()) {
					laserSightEnd.Kill();
				}

				if(lightningParticle.IsValid()) {
					lightningParticle.Kill();
				}

				if(zapperProp.IsValid()) {
					zapperProp.AcceptInput("DispatchEffect", "ParticleEffectStop", null, null);
					zapperProp.Kill()
				}

				//Clean up any sounds that may still be playing
				playSound("MVM.ZapperSoldierChargeup", self, 4)

				return
			}

			//In spawn: dont bother
			//Trace is still done for laser sight
			if(self.InCond(TF_COND_INVULNERABLE_HIDE_UNLESS_DAMAGED)) return

			//We're deploying the bomb, stop everything!
			if(IsDeployingBomb) return

			visionTrace.start = self.EyePosition()
			visionTrace.end = self.EyePosition() + self.EyeAngles().Forward() * 4096.0
			TraceLineFilter(visionTrace)

			laserSightEnd.SetAbsOrigin(visionTrace.endpos)

			//Functions to execute once the moment the bot drops down from spawn
			if(!vulnerableFirstTime) {
				vulnerableFirstTime = true
				laserSightParticle.AcceptInput("Start", null, null, null)
			}

			if(visionTrace.hit) {
				local enthit = visionTrace.enthit
				if(evaluateTargetValidity(enthit)) {
					//saw a player for first time, potentially get mad at them
					if(!(enthit in potentiallyReactablePlayers) && !(enthit in spottedPlayersForgetTimes)) {
						potentiallyReactablePlayers[enthit] <- Time()
					}
					else if(enthit in spottedPlayersForgetTimes) {
						//time to get mad
						if(laserState == LASER_STATE_INACTIVE) {
							laserState = LASER_STATE_CHARGING
							chargeupTime = Time()
						}
						spottedPlayersForgetTimes[enthit] = Time()
					}
				}
				
				//these two probably should be done outside of the trace result
				foreach(player in potentiallyReactablePlayers.keys()) {
					if(hasEnoughTimePassed(potentiallyReactablePlayers[player], ZAPPER_REACTION_TIME)) {
						delete potentiallyReactablePlayers[player]
						spottedPlayersForgetTimes[player] <- Time()
					}
				}
				
				foreach(player in spottedPlayersForgetTimes.keys()) {
					if(hasEnoughTimePassed(spottedPlayersForgetTimes[player], ZAPPER_FORGET_TIME)) {
						delete spottedPlayersForgetTimes[player]
					}
				}	
			}
			
			//delay firing until it's time to fire for real
			NetProps.SetPropFloat(rocketlauncher, "m_flNextPrimaryAttack", Time() + 10) 
			
			switch(laserState) {
				case LASER_STATE_CHARGING:
					//Just entered chargeup, do these only once per chargeup routine
					//Start charging up particle!!
					triggerParticle.AcceptInput("StartTouch", "!activator", zapperProp, zapperProp)
					playSound("MVM.ZapperSoldierChargeup", self)
					laserState = LASER_STATE_FIRING
					break;
				case LASER_STATE_FIRING:
					//Time to actually shoot
					if(hasEnoughTimePassed(chargeupTime, ZAPPER_CHARGEUP_LENGTH)) {
						chargeupTime = -1
						cooldownTime = Time()

						NetProps.SetPropFloat(rocketlauncher, "m_flNextPrimaryAttack", Time())
						self.PressFireButton(1)
						
						//recoil requires a rocket to be actually fired, so delete all existence of the rocket
						EntFireByHandle(self, "RunScriptCode", "zapperCallbacks.deleteRocket(self, rocketlauncher)", -1, null, null)
						
						laserTrace.start = self.EyePosition()
						laserTrace.end = self.EyePosition() + self.EyeAngles().Forward() * 4096.0
						TraceLineGather(laserTrace);

						laserSightEnd.SetAbsOrigin(laserTrace.endpos)

						if (laserTrace.hits.len() > 0)
						{
							ScreenShake(laserTrace.endpos, 15, 15, 1, 600, 0, false)
							foreach (i, hit in laserTrace.hits)
							{
								local enthit = hit.enthit
								
								//Direct hit damage
								if(enthit.IsPlayer()) {
									enthit.TakeDamageEx(self, self, rocketlauncher, Vector(1, 1, 1), hit.endpos, ZAPPER_BASE_DAMAGE, ZAPPER_DAMAGE_TYPE)
									zappedPlayers[enthit] <- true //Prevent players that have suffered a direct hit from getting double donk insta killed
									playSound("MVM.ZapperSoldierDirectHit", enthit, 0, true) //Players that were hit directly receive a sound to let them know they messed up bad
								}
								else {
									//Buildings take multiplied dmg and get disabled for a second
									//Implementation doesnt allow for stun to stack but I don't want it to anyways
									
									enthit.TakeDamageEx(self, self, rocketlauncher, Vector(1, 1, 1), hit.endpos, ZAPPER_BASE_DAMAGE * ZAPPER_BUILDING_DAMAGE_MULTIPLIER, ZAPPER_DAMAGE_TYPE)

									EntFireByHandle(enthit, "Disable", null, -1, null, null)
									EntFireByHandle(enthit, "Show", null, ZAPPER_BUILDING_STUN_DURATION, null, null)
								}

								//Collateral explosion damage. Don't re-hit players that have already been hit
								zapperCallbacks.doZapperExplosion(hit.endpos, laserSightEnd, self, rocketlauncher, zappedPlayers)
							}
						}

						zapperCallbacks.doZapperExplosion(laserTrace.endpos, laserSightEnd, self, rocketlauncher, zappedPlayers)

						//Stop chargeup particle
						zapperProp.AcceptInput("DispatchEffect", "ParticleEffectStop", null, null);

						lightningParticle.AcceptInput("Start", null, null, null)
						EntFireByHandle(lightningParticle, "Stop", null, 0.5, null, null)

						playSound("MVM.ZapperSoldierFire", self)
						
						//done zapping people
						zappedPlayers.clear()
						laserState = LASER_STATE_COOLDOWN
					}
					break;
				case LASER_STATE_COOLDOWN:
					//Cooled down enough, tell the rest of the code that we're ready to fire again
					if(hasEnoughTimePassed(cooldownTime, ZAPPER_COOLDOWN_TIME)) {
						cooldownTime = -1
						laserState = LASER_STATE_INACTIVE
					}
					break;
				default:
					break;
			}
			return;
		}
	}
}

__CollectGameEventCallbacks(zapperCallbacks);