#include "scripts/common.txt"

const array<int> g_KnifeRaptorHits          = { 12, 12, 12 };
const array<int> g_KnifeHumanHits           = { 15, 15, 15 };
const array<int> g_knifeDimetrondonHits     = { 12, 12, 12 };
const array<int> g_KnifeTriceratopsHits     = { 1, 1, 1 };
const array<int> g_KnifeSubterraneanHits    = { 1, 1, 1 };
const array<int> g_KnifeStalkerHits         = { 12, 12, 12 };
const array<int> g_KnifeAlienHits           = { 12, 12, 12 };
const array<int> g_KnifePurlinHits          = { 13, 13, 13 };
const array<int> g_KnifeRobotHits           = { 1, 1, 1 };
const array<int> g_KnifeSewerCrabHits       = { 12, 12, 12 };
const array<int> g_KnifePlantHits           = { 12, 12, 12 };

const int g_KnifeMortalWoundHits    = 30;
const int g_KnifeMortalDeathHits    = 100;
const int g_KnifeTrexHits           = 25;
const int g_KnifeCampaignerHits     = 20;
const int g_KnifeLonghunterHits     = 15;
const int g_KnifeMantisHits         = 25;

const float ChronoAmmoTime = 600.0f;
float playerChronoChargeTime;
bool noAttackEnemies = false;

const float playerRopeFriction = 0.25f;
const float playerRopeSpeed = 30.0f;
bool playerIsRoping;
kVec3 playerRopePoint;
kActor@ playerRopeActor;
kVec3 playerRopeActorLP;
float playerRopeActorDistTime;
bool isPlayerCheating = false;
bool didEditorWarp = false;
bool loadedWrongVersion = false;
bool weaponUpgraded = false;
array<kStr> wrongVersionMsg;
array<kActor@> ropePoints; //in current map
array<kActor@> pickupActors; //all the actors that can pickup spawnmp items

const float PLAYER_DASH_LENGTH = 0.5f; //0.4f;
const float PLAYER_DASH_INPUT_TIME = 0.25f;
const float PLAYER_DASH_COOLDOWN = 1.5f; //0.5f;

bool gameInitialStart;
kStr sHash;
kStr sDollar;

class TurokPlayer : ScriptObjectPlayer
{
    kPuppet @self;
    kActor @m_pWarpBuzzActor;
    kVec3 m_vBloodVector;
    kVec3 m_vStabVector;
    kVec3 m_vShoveVector;
    float m_damageFloorTime;
    float m_initialFriction;
    float m_nBubbles;
    float m_bubblesSfxTimer;
    float m_shoveTime;
	bool wasUnderWater;
	float playLastMusicDelay;
	float fallTime;
	int character;
	uint16 buttons;
	
	bool airDashEnabled;
	float dashInputTime; //double tap dash time (time between inputs)
	float dashCooldown; //time until can dash again
	float dashTimeLength; //How long the dash effect last has left
	float dashForwardSpeed = 15.0f; //8.0f;
	float dashJumpSpeed = 3.0f; //5.0f;
	float dashForwardDirectionX;
	float dashForwardDirectionY;
	float dashLastYawOffset;

	bool charCanGroundDash;
	bool charCanAirDash;
	int lastAnimID = -1;
	bool wasClimbThurst;
	float climbThrustTime;
	bool wasJumping;
	bool wasGrounded;
	kVec3 lastAirVelocity;
	float underwaterTime;
	float lastAnimTrackTime;
	int lastAreaID = -1;
	bool lastAreaWasSpecialWater = false;
	float airTime; //amount of time not on the ground
	int lastCurrentWeapon = -1;
	float initialGravity;
	float sectorGravity;
	float statsTime;
	//------------------------------------------------------------------------------------------------------------------------
    TurokPlayer(kPuppet @actor)
    {
		if (!gameInitialStart)
		{
			gameInitialStart = true;
			kActor@ initDefActor = ActorFactory.Spawn("BP_StartUpDef", 0.0f, 0.0f, 0.0f, 0.0f, 0);
			kDictMem@ def = initDefActor.Definition();
			def.GetString("hash", sHash);
			def.GetString("dollar", sDollar);
			initDefActor.Remove();
		}

        @self = actor;
        @m_pWarpBuzzActor = null;
        m_damageFloorTime = 0.0f;
        m_initialFriction = 0;
        m_nBubbles = 0;
        m_bubblesSfxTimer = 0;
        m_shoveTime = 0;
		airTime = 0.0f;
		lastCurrentWeapon = -1;
		sectorGravity = 0.0f;
		
		playerChronoChargeTime = GetChronoTime();
    }
	//------------------------------------------------------------------------------------------------------------------------
    bool InSpiritWorld(void)
    {
        if (Game.GetCurrentMapID() < 26 || Game.GetCurrentMapID() > 41)
        {
            return false;
        }
        
        return true;
    }
	//------------------------------------------------------------------------------------------------------------------------
    void DoSplashBubbles(void)
    {
        float cnt;
        
        if(m_nBubbles <= 0)
        {
            return;
        }
        
        m_nBubbles -= GAME_FRAME_TIME;
        
        if(m_nBubbles < 0)
        {
            m_nBubbles = 0;
        }
        
        if(m_nBubbles < m_bubblesSfxTimer)
        {
            self.PlaySound("sounds/shaders/underwater_swim_1.ksnd");
            m_bubblesSfxTimer = 0;
        }
        
        cnt = m_nBubbles / 6;
        if(cnt < 1)
        {
            cnt = 1;
        }
        
        for(int i = 0; i < int(cnt); ++i)
        {
            self.SpawnFx("fx/water_bubble.kfx", Math::vecZero);
        }
    }
    
    /*
    ==============================================================
    DoWarpSounds
    ==============================================================
    */
    
    void DoWarpSounds(void)
    {
        if(m_pWarpBuzzActor is null)
        {
            return;
        }
        
        float time = float(PlayLoop.Ticks());
        
        m_pWarpBuzzActor.Origin() = self.Origin();
        
        m_pWarpBuzzActor.Origin().x += (GAME_SCALE*15) * Math::Sin(Math::Deg2Rad(time * 6.0f)) + self.Yaw();
        m_pWarpBuzzActor.Origin().y += (GAME_SCALE*15) * Math::Cos(Math::Deg2Rad(time * 7.0f)) + self.Yaw();
        
        m_pWarpBuzzActor.PlaySound("sounds/shaders/generic_185.ksnd");
    }
    
    /*
    ==============================================================
    IntroCinematicEvent
    ==============================================================
    */
    
    void IntroCinematicEvent(kActor @instigator, const float w, const float x, const float y, const float z)
    {
        if(Game.GetCurrentMapID() != 43)
        {
            return;
        }
        
        Game.CallDelayedMapScript(3, instigator, 0);
    }
	//------------------------------------------------------------------------------------------------------------------------
	void SetupModel()
	{
		kActor@ actor =  self.CastToActor();
		kRenderModel @model = actor.RenderModel();
		if (model is null)
			return;
		
		if (IsAdon())
		{
			model.SetVisibility(9, false); //loincloth front center
			model.SetVisibility(10, false); //loincloth front lower
			model.SetVisibility(11, false); //loincloth back center
			model.SetVisibility(12, false); //loincloth back lower
			model.SetTexture(0, 1); //lower body textures
			model.SetTexture(1, 1); //left upper leg
			model.SetTexture(2, 2); //left lower leg
			model.SetTexture(3, 1); //left foot back
			model.SetTexture(4, 1); //left foot front
			model.SetTexture(5, 1); //right upper leg
			model.SetTexture(6, 2); //right lower leg
			model.SetTexture(7, 1); //right foot back
			model.SetTexture(8, 1); //right foot front
			model.SetTexture(9, 1); //loincloth front center
			model.SetTexture(10, 1); //loincloth front lower
			model.SetTexture(11, 1); //loincloth back center
			model.SetTexture(12, 1); //loincloth back lower
			model.SetTexture(13, 1); //upper body
			model.SetTexture(14, 2); //left shoulder
			model.SetTexture(15, 1); //left upper arm
			model.SetTexture(16, 1); //left lower arm
			model.SetTexture(17, 1); //left hand
			model.SetTexture(18, 2); //right shoulder
			model.SetTexture(19, 1); //right upper arm
			model.SetTexture(20, 1); //right lower arm
			model.SetTexture(21, 1); //right hand
			model.SetTexture(22, 1); //neck textures
			model.SetTexture(23, 1); //head textures
			model.SetTexture(24, 2); //gold pin for left and center feathers
			model.SetTexture(25, 1); //center feather
			model.SetTexture(26, 1); //left feather
			model.SetTexture(27, 2); //gold pin for right feather
			model.SetTexture(28, 1); //right feather
			model.SetTexture(29, 2); //right gold pin dread lock
			model.SetTexture(30, 1); //right dreadlock
			model.SetTexture(31, 1); //right dreadlock
			model.SetTexture(32, 1); //right dreadlock
			model.SetTexture(33, 1); //right dreadlock
			model.SetTexture(34, 2); //left gold pin dread lock
			model.SetTexture(35, 1); //left dreadlock
			model.SetTexture(36, 1); //left dreadlock
			model.SetTexture(37, 1); //left dreadlock
			model.SetTexture(38, 1); //left dreadlock
		}
		else
		{
			model.SetVisibility(9, true); //loincloth front center
			model.SetVisibility(10, true); //loincloth front lower
			model.SetVisibility(11, true); //loincloth back center
			model.SetVisibility(12, true); //loincloth back lower
			for (int i = 0; i < 39; i++)
			{
				model.SetTexture(i, 0);
			}
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void SetGravity(const float gravity)
	{
		initialGravity = gravity;
	}
	//------------------------------------------------------------------------------------------------------------------------
	void SetCharacter(int charType)
	{
		character = charType;
		if (IsAdon())
		{
			charCanGroundDash = true;
			charCanAirDash = true;
			SetGravity(0.25f);
			m_initialFriction = 0.6f;
		}
		else if (IsHummer())
		{
			charCanGroundDash = false;
			charCanAirDash = false;
			SetGravity(0.425f);
			m_initialFriction = 0.7f;
		}
		else
		{
			charCanGroundDash = false;
			charCanAirDash = false;
			SetGravity(0.425f);
		}
		SetupModel();
	}
	//------------------------------------------------------------------------------------------------------------------------
	bool IsAdon()
	{
		return character == PLAYER_CHAR_ADON;
	}
	//------------------------------------------------------------------------------------------------------------------------
	bool IsHummer()
	{
		return character == PLAYER_CHAR_HUMMER;
	}
	//------------------------------------------------------------------------------------------------------------------------
	void SetNormalFriction(const float waterFriction, const float multiplier)
	{
		if (playerIsRoping)
		{
			self.Friction() = playerRopeFriction * multiplier;
		}
		else
		{
			self.Friction() = (m_initialFriction + waterFriction + GetPlayerSpeed()) * multiplier;
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	void TickDash()
	{
		dashInputTime -= GAME_DELTA_TIME;
		dashCooldown -= GAME_DELTA_TIME;
		dashTimeLength -= GAME_DELTA_TIME;
		if (dashTimeLength > 0.0f)
		{
			float speed = Math::Sin((dashTimeLength / PLAYER_DASH_LENGTH) * Math::pi) * dashForwardSpeed;
			self.Velocity() += kVec3(dashForwardDirectionX * speed, dashForwardDirectionY * speed, 0.0f);
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
	// if is able to dash then dashs in the forward direction + yawOffset of the actor. Returns true if did dash.
	//------------------------------------------------------------------------------------------------------------------------
	bool TryDashInput(float yawOffset)
	{
		bool didDash = false;
		//last input was in the same direction as current input and isn't on cooldown and pressed within a certain time
		if (Math::Approximately(dashLastYawOffset, yawOffset) && dashCooldown <= 0.0f && dashInputTime > 0.0f && dashInputTime < PLAYER_DASH_INPUT_TIME)
		{
			dashInputTime = 0.0f;
			dashCooldown = PLAYER_DASH_COOLDOWN;
			dashTimeLength = PLAYER_DASH_LENGTH;
			self.Velocity().z += dashJumpSpeed;
			dashForwardDirectionX = Math::Sin(self.Yaw() + yawOffset);
			dashForwardDirectionY = Math::Cos(self.Yaw() + yawOffset);
			didDash = true;
		}
		else
		{
			dashInputTime = PLAYER_DASH_INPUT_TIME;
		}
		
		dashLastYawOffset = yawOffset;
		return didDash;
	}
	//------------------------------------------------------------------------------------------------------------------------
	void TryDash(float yawOffset)
	{			
		bool isClimbing = IsPlayerClimbing();
		bool touchingWater = (self.GetWaterLevel() == WLT_UNDER || self.GetWaterLevel() == WLT_BETWEEN);
		
		//locked or climbing or in water
		if (Player.Locked() || isClimbing || touchingWater)
		{
			return;
		}
			
		if (self.OnGround())
		{
			if (!charCanGroundDash)
			{
				return;
			}
			
			if (dashCooldown <= 0.0f)
			{
				bool didGroundDash = TryDashInput(yawOffset);
				if (didGroundDash)
				{
					self.PlaySound("sounds/shaders/swoosh1.ksnd");
				}
			}
		}
		else
		{
			if (!charCanAirDash)
			{
				return;
			}
			
			//airDash allowed and dash not on cooldown
			if (airDashEnabled && dashCooldown <= 0.0f)
			{
				bool didAirDash = TryDashInput(yawOffset);
				if (didAirDash)
				{
					
					self.PlaySound("sounds/shaders/swoosh1.ksnd");
					airDashEnabled = false;
				}
			}
		}
	}
	//------------------------------------------------------------------------------------------------------------------------
    void OnTick(void)
    {
		//
		const int areaID = self.AreaID();
		int areaFlags = World.GetAreaFlags(areaID);
		bool underWater = self.GetWaterLevel() == WLT_UNDER && (areaFlags & AAF_ANTIGRAVITY) == 0;
		bool isDead = IsActorDead(self.CastToActor());

		int animID = Player.Actor().CastToActor().AnimState().PlayingID();
		float animTrackTime = Player.Actor().CastToActor().AnimState().TrackTime();
		
		if (animID == anim_aiTeleportIn && lastAnimTrackTime < 0.83f && animTrackTime >= 0.83f) //2550 regen I am turok
		{
			PlayRegenSound();
		}
		lastAnimTrackTime = animTrackTime;
		
		if (lastAnimID != animID)
		{
			lastAnimID = animID;
			
			//2507 is underwater death
			//anim_aiDeathStand 34 is normal death
			if (animID == 36) //anim 36 is the fall death pit
			{
				PlayDeathPitSound();
			}
			
		}
		
		if (Player.CurrentWeapon() != lastCurrentWeapon)
		{
			lastCurrentWeapon = Player.CurrentWeapon();
		}
		
		if (Player.GetAmmo(TW_WEAPON_CHRONO) < 3)
		{		
			playerChronoChargeTime += GAME_DELTA_TIME;
			if (playerChronoChargeTime >= ChronoAmmoTime)
			{
				playerChronoChargeTime = 0.0f;
				if (Player.HasWeapon(TW_WEAPON_CHRONO))
				{
					Player.GiveWeapon(TW_WEAPON_CHRONO, 1);
				}
			}
		}
		else
		{
			playerChronoChargeTime = 0.0f;
		}
		
		if (playerRopeActor !is null)
		{
			if (IsInActorRadius(self.CastToActor(), playerRopeActor, 1.1f))
			{
				playerRopeActor.Velocity() = Math::vecZero;
				@playerRopeActor = null;
			}
			else
			{
				playerRopeActor.Velocity() = (self.Origin() - playerRopeActor.Origin()).Normalize() * playerRopeSpeed;
			}
			
			//if distance doesn't go past 200 for 3 seconds then disconnect
			if (playerRopeActor !is null)
			{
				float dist = playerRopeActorLP.Distance(playerRopeActor.Origin());
				if (dist < 200.0f)
				{
					playerRopeActorDistTime += GAME_DELTA_TIME;
					if (playerRopeActorDistTime >= 1.0f)
					{
						@playerRopeActor = null;
					}
				}
				else
				{
					playerRopeActorLP = playerRopeActor.Origin();
					playerRopeActorDistTime = 0.0f;
				}
			}
		}
		
		bool isGrounded = self.OnGround();
		if (wasGrounded != isGrounded)
		{
			wasGrounded = isGrounded;
			if (isGrounded)
			{
				airDashEnabled = true;
				if (!underWater && airTime > 0.5f)
				{
					if (lastAirVelocity.z <= -15.0f)
					{
						PlayJumpLandSound();
					}
					
					if (IsAdon() && lastAirVelocity.z <= -16.4f)
					{
						self.SpawnFx("fx/BP_Player_Adon_Shockwave.kfx", kVec3(0.0f, 0.0f, 1.0f));
						Game.PlaySound("sounds/shaders/adon_shockwave.ksnd");
					}
				}
			}
		}
		else if (!isGrounded)
		{
			lastAirVelocity = self.Velocity();
		}
		
		if (isGrounded)
		{
			airTime = 0.0f;
		}
		else
		{
			airTime += GAME_DELTA_TIME;
		}
		
		if (IsAdon())
		{
			float speed = self.Velocity().z;
			if (speed < -10.0f)
			{
				SetGravity(0.425f);
			}
			else if (speed < -5.0f)
			{
				SetGravity(0.35f);
			}
			else
			{
				SetGravity(0.25f);
			}
		}
		
		buttons = InputUpdate(buttons);
		
		bool isClimbThrust = self.PlayerFlags() & (PF_CLIMBTHRUST) != 0;
		if (wasClimbThurst != isClimbThrust)
		{
			climbThrustTime = 0.0f;
			wasClimbThurst = isClimbThrust;
			if (isClimbThrust)
			{
				PlayClimbSound();
			}
		}
		else if (isClimbThrust)
		{
			if (IsPlayerClimbing())
			{
				climbThrustTime += GAME_DELTA_TIME;
				if (climbThrustTime > 0.75f)
				{
					climbThrustTime = 0.0f;
					PlayClimbSound();
				}
			}
			else
			{
				climbThrustTime = 0.0f;
			}
		}
		
		// Game.PrintLine("" + Player.Actor().CastToActor().AnimState().PlayingID(), 0);
		//Game.PrintLine("PT" + Player.Actor().CastToActor().AnimState().PlayTime(), 1);
		// Game.PrintLine("TT" + Player.Actor().CastToActor().AnimState().TrackTime(), 2);
		//TT 0.83
		
		// Game.PrintLine("pflags " + self.PlayerFlags(), 1);
		// Game.PrintLine("v " + self.Velocity().ToString(), 2);
		// Game.PrintLine("m " + self.Movement().ToString(), 3);
		bool isJumping = self.PlayerFlags() & (PF_JUMPING) != 0;
		if (wasJumping != isJumping)
		{
			wasJumping = isJumping;
			if (isJumping)
			{
				if (!IsWeaponDisabled()) //don't play sounds when weapon disabled
				{
					if (Math::RandMax(3) != 0)
					{
						PlayJumpSound();
					}
				}
			}
		}
		
		TickDash();
		if (InputForwardDown())
		{
			TryDash(0.0f);
		}
		else if (InputBackwardDown())
		{
			TryDash(Math::pi);
		}
		else if (InputStrafeLeftDown())
		{
			TryDash(-Math::piHalf);
		}
		else if (InputStrafeRightDown())
		{
			TryDash(Math::piHalf);
		}
		
		statsTime = Math::Max(statsTime - GAME_DELTA_TIME, 0.0f);
		//in mountain of sun map and not in training map
		if ((Game.GetCurrentMapID() >= 95 && Game.GetCurrentMapID() != 120) &&
			statsTime <= 0.0f && !Player.Locked() && InputMapZoomInPress() && InputMapZoomOutPress())
		{
			statsTime = 5.0f;
			Game.PrintLine(Game.GetLocalizedText("" + GetFeathers() + " " + Game.GetLocalizedText(LTKey(29))), 0, 300);
			Game.PrintLine(Game.GetLocalizedText(LTKey(24)) + " " + (SpecialPickupCount()) + " " + Game.GetLocalizedText(LTKey(7)) + " 4", 1, 300);
			Game.PrintLine(Game.GetLocalizedText(LTKey(23)) + " " + (GetDefensePickups() + GetSpeedPickups()) + " " + Game.GetLocalizedText(LTKey(7)) + " 11", 2, 300);
		}

		if (!BP_DEBUG)
		{
			if (self.PlayerFlags() & (PF_NOCLIP|PF_FLY|PF_GOD) != 0)
			{
				if (!isPlayerCheating)
				{
					isPlayerCheating = true;
					if (IsAdon())
					{
						Game.PlaySound("sounds/shaders/talset_ashamed.ksnd");
					}
					else
					{
						Game.PlaySound("sounds/shaders/adon_taunt_01.ksnd");
					}
				}
			}
			else
			{
				isPlayerCheating = false;
			}
		}
		else
		{
			//Player.Actor().PlayerFlags() |= (1 << 30); //hide weapon
			
			// if (InputMapZoomInDown()) //x
			// {
				// //ray1.Origin() = self.Origin();				
				// //bool hit = self.CheckPosition(kVec3(660, 1213, 120));
				// ray2.Origin() = kVec3(660, 1213, 120);
				// self.CanSee(ray2);
				// //Raycast(kVec3(622, 525, 85), kVec3(619, 2637, 181), CF_CLIPEDGES|CF_IGNOREBLOCKERS|CF_DROPOFF|CF_NOCLIPACTORS|CF_COLLIDEFLOORS|CF_NOCLIPSTATICS);
				// //Raycast(self.Origin(), kVec3(660, 1213, 120), CF_NOCLIPACTORS);
				// if (CModel.Fraction() < 1.0)
				// //if (!hit)
				// {
					// Sys.Print("Hit Point: " + CModel.InterceptVector().ToString() + " Fraction: " + CModel.Fraction() + " ClipResult: " + CModel.ClipResult());
					// raytest.Origin() = CModel.InterceptVector();
					// //CModel.ContactNormal();
				// }
				// else
					// Sys.Print("Hit nothing");
				
				// // GameVariables.SetValue("g_newgame", "1"); //need to reload map(with change map or load a new map) to take effect
				// // Game.ChangeMap("levels/05.map");

				// //PlayLoop.StartWarp(self.CastToActor(), 1000, 102);
				// //PlayLoop.StartFreeWarp(self.CastToActor(), self.Origin(), self.Yaw(), self.SectorIndex(), Game.GetCurrentMapID());
				
				// //self.InflictGenericDamage(self.CastToActor(), 100);
				// //World.TriggerActorsByTID(self.CastToActor(), 1034);
				// //Game.CallDelayedMapScript(10005, self.CastToActor(), 0);
			// }
			
			
			// if (@colModel != null)
			// {
				// bool hit = colModel.Fraction() < 1.0;
				// if (hit && !lastCModelHit)
				// //if (!notHit)
				// {
					// Sys.Print("colModel " + self.GameTicks() + " Hit Point: " + colModel.InterceptVector().ToString() + " Fraction: " + colModel.Fraction() + " ClipResult: " + colModel.ClipResult());
					// raytest.Origin() = colModel.InterceptVector();
					// //CModel.ContactNormal();
				// }
				// lastCModelHit = hit;
			// }
			
			// if (InputMapZoomOutDown()) //z
			// {
				// self.Flags() &= ~AF_SOLID;
				// kActor@ bird = World.GetActorByTID(1010);
				// //bird.Origin() = self.Origin() + (ActorForward(self.CastToActor()) * (GAME_SCALE * 10));
				// //bird.SetSector(bird.GetSectorIndexAtLocation(bird.Origin()));
				
				// ray2.Origin() = self.Origin();//kVec3(660, 1213, 120);
				// bird.SetTarget(ray2);
				// bird.SpawnProjectile("fx/hittest.kfx", kVec3(0, 0, 0), ray2.Origin(), Math::Deg2Rad(45.0f));

				// @colModel = @CModel;
				// //kVec3 playerForward = ActorForward(self) * GAME_SCALE;
				// //ray1.Origin() = self.Origin();
				
				// //bird.CanSee(self.CastToActor());
				// //bool notHit = bird.CheckPosition(kVec3(660, 1213, 120));
				// //Raycast(kVec3(622, 525, 85), kVec3(660, 1213, 120), 0);
				// //Raycast(self.Origin(), kVec3(660, 1213, 120), 0);
				// if (CModel.Fraction() < 1.0)
				// //if (!notHit)
				// {
					// Sys.Print("Hit " + self.GameTicks() + " Point: " + CModel.InterceptVector().ToString() + " Fraction: " + CModel.Fraction() + " ClipResult: " + CModel.ClipResult());
					// raytest.Origin() = CModel.InterceptVector();
					// //CModel.ContactNormal();
				// }
				// else
					// Sys.Print("Hit nothing " + self.GameTicks());
				
				// // for (int i = 0; i < 10; i++)
					// // ActorFactory.Spawn("LifeForce_10", self.Origin().x, self.Origin().y, self.Origin().z, 0, self.SectorIndex());
				
				// //noAttackEnemies = !noAttackEnemies;
				// //self.InflictGenericDamage(self.CastToActor(), 500);
			// }
		}
		
		bool touchingWater = ((self.GetWaterLevel() == WLT_UNDER || self.GetWaterLevel() == WLT_BETWEEN) && (areaFlags & AAF_ANTIGRAVITY) == 0);
		
		if (playerIsRoping)
		{
			if (IsInPlayerBounds(playerRopePoint, 2.0f) || InputJumpDown() || underWater)
			{
				playerIsRoping = false;
				self.Velocity() = Math::vecZero;
			}
			else
			{
				self.Velocity() = (playerRopePoint - self.Origin()).Normalize() * playerRopeSpeed;
			}
		}
				

		if (lastAreaID != areaID)
		{
			if (lastAreaWasSpecialWater)
			{
				lastAreaWasSpecialWater = false;
				World.ChangeAreaFlag(lastAreaID, AAF_WATER, false);
			}
		}
        float damageDelay = float(World.GetAreaArg(areaID, 5)) / 1024.0f;
		float waterFriction = 0.0f;
		float frictionMul = 1.0f;
		bool customDamage = false;
		sectorGravity = 0.0f;
		if (areaFlags & AAF_TOGGLE != 0 && Game.GetCurrentMapID() >= 95)
		{
			float areaType = int(World.GetAreaArg(areaID, 4)); //arg5
			if (areaType == 0) //water
			{
				float bottomWaterHeight = World.GetAreaArg(areaID, 5); //arg6
				lastAreaWasSpecialWater = true;
				if (self.Origin().z + 51.2f < bottomWaterHeight)
				{
					World.ChangeAreaFlag(areaID, AAF_WATER, false);
				}
				else
				{
					World.ChangeAreaFlag(areaID, AAF_WATER, true);
				}
			}
			else if (areaType == 1) //waterFricition
			{
				float bottomWaterHeight = World.GetAreaArg(areaID, 5); //arg6
				lastAreaWasSpecialWater = true;
				
				if (touchingWater)
				{
					waterFriction = -m_initialFriction * 0.5f; //slow by half
				}
				
				if (self.Origin().z + 51.2f < bottomWaterHeight)
				{
					World.ChangeAreaFlag(areaID, AAF_WATER, false);
				}
				else
				{
					World.ChangeAreaFlag(areaID, AAF_WATER, true);
				}
			}
			else if (areaType == 2) //water current movement
			{
				int waterDirectionType = int(World.GetAreaArg(areaID, 5)); //arg6
				kVec3 waterDir;
				switch (waterDirectionType)
				{
					case 1:
						waterDir = kVec3(0.0f, 1.0f, 0.0f);
						break;
					case 2:
						waterDir = kVec3(-1.0f, 0.0f, 0.0f);
						break;
				}
				self.Velocity() += waterDir * 2.0f;
			}
			else if (areaType == 3) //damage on ground or not
			{
				customDamage = true;
				m_damageFloorTime = Math::Clampf(m_damageFloorTime - GAME_DELTA_TIME, 0.0f, 0.25f);
				if (m_damageFloorTime <= 0.0f)
				{
					m_damageFloorTime = 0.25f;
					self.InflictGenericDamage(self.CastToActor(), 10);
				}
			}
			else if (areaType == 4) //set gravity
			{
				sectorGravity = float(World.GetAreaArg(areaID, 5)) / 100.0f; //arg6 gravity
			}
		}
		lastAreaID = areaID;
		
        if(self.AnimState().PlayingID() == anim_campaingerRage)
        {
            return;
        }
        
        if(InSpiritWorld())
        {
            DoWarpSounds();
        }
        
		if (wasUnderWater && !underWater)
		{
			if (!isDead)
			{
				if (underwaterTime > 50.0f)
				{
					PlayBigWaterGaspSound();
				}
				else
				{
					PlaySmallWaterGaspSound();
				}
			}
			underwaterTime = 0.0f;
		}

        if (underWater)
        {
			underwaterTime += GAME_DELTA_TIME;
            DoSplashBubbles();
        }
		
		wasUnderWater = underWater;

		
		//not under water and not arrow roping and not flying or nocliping and not in anitgravity and above the floor height and not locked in cinema
		if ((self.GetWaterLevel() == WLT_OVER || self.GetWaterLevel() == WLT_INVALID) && !playerIsRoping && (self.PlayerFlags() & (PF_NOCLIP|PF_FLY)) == 0 &&
			(areaFlags & AAF_ANTIGRAVITY) == 0 && self.Origin().z > self.FloorHeight() + 20.0f && !Player.Locked())
		{
			if (fallTime < 1.8f)
			{
				fallTime += GAME_DELTA_TIME;
				if (fallTime >= 1.8f)
				{
					if (IsCheatOn(CHEAT_ARGH))
					{
						self.PlaySound("sounds/shaders/fall_argh.ksnd");
					}
					// else
					// {
						// if (!IsAdon())
						// {
							// self.PlaySound("sounds/shaders/generic_20_turok_t-rex_death.ksnd");
						// }
					// }
				}
			}
		}
		else
		{
			fallTime = 0.0f;
		}
        
		if (!customDamage)
		{
			if ((areaFlags & AAF_DAMAGE) != 0)
			{
				m_damageFloorTime = Math::Clampf(m_damageFloorTime - GAME_DELTA_TIME, 0.0f, damageDelay);
				if (isGrounded || touchingWater)
				{
					if (m_damageFloorTime <= 0.0f)
					{
						m_damageFloorTime = damageDelay;
						
						//if immune to lava then don't do damage
						if ((areaFlags & AAF_LAVA) == 0 || !GetPlayerLavaArmor())
						{
							self.InflictGenericDamage(self.CastToActor(), World.GetAreaArg(areaID, 4));
						}
					}
					
					if ((areaFlags & AAF_LAVA) != 0 && !touchingWater)
					{
						frictionMul = 0.5f;
					}
				}
			}
			else
			{
				m_damageFloorTime = 0.0f;
			}
		}
		
		SetNormalFriction(waterFriction, frictionMul);
		if (sectorGravity > 0.0f)
		{
			self.Gravity() = sectorGravity;
		}
		else
		{
			self.Gravity() = initialGravity;
		}
        
        if(m_shoveTime > 0)
        {
            kVec3 vDir = m_vShoveVector - self.Origin();
            float anYaw = vDir.ToYaw();
            
            self.Yaw() = self.Yaw().Interpolate(anYaw, 0.2f);
            m_shoveTime -= GAME_DELTA_TIME;
        }
    }
	//------------------------------------------------------------------------------------------------------------------------
    void OnBeginLevel()
    {
		SetupWayPoints();
		SetCharacter(GetPlayerCharacter());
				
		if(!InSpiritWorld())
        {
            return;
        }
		
        @m_pWarpBuzzActor = ActorFactory.Spawn("DummyActor", 0, 0, 0, 0, self.SectorIndex());
    }
    //------------------------------------------------------------------------------------------------------------------------
    void OnPostBeginLevel(void)
    {
		if (self.Health() <= 0)
		{
			self.Health() = 1;
		}
		
	
		if (self.GameTicks() == 0)
		{
			if (BP_DEBUG && !didEditorWarp)
			{
				didEditorWarp = true;
				PlayLoop.ChangeMap("levels/" + EditorStartMap + ".map");
			}
		}
		
		// if (loadedWrongVersion)
		// {
			// loadedWrongVersion = false;
			// PrintLinesReverse(wrongVersionMsg, 600);
		// }
		// else
		// {
			// //check for version
			// int curVersion = GetVersion();
			// if (curVersion != BP_VERSION)
			// {
				// wrongVersionMsg.resize(2);
				// wrongVersionMsg[1] = "Save file version -" + curVersion + "- incompatible with";
				// wrongVersionMsg[0] = "Current game version of -" + BP_VERSION + "-";
				// Sys.Warning("Save file version '" + curVersion + "' incompatible with current game version of '" + BP_VERSION + "'");
				// loadedWrongVersion = true;
				// Game.Restart();
			// }
		// }
	}
    //------------------------------------------------------------------------------------------------------------------------
    void OnSpawn(void)
    {
        m_initialFriction = self.Friction();
    }
    //------------------------------------------------------------------------------------------------------------------------
    void OnEnterWater(void)
    {
        // what speed is the player entering the water?
        if(self.Velocity().z < 0)
        {
            m_nBubbles = -self.Velocity().z * 2;
            
            if(m_nBubbles < 0)
            {
                m_nBubbles = 0;
                m_bubblesSfxTimer = m_nBubbles / 2;
            }
        }
    }
    //------------------------------------------------------------------------------------------------------------------------
    void OnArmorDamage(kActor @instigator, kDictMem @damageDef, const int damage)
    {
        // note: threshold is actually 5 but actual armor is divided by 3
        if(Player.Armor() > 15 && Player.Armor() - damage <= 15)
        {
            Game.PrintLine("$str_180", 0);
        }
    }
    //------------------------------------------------------------------------------------------------------------------------
    void DoViewShake(float velocity, float angle, float duration)
    {
        kActor @actor = ActorFactory.Spawn("QuakeSource", 0, 0 ,0 ,0, self.SectorIndex());
        TurokQuakeSource @quake;
        
        if(actor is null)
        {
            return;
        }
        
        @quake = cast<TurokQuakeSource@>(actor.ScriptObject().obj);
        
        if(quake is null)
        {
            return;
        }
        
        quake.SetupShake(self.Origin(), velocity, angle, duration);
    }
    
    /*
    ==============================================================
    DoShoveWithCamera
    ==============================================================
    */
    
    void DoShoveWithCamera(kActor @instigator)
    {
        if(instigator is null)
        {
            return;
        }
        
        kVec3 org;
        kVec3 pos;
        
        org.x = instigator.Origin().x;
        org.y = instigator.Origin().y;
        org.z = instigator.Origin().z + instigator.Height() * 0.5f;
        
        kVec3 dir = self.Origin() - org;
        
        if(dir.x*dir.x+dir.y*dir.y <= 0.0f)
        {
            dir.x = Math::RandCFloat();
            dir.y = Math::RandCFloat();
        }
        
        dir.Normalize();
        
        dir *= (1.75f*GAME_SCALE);
        dir.z = (0.875f*GAME_SCALE);
        
        self.Velocity() += dir;
        
        self.Origin().z += GAME_SCALE;
        self.PlayerFlags() |= PF_NOAIRFRICTION;
        
        m_shoveTime = 0.5f;
        m_vShoveVector = org;
    }
    
	void PlayHurtSound()
	{
		if (IsAdon())
		{
			switch(Math::RandMax(2))
			{
			case 0:
				self.PlaySound("sounds/shaders/adon_hurt1.ksnd");
				break;
			case 1:
				self.PlaySound("sounds/shaders/adon_hurt2.ksnd");
				break;
			}
		}
		else
		{
			if(self.GetWaterLevel() == WLT_UNDER)
			{
				switch(Math::RandMax(2))
				{
				case 0:
					self.PlaySound("sounds/shaders/generic_14_turok_water_injury_1.ksnd");
					break;
				case 1:
					self.PlaySound("sounds/shaders/generic_15_turok_water_injury_2.ksnd");
					break;
				}
			}
			else
			{
				switch((Math::RandMax(5)) & 3)
				{
				case 0:
					self.PlaySound("sounds/shaders/generic_10_turok_injury_1.ksnd");
					break;
				case 1:
					self.PlaySound("sounds/shaders/generic_11_turok_injury_2.ksnd");
					break;
				case 2:
					self.PlaySound("sounds/shaders/generic_12_turok_injury_3.ksnd");
					break;
				case 3:
					self.PlaySound("sounds/shaders/generic_13_turok_injury_4.ksnd");
					break;
				}
			}
		}
	}
	
	void PlayClimbSound()
	{
		if (IsAdon())
		{
			switch(Math::RandMax(2))
			{
			case 0:
				self.PlaySound("sounds/shaders/adon_climb1.ksnd");
				break;
			case 1:
				self.PlaySound("sounds/shaders/adon_climb2.ksnd");
				break;
			}
		}
		else
		{
			switch(Math::RandMax(2))
			{
			case 0:
				self.PlaySound("sounds/shaders/turok_climb_1.ksnd");
				break;
			case 1:
				self.PlaySound("sounds/shaders/turok_climb_2.ksnd");
				break;
			}
		}
	}
	
	void PlayJumpSound()
	{
		if (IsAdon())
		{
			switch(Math::RandMax(2))
			{
			case 0:
				self.PlaySound("sounds/shaders/adon_jump1.ksnd");
				break;
			case 1:
				self.PlaySound("sounds/shaders/adon_jump2.ksnd");
				break;
			}	
		}
		else
		{
			self.PlaySound("sounds/shaders/turok_jump.ksnd");
		}
	}
	
	void PlayJumpLandSound()
	{
		if (IsAdon())
		{
			switch(Math::RandMax(2))
			{
			case 0:
				self.PlaySound("sounds/shaders/adon_land1.ksnd");
				break;
			case 1:
				self.PlaySound("sounds/shaders/adon_land2.ksnd");
				break;
			}	
		}
		else
		{
			self.PlaySound("sounds/shaders/turok_land.ksnd");
		}
	}
	
	void PlaySmallWaterGaspSound()
	{
		if (IsAdon())
		{
			//self.PlaySound("sounds/shaders/adon_smallgasp.ksnd");
		}
		else
		{
			self.PlaySound("sounds/shaders/turok_small_water_gasp.ksnd");
		}
	}
	
	void PlayBigWaterGaspSound()
	{
		if (IsAdon())
		{
			self.PlaySound("sounds/shaders/adon_biggasp.ksnd");
		}
		else
		{
			self.PlaySound("sounds/shaders/turok_big_water_gasp.ksnd");
		}
	}

	//also used for getting an extra life
	void PlayRegenSound()
	{
		if (IsAdon())
		{
			Game.PlaySound("sounds/shaders/adon_i_am.ksnd"); //I am Adon!
		}
		else
		{
			Game.PlaySound("sounds/shaders/turok_i_am.ksnd"); //I am Turok!
		}
	}
	
	void PlayDeathPitSound()
	{
		if (IsAdon())
		{
			Game.PlaySound("sounds/shaders/adon_death" + (Math::RandMax(9) + 1) + ".ksnd");
		}
		else
		{
			Game.PlaySound("sounds/shaders/turok_fall_to_death.ksnd");
		}
	}
	
    void OnDamage(kActor @instigator, kDictMem @damageDef, const int damage)
    {
		Game.CallDelayedMapScript(PlayerDamageScriptID, self.CastToActor(), 0);
		
        // check for quake effect
        if(!(damageDef is null))
        {
            bool bQuake = false;
            bool bShove = false;
            
            if(damageDef.GetBool("bQuake", bQuake) && bQuake)
            {
                float velocity, angle, duration;
                
                damageDef.GetFloat("quakeVelocity", velocity, 0.5625 * GAME_SCALE);
                damageDef.GetFloat("quakeAngle",    angle,    -0.03f);
                damageDef.GetFloat("quakeDuration", duration, 2.75f);
                
                DoViewShake(velocity, angle, duration);
            }
            
            if(damageDef.GetBool("bShove", bShove) && bShove)
            {
                DoShoveWithCamera(instigator);
            }
        }
        
        if (damage <= 0)
        {
            return;
        }
		
		int damageReduction = int(GetPlayerDefense() * damage);
		self.Health() += damageReduction;
        
        if (damage - damageReduction <= 0)
        {
            return;
        }
		
        if(self.Health() > 15 && self.Health() - damage <= 15)
        {
            Game.PrintLine("$str_181", 0);
        }
        
        if(self.Health() > 0 && self.Health() - damage <= 0)
        {
            return;
        }
        
		PlayHurtSound();
    }
    
    /*
    ==============================================================
    OnDeath
    ==============================================================
    */
    
    void OnDeath(kActor @killer, kDictMem @damageDef)
    {
		SetDeaths(GetDeaths() + 1);
		Game.CallDelayedMapScript(PlayerDeathScriptID, self.CastToActor(), 0);
		
		if (IsAdon())
		{
			Game.PlaySound("sounds/shaders/adon_death" + (Math::RandMax(9) + 1) + ".ksnd");
			//bug fix for playing death animation in water
			if (self.GetWaterLevel() == WLT_UNDER || self.GetWaterLevel() == WLT_BETWEEN)
			{
				Player.Actor().AnimState().Set(2507, 5.5f, ANF_LOOP|ANF_ROOTMOTION);
			}
		}
		else
		{
			//bug fix for playing death animation in water
			if (self.GetWaterLevel() == WLT_UNDER || self.GetWaterLevel() == WLT_BETWEEN)
			{
				Game.PlaySound("sounds/shaders/death_water.ksnd");
				Player.Actor().AnimState().Set(2507, 5.5f, ANF_LOOP|ANF_ROOTMOTION);
			}
			else
			{
				self.PlaySound("sounds/shaders/generic_18_turok_normal_death.ksnd");
			}
		}
        
        //
        // we're going to be doing our own custom death cinematic for these specific maps
        //
        
        int currentMap = Game.GetCurrentMapID();
        
        switch(currentMap)
        {
        case 0: // campaigner boss map?
            self.PlayerFlags() |= PF_PREVENTDEATHCAM;
            Game.CallDelayedMapScript(2, self.CastToActor(), 0);
            break;
        case 3: // trex boss map?
            self.PlayerFlags() |= PF_PREVENTDEATHCAM;
            Game.CallDelayedMapScript(2, self.CastToActor(), 0);
            break;
        case 49: // mantis boss map?
            self.PlayerFlags() |= PF_PREVENTDEATHCAM;
            Game.CallDelayedMapScript(5, self.CastToActor(), 0);
            break;
        default:
            break;
        }
    }
    
    /*
    ==============================================================
    OnPickup
    ==============================================================
    */
    
    void OnPickup(kActor @pickup)
    {
        int giveBits = 0;
        int chronoBits;
        
        switch(pickup.Type())
        {
        case AT_PICKUP_CHRONOPIECE1:
            giveBits = 0;
            break;
        case AT_PICKUP_CHRONOPIECE2:
            giveBits = 1;
            break;
        case AT_PICKUP_CHRONOPIECE3:
            giveBits = 2;
            break;
        case AT_PICKUP_CHRONOPIECE4:
            giveBits = 3;
            break;
        case AT_PICKUP_CHRONOPIECE5:
            giveBits = 4;
            break;
        case AT_PICKUP_CHRONOPIECE6:
            giveBits = 5;
            break;
        case AT_PICKUP_CHRONOPIECE7:
            giveBits = 6;
            break;
        case AT_PICKUP_CHRONOPIECE8:
            giveBits = 7;
            break;
        case AT_PICKUP_KEY1:
        case AT_PICKUP_KEY2:
        case AT_PICKUP_KEY3:
        case AT_PICKUP_KEY5:
        case AT_PICKUP_KEY6:
            self.RenderModel().SetTexture(21, pickup.SpawnParams(5));
            return;
        case AT_PICKUP_KEY4:
            self.RenderModel().SetTexture(21, pickup.SpawnParams(5));
            if(Game.GetCurrentMapID() == 48)
            {
                GameVariables.SetValue("bGotLonghunterKey", "1");
            }
            // fall through
        case AT_PICKUP_FINALKEY2:
            if(Game.GetCurrentMapID() == 49)
            {
                GameVariables.SetValue("bGotMantisKey", "1");
            }
            // fall through
        default:
            return;
        }
        
        GameVariables.GetInt("chronoPieceFlags", chronoBits);
        chronoBits |= (1 << giveBits);
        
        if(chronoBits == 0xFF)
        {
            // give chronosceptor weapon
            Player.GiveWeapon(TW_WEAPON_CHRONO, 3);
            Game.PrintLine("$str_156", 0);
        }
        
        GameVariables.SetValue("chronoPieceFlags", "" + chronoBits);
    }
    
    /*
    ==============================================================
    KnifeAttack
    ==============================================================
    */
    
    void KnifeAttack(kActor @actor, const float arg1, const float arg2, const float arg3, const float arg4)
    {
        float radius;
        float dist;
        int damage = 0;
        int angle1, angle2;
        int angDiff;
        int nKnife = int(arg1);
        int health;
        
        
        if(actor is null)
        {
            return;
        }
        
        if(actor.IsStale() || (actor.ScriptObject() is null))
        {
            // actor must be alive and has a script object
            return;
        }
        
        if(actor.ScriptObject().obj is null)
        {
            // ref handle to script object must exist
            return;
        }
        
        if ((actor.Flags() & AF_HOLDTRIGGERANIM) != 0)
        {
            return;
        }
		
		if (IsAIDisabled(actor))
		{
			return;
		}
        
        // did actor get hit?
        radius = (arg2 * GAME_SCALE);
        
        dist = Math::Sqrt(actor.DistanceToPoint(m_vStabVector));
        
        angle1 = int(Math::Rad2Deg(actor.Yaw())) % 360;
        angle2 = int(Math::Rad2Deg( self.Yaw())) % 360;
        
        if(angle1 < 0) angle1 += 360;
        if(angle2 < 0) angle2 += 360;
        
        angDiff = (angle1 - angle2);
        
        // check for backstabs
        if(dist <= ((radius * 0.5f) + actor.Radius()) && Math::Abs(angDiff) <= 40)
        {
            switch(actor.Type())
            {
            case AT_GRUNT:
			case 6050: //Robot Grunt
			case 6075: //Fireborn Spawn
            case AT_ANIMAL:
            case AT_BOAR:
			case AT_FISH:
                // grunts and animals take super damage
                damage = g_KnifeMortalDeathHits;
                break;

            case AT_RAPTOR:
            case AT_DINOSAUR1:
			case 6051: //robot rider
            case AT_RIDER:
            case AT_SANDWORM:
            case AT_STALKER:
            case AT_ALIEN:
            case AT_PURLIN:
            case AT_MECH:
                damage = g_KnifeMortalWoundHits;
                break;
                
			case 6071: //trex norm
            case AT_AIBOSS_TREX:
                damage = g_KnifeTrexHits;
                break;
            
			case 6072: //campainger norm
			case 6093: //spirit campainger
            case AT_AIBOSS_CAMPAINGER:
                damage = g_KnifeCampaignerHits;
                break;
				
            case 6052: //longhunter robot
			case 6053: //longhunter norm
            case AT_AIBOSS_HUNTER:
                damage = g_KnifeLonghunterHits;
                break;
            
			case 6073: //mantis norm
            case AT_AIBOSS_MANTIS:
                damage = g_KnifeMantisHits;
                break;

            default:
                // other actor types should not be hit by backstabs
                damage = 0;
                break;
            }
        }
        // do regular direct damage
        else if(dist <= (radius + actor.Radius()))
        {
            switch(actor.Type())
            {
            case AT_RAPTOR:
                damage = g_KnifeRaptorHits[nKnife];
                break;
                
            case AT_DINOSAUR1:
                damage = g_knifeDimetrondonHits[nKnife];
                break;
            
			case 6051: //robot rider
            case AT_RIDER:
                damage = g_KnifeTriceratopsHits[nKnife];
                break;
                
            case AT_SANDWORM:
                damage = g_KnifeSubterraneanHits[nKnife];
                break;
                
            case AT_STALKER:
                damage = g_KnifeStalkerHits[nKnife];
                break;
                
            case AT_ALIEN:
                damage = g_KnifeAlienHits[nKnife];
                break;
                
            case AT_PURLIN:
                damage = g_KnifePurlinHits[nKnife];
                break;
                
            case AT_MECH:
                damage = g_KnifeRobotHits[nKnife];
                break;
                
            case AT_SEWERCRAB:
                damage = g_KnifeSewerCrabHits[nKnife];
                break;
                
            case AT_KILLERPLANT:
                damage = g_KnifePlantHits[nKnife];
                break;
				
			case 6050: //Robot Grunt
			case 6075: //Fireborn Spawn
            case AT_GRUNT:
            case AT_INSECT:
            case AT_DRAGONFLY:
            case AT_ANIMAL:
            case AT_BOAR:
			case AT_FISH:
                damage = g_KnifeHumanHits[nKnife];
                break;
				
            case 6071: //trex norm
            case AT_AIBOSS_TREX:
                damage = g_KnifeTrexHits;
                break;
            
			case 6072: //campainger norm
			case 6093: //spirit campainger
            case AT_AIBOSS_CAMPAINGER:
                damage = g_KnifeCampaignerHits;
                break;
                
            case 6052: //longhunter robot
			case 6053: //longhunter norm
            case AT_AIBOSS_HUNTER:
                damage = g_KnifeLonghunterHits;
                break;
                
			case 6073: //mantis norm
            case AT_AIBOSS_MANTIS:
                damage = g_KnifeMantisHits;
                break;

            default:
                damage = 0;
                break;
            }
        }
        
        if (damage <= 0)
        {
            return;
        }
        
        health = actor.Health();
		damage = int(Math::RandRange(damage * 0.75f, float(damage))); //random damage
		damage = Math::Max(damage, 1);
				
        actor.InflictGenericDamage(self.CastToActor(), damage);
            
        // very important that we know that all actors that we attack contains a pointer
        // to the TurokEnemy script object class
        if (!(actor.ScriptObject() is null) && !(actor.ScriptObject().obj is null))
        {
            TurokEnemy @enemyObj = cast<TurokEnemy@>(actor.ScriptObject().obj);
			if (enemyObj is null)
			{
				TurokFish @fishObj = cast<TurokFish@>(actor.ScriptObject().obj);
				if (fishObj !is null)
				{
					fishObj.OnKnifeHit();
				}
			}
			else if (health > 0 && actor.Health() <= 0)
			{
                enemyObj.m_bMortallyWounded = true;
			}
        }
		
        actor.PlaySound("sounds/shaders/tomahawk_impact_flesh.ksnd");
        KnifeParticles(actor, nKnife, damage);
    }
    
    /*
    ==============================================================
    KnifeParticles
    ==============================================================
    */
    
    void KnifeParticles(kActor @actor, const int nKnife, const int damage)
    {
        kStr redBlood, greenBlood, spark;
        kStr particle;
        
        // The original code references 3 left/right/forward spark fx, but those don't
        // exist in the game's data and get ignored. Use a generic spark instead of nothing.
        spark = "fx/spark.kfx";
        
        switch(nKnife)
        {
        case 0:
            redBlood    = "fx/knifeleft_blood.kfx";
            greenBlood  = "fx/knifeleft_gblood.kfx";
            break;
            
        case 1:
            redBlood    = "fx/kniferight_blood.kfx";
            greenBlood  = "fx/kniferight_gblood.kfx";
            break;
            
        case 2:
            redBlood    = "fx/knifeforward_blood.kfx";
            greenBlood  = "fx/knifeforward_gblood.kfx";
            break;
        }
        
        switch(actor.Type())
        {
        case AT_GRUNT:
            // fix an original game bug: robotic grunts would bleed red if stabbed.
            if(actor.ImpactType() == IT_METAL)
            {
                particle = spark;
            }
            else
            {
                particle = redBlood;
            }
            break;
            
        case AT_ANIMAL:
        case AT_BOAR:
        case AT_RAPTOR:
        case AT_DINOSAUR1:
        case AT_RIDER:
        case AT_SANDWORM:
        case AT_STALKER:
        case AT_PURLIN:
        case AT_AIBOSS_TREX:
        case AT_AIBOSS_CAMPAINGER:
        case AT_AIBOSS_HUNTER:
            particle = redBlood;
            break;
            
        case AT_INSECT:
        case AT_DRAGONFLY:
        case AT_ALIEN:
        case AT_KILLERPLANT:
        case AT_SEWERCRAB:
        case AT_AIBOSS_MANTIS:
            particle = greenBlood;
            break;
            
        case AT_MECH:
            particle = spark;
            break;
            
        default:
            return;
        }
        
        actor.SpawnProjectile(particle, m_vBloodVector, self.Origin(), Math::Deg2Rad(360));
    }
	//------------------------------------------------------------------------------------------------------------------------
	void OnEndLevel()
	{
		DestroyWayPoints();
		pickupActors.resize(0);
		spawnersMP.resize(0);
		ropePoints.resize(0);
		@playerRopeActor = null;
		playerIsRoping = false;
		fallTime = 0.0f;
		@wavMusic = null;
		SetChronoTime(playerChronoChargeTime);
	}
	//------------------------------------------------------------------------------------------------------------------------
};
