// Level_Bullet_Manager.cpp:	для обеспечения полета пули по траектории
//								все пули и осколки передаются сюда
//								(для просчета столкновений и их визуализации)
//////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "Level_Bullet_Manager.h"
#include "entity.h"
#include "../xrEngine/gamemtllib.h"
#include "level.h"
#include "gamepersistent.h"
#include "game_cl_base.h"
#include "xrmessages.h"
#include "../Include/xrRender/Kinematics.h"
#include "Actor.h"
#include "AI/Stalker/ai_stalker.h"
#include "character_info.h"
#include "game_cl_base_weapon_usage_statistic.h"
#include "../xrcdb/xr_collide_defs.h"
#include "../xrengine/xr_collide_form.h"
#include "weapon.h"
#include "ik/math3d.h"
#include "actor.h"
#include "ai/monsters/basemonster/base_monster.h"

//константы ShootFactor, определяющие 
//поведение пули при столкновении с объектом
#define RICOCHET_THRESHOLD		0.1
#define STUCK_THRESHOLD			0.4

//расстояния не пролетев которого пуля не трогает того кто ее пустил
extern float gCheckHitK;

//test callback функция 
//  object - object for testing
//return TRUE-тестировать объект / FALSE-пропустить объект
BOOL CBulletManager::test_callback(const collide::ray_defs& rd, CObject* object, LPVOID params)
{
	bullet_test_callback_data* pData	= (bullet_test_callback_data*)params;
	SBullet* bullet = pData->pBullet;

	if( (object->ID() == bullet->parent_id)		&&  
		(bullet->fly_dist<parent_ignore_distance)	&&
		(!bullet->flags.ricochet_was))			return FALSE;

	BOOL bRes						= TRUE;
	if (object){
		CEntity*	entity			= smart_cast<CEntity*>(object);
		if (entity&&entity->g_Alive()&&(entity->ID()!=bullet->parent_id)){
			ICollisionForm*	cform	= entity->collidable.model;
			if ((NULL!=cform) && (cftObject==cform->Type())){
				CActor* actor		= smart_cast<CActor*>(entity);
				CAI_Stalker* stalker= smart_cast<CAI_Stalker*>(entity);
				// в кого попали?
				if (actor && IsGameTypeSingle()/**/||stalker/**/){
					// попали в актера или сталкера
					Fsphere S		= cform->getSphere();
					entity->XFORM().transform_tiny	(S.P)	;
					float dist		= rd.range;
					// проверим попали ли мы в описывающую сферу 
					if (Fsphere::rpNone!=S.intersect_full(bullet->bullet_pos, bullet->dir, dist))
					{
						// да попали, найдем кто стрелял
						bool play_whine				= true;
						CObject* initiator			= Level().Objects.net_Find	(bullet->parent_id);
						if (actor){
							// попали в актера
							float hpf				= 1.f;
							float ahp				= actor->HitProbability();
#if 1
#	if 0
							CObject					*weapon_object = Level().Objects.net_Find	(bullet->weapon_id);
							if (weapon_object) {
								CWeapon				*weapon = smart_cast<CWeapon*>(weapon_object);
								if (weapon) {
									float fly_dist		= bullet->fly_dist+dist;
									float dist_factor	= _min(1.f,fly_dist/Level().BulletManager().m_fHPMaxDist);
									ahp					= dist_factor*weapon->hit_probability() + (1.f-dist_factor)*1.f;
								}
							}
#	else
							float					game_difficulty_hit_probability = actor->HitProbability();
							CAI_Stalker				*stalker = smart_cast<CAI_Stalker*>(initiator);
							if (stalker)
								hpf					= stalker->SpecificCharacter().hit_probability_factor();

							float					dist_factor = 1.f;
							CObject					*weapon_object = Level().Objects.net_Find	(bullet->weapon_id);
							if (weapon_object) {
								CWeapon				*weapon = smart_cast<CWeapon*>(weapon_object);
								if (weapon) {
									game_difficulty_hit_probability = weapon->hit_probability();
									float fly_dist	= bullet->fly_dist+dist;
									dist_factor		= _min(1.f,fly_dist/Level().BulletManager().m_fHPMaxDist);
								}
							}

							ahp						= dist_factor*game_difficulty_hit_probability + (1.f-dist_factor)*1.f;
#	endif
#else
							CAI_Stalker* i_stalker	= smart_cast<CAI_Stalker*>(initiator);
							// если стрелял сталкер, учитываем - hit_probability_factor сталкерa иначе - 1.0
							if (i_stalker) {
								hpf					= i_stalker->SpecificCharacter().hit_probability_factor();
								float fly_dist		= bullet->fly_dist+dist;
								float dist_factor	= _min(1.f,fly_dist/Level().BulletManager().m_fHPMaxDist);
								ahp					= dist_factor*actor->HitProbability() + (1.f-dist_factor)*1.f;
							}
#endif
							if (Random.randF(0.f,1.f)>(ahp*hpf)){ 
								bRes				= FALSE;	// don't hit actor
								play_whine			= true;		// play whine sound
							}else{
								// real test actor CFORM
								Level().BulletManager().m_rq_results.r_clear();

								if (cform->_RayQuery(rd,Level().BulletManager().m_rq_results)){
									bRes			= TRUE;		// hit actor
									play_whine		= false;	// don't play whine sound
								}else{
									bRes			= FALSE;	// don't hit actor
									play_whine		= true;		// play whine sound
								}
							}
						}
						// play whine sound
						if (play_whine){
							Fvector					pt;
							pt.mad					(bullet->bullet_pos, bullet->dir, dist);
							Level().BulletManager().PlayWhineSound				(bullet,initiator,pt);
						}
					}else{
						// don't test this object again (return FALSE)
						bRes		= FALSE;
					}

				}
			}
		}
	}
	
	return bRes;
}

//callback функция 
//	result.O;		// 0-static else CObject*
//	result.range;	// range from start to element 
//	result.element;	// if (O) "num tri" else "num bone"
//	params;			// user defined abstract data
//	Device.Statistic.TEST0.End();
//return TRUE-продолжить трассировку / FALSE-закончить трассировку

void CBulletManager::FireShotmark (SBullet* bullet, const Fvector& vDir, const Fvector &vEnd, collide::rq_result& R, u16 target_material, const Fvector& vNormal, bool ShowMark)
{
	SGameMtlPair* mtl_pair	= GMLib.GetMaterialPair(bullet->bullet_material_idx, target_material);
	Fvector particle_dir	= vNormal;

	if (R.O)
	{
/*  add_SkeletonWallmark not implemented now...
		particle_dir		 = vDir;
		particle_dir.invert	();

		//на текущем актере отметок не ставим
		if(Level().CurrentEntity() && Level().CurrentEntity()->ID() == R.O->ID()) return;

		if (mtl_pair && !mtl_pair->m_pCollideMarks->empty() && ShowMark)
		{
			//добавить отметку на материале
			Fvector p;
			p.mad(bullet->bullet_pos,bullet->dir,R.range-0.01f);
			if(!g_dedicated_server)
				::Render->add_SkeletonWallmark	(	&R.O->renderable.xform, 
													PKinematics(R.O->Visual()), 
													&*mtl_pair->m_pCollideMarks,
													p, 
													bullet->dir, 
													bullet->wallmark_size);
		}
*/
	} 
	else 
	{
		//вычислить нормаль к пораженной поверхности
		Fvector*	pVerts	= Level().ObjectSpace.GetStaticVerts();
		CDB::TRI*	pTri	= Level().ObjectSpace.GetStaticTris()+R.element;

		if (mtl_pair && !mtl_pair->m_pCollideMarks->empty() && ShowMark)
		{
			//добавить отметку на материале
			::Render->add_StaticWallmark	(&*mtl_pair->m_pCollideMarks, vEnd, bullet->wallmark_size, pTri, pVerts);
		}
	}

	ref_sound* pSound = (!mtl_pair || mtl_pair->CollideSounds.empty())?
						NULL:&mtl_pair->CollideSounds[::Random.randI(0,mtl_pair->CollideSounds.size())];

	//проиграть звук
	if(pSound && ShowMark)
	{
		CObject* O			= Level().Objects.net_Find(bullet->parent_id );
		bullet->m_mtl_snd	= *pSound;
		bullet->m_mtl_snd.play_at_pos(O, vEnd, 0);
	}

	LPCSTR ps_name = ( !mtl_pair || mtl_pair->CollideParticles.empty() ) ? NULL : 
		*mtl_pair->CollideParticles[ ::Random.randI(0,mtl_pair->CollideParticles.size()) ];

	SGameMtl*	tgt_mtl = GMLib.GetMaterialByIdx(target_material);
	BOOL bStatic = !tgt_mtl->Flags.test(SGameMtl::flDynamic);

	if( (ps_name && ShowMark) || (bullet->flags.explosive && bStatic) )
	{
		VERIFY2					(
			(particle_dir.x*particle_dir.x+particle_dir.y*particle_dir.y+particle_dir.z*particle_dir.z) > flt_zero,
			make_string("[%f][%f][%f]", VPUSH(particle_dir))
		);
		Fmatrix pos;
		pos.k.normalize(particle_dir);
		Fvector::generate_orthonormal_basis(pos.k, pos.j, pos.i);
		pos.c.set(vEnd);
		if(ps_name && ShowMark)
		{
			//отыграть партиклы попадания в материал
			CParticlesObject* ps = CParticlesObject::Create(ps_name,TRUE);

			ps->UpdateParent( pos, zero_vel );
			GamePersistent().ps_needtoplay.push_back( ps );
		}

		if( bullet->flags.explosive && bStatic )
		{
			PlayExplodePS( pos );
		}
	}
}

void CBulletManager::StaticObjectHit	(CBulletManager::_event& E)
{
//	Fvector hit_normal;
	FireShotmark(&E.bullet, E.bullet.dir,	E.point, E.R, E.tgt_material, E.normal);
//	ObjectHit	(&E.bullet,					E.point, E.R, E.tgt_material, hit_normal);
}

static bool g_clear = false;
void CBulletManager::DynamicObjectHit	(CBulletManager::_event& E)
{
	//только для динамических объектов
	VERIFY(E.R.O);

	if ( CEntity* entity = smart_cast<CEntity*>(E.R.O) )
	{
		if ( !entity->in_solid_state() )
		{
			return;
		}
	}

	if (g_clear) E.Repeated = false;
	if (GameID() == eGameIDSingle) E.Repeated = false;
	bool NeedShootmark = true;//!E.Repeated;
	
	if (smart_cast<CActor*>(E.R.O))
	{
		game_PlayerState* ps = Game().GetPlayerByGameID(E.R.O->ID());
		if (ps && ps->testFlag(GAME_PLAYER_FLAG_INVINCIBLE))
		{
			NeedShootmark = false;
		};
	}
	else if ( CBaseMonster * monster = smart_cast<CBaseMonster *>(E.R.O) )
	{
		NeedShootmark	=	monster->need_shotmark();
	}
	
	//визуальное обозначение попадание на объекте
//	Fvector			hit_normal;
	FireShotmark	(&E.bullet, E.bullet.dir, E.point, E.R, E.tgt_material, E.normal, NeedShootmark);
	
	Fvector original_dir = E.bullet.dir;
	//ObjectHit(&E.bullet, E.end_point, E.R, E.tgt_material, hit_normal);

	SBullet_Hit hit_param = E.hit_result;

	// object-space
	//вычислить координаты попадания
	Fvector				p_in_object_space,position_in_bone_space;
	Fmatrix				m_inv;
	m_inv.invert		(E.R.O->XFORM());
	m_inv.transform_tiny(p_in_object_space, E.point);

	// bone-space
	IKinematics* V = smart_cast<IKinematics*>(E.R.O->Visual());

	if(V)
	{
		VERIFY3(V->LL_GetBoneVisible(u16(E.R.element)),*E.R.O->cNameVisual(),V->LL_BoneName_dbg(u16(E.R.element)));
		Fmatrix& m_bone = (V->LL_GetBoneInstance(u16(E.R.element))).mTransform;
		Fmatrix  m_inv_bone;
		m_inv_bone.invert(m_bone);
		m_inv_bone.transform_tiny(position_in_bone_space, p_in_object_space);
	}
	else
	{
		position_in_bone_space.set(p_in_object_space);
	}

	//отправить хит пораженному объекту
	if (E.bullet.flags.allow_sendhit && !E.Repeated)
	{
		//-------------------------------------------------
		bool AddStatistic = false;
		if (GameID() != eGameIDSingle && E.bullet.flags.allow_sendhit && smart_cast<CActor*>(E.R.O)
			&& Game().m_WeaponUsageStatistic->CollectData())
		{
			CActor* pActor = smart_cast<CActor*>(E.R.O);
			if (pActor)// && pActor->g_Alive())
			{
				Game().m_WeaponUsageStatistic->OnBullet_Hit(&E.bullet, E.R.O->ID(), (s16)E.R.element, E.point);
				AddStatistic = true;
			};
		};

		SHit	Hit = SHit(	hit_param.power,
							original_dir,
							NULL,
							u16(E.R.element),
							position_in_bone_space,
							hit_param.impulse,
							E.bullet.hit_type,
							E.bullet.armor_piercing,
							E.bullet.flags.aim_bullet);

		Hit.GenHeader(u16((AddStatistic)? GE_HIT_STATISTIC : GE_HIT)&0xffff, E.R.O->ID());
		Hit.whoID			= E.bullet.parent_id;
		Hit.weaponID		= E.bullet.weapon_id;
		Hit.BulletID		= E.bullet.m_dwID;

		NET_Packet			np;
		Hit.Write_Packet	(np);
		
//		Msg("Hit sended: %d[%d,%d]", Hit.whoID, Hit.weaponID, Hit.BulletID);
		CGameObject::u_EventSend(np);
	}
}

#ifdef DEBUG
FvectorVec g_hit[3];
#endif

extern void random_dir	(Fvector& tgt_dir, const Fvector& src_dir, float dispersion);

bool CBulletManager::ObjectHit( SBullet_Hit* hit_res, SBullet* bullet, const Fvector& end_point, 
							    collide::rq_result& R, u16 target_material, Fvector& hit_normal )
{
	//----------- normal - start
	if ( R.O )
	{
		//вернуть нормаль по которой играть партиклы
		CCF_Skeleton* skeleton = smart_cast<CCF_Skeleton*>(R.O->CFORM());
		if ( skeleton )
		{
			Fvector			e_center;
			hit_normal.set	(0,0,0);
			if ( skeleton->_ElementCenter( (u16)R.element,e_center ) )
				hit_normal.sub							(end_point, e_center);
			float len		= hit_normal.square_magnitude();
			if ( !fis_zero(len) )	hit_normal.div		(_sqrt(len));
			else				hit_normal.invert	(bullet->dir);
		}
	}
	else
	{
		//вычислить нормаль к поверхности
		Fvector*	pVerts	=  Level().ObjectSpace.GetStaticVerts();
		CDB::TRI*	pTri	= Level().ObjectSpace.GetStaticTris()+R.element;
		hit_normal.mknormal	(pVerts[pTri->verts[0]],pVerts[pTri->verts[1]],pVerts[pTri->verts[2]]);
		if ( bullet->density_mode )
		{
			Fvector new_pos;
			new_pos.mad(bullet->bullet_pos, bullet->dir, R.range);
			float l = bullet->begin_density.distance_to(new_pos);
			float shootFactor = l * bullet->density;
			bullet->speed -= shootFactor;
			if ( bullet->speed < 0 ) bullet->speed = 0;
		}
		if ( DOT( hit_normal, bullet->dir ) < 0 )
		{
			if ( bullet->density_mode )
			{
//				Log("WARNING: Material in material found while bullet tracing. Incorrect behaviour of shooting is possible.");
			}
			bullet->density_mode = true;
			SGameMtl* mtl = GMLib.GetMaterialByIdx(target_material);
			bullet->density = mtl->fDensityFactor;
			bullet->begin_density.mad( bullet->bullet_pos, bullet->dir,R.range );
		}
		else
		{
			bullet->density_mode=false;
		}
	}		
	//----------- normal - end
	float old_speed = bullet->speed;

	//коэффициент уменьшение силы с падением скорости
	float speed_factor = bullet->speed / bullet->max_speed;
	//получить силу хита выстрела с учетом патрона
	*hit_res = bullet->hit_param; //default param
	
	hit_res->power = bullet->hit_param.power*speed_factor;
	
	//(Если = 0, то пуля либо рикошетит(если контакт идёт по касательной), либо застряёт в текущем 
	//объекте, если больше 0, то пуля прошивает объект)

	SGameMtl* mtl = GMLib.GetMaterialByIdx( target_material );
	float mtl_ap = mtl->fShootFactor;
	float shoot_factor = 0.0f; //default >> пуля НЕ пробила материал!
	float ap = bullet->armor_piercing;

	if ( ap > EPS && ap >= mtl_ap )
	{
		//пуля пробила материал
		shoot_factor = (( ap - mtl_ap ) / ap);
	}

	hit_res->impulse = 0.0f;
	float speed_scale = 0.0f;

#ifdef DEBUG
	//Fvector dbg_bullet_pos;
	//dbg_bullet_pos.mad(bullet->bullet_pos,bullet->dir,R.range);
	int bullet_state = 0;
#endif

	if ( fsimilar( mtl_ap, 0.0f ) )//Если материал полностью простреливаемый (кусты)
	{
#ifdef DEBUG
		bullet_state = 2;
#endif	
		return true;
	}

	if (bullet->flags.magnetic_beam && (shoot_factor > EPS))
	{
#ifdef DEBUG
		bullet_state = 2;
#endif
		//air resistance of magnetic_beam bullet is armor resistance too
		bullet->armor_piercing	-= mtl_ap * bullet->air_resistance;
		return true;
	}

	//рикошет
	Fvector			new_dir;
	new_dir.reflect	( bullet->dir,hit_normal );
	Fvector			tgt_dir;
	random_dir		( tgt_dir, new_dir, deg2rad( 10.0f ) );
	float ricoshet_factor = bullet->dir.dotproduct( tgt_dir );

	float f			= Random.randF( 0.5f, 0.8f ); //(0.5f,1.f);
	if ( (f < ricoshet_factor) && !mtl->Flags.test(SGameMtl::flNoRicoshet) && bullet->flags.allow_ricochet )	
	{
		// уменьшение скорости полета в зависимости от угла падения пули (чем прямее угол, тем больше потеря)
		bullet->flags.allow_ricochet = 0;
		float scale = 1.0f - _abs(bullet->dir.dotproduct(hit_normal)) * m_fCollisionEnergyMin;
		clamp(scale, 0.0f, m_fCollisionEnergyMax);
		speed_scale = scale;

		// вычисление рикошета, делается немного фейком, т.к. пуля остается в точке столкновения
		// и сразу выходит из RayQuery()
		bullet->dir.set				(tgt_dir);
		bullet->bullet_pos			= end_point;
		bullet->flags.ricochet_was	= 1;

#ifdef DEBUG
		bullet_state = 0;
#endif		
	}
	else if ( shoot_factor < EPS )
	{
		//застрявание пули в материале
		speed_scale = 0.0f;
#ifdef DEBUG
		bullet_state = 1;
#endif		
	}
	else
	{
		//пробивание материала
		speed_scale = shoot_factor;//mtl->fShootFactor;
		
		bullet->bullet_pos.mad(bullet->bullet_pos,bullet->dir,EPS);//fake
		//ввести коэффициент случайности при простреливании
		Fvector rand_normal;
		rand_normal.random_dir(bullet->dir, deg2rad(2.0f), Random);
		bullet->dir.set(rand_normal);
#ifdef DEBUG
		bullet_state = 2;
#endif
	}

	//уменьшить скорость в зависимости от простреливаемости
	bullet->speed *= speed_scale;
	//сколько энергии в процентах потеряла пуля при столкновении
	float energy_lost = 1.0f - bullet->speed / old_speed;
	//импульс переданный объекту равен прямопропорционален потерянной энергии
	hit_res->impulse = bullet->hit_param.impulse * speed_factor * energy_lost;


#ifdef DEBUG
	extern BOOL g_bDrawBulletHit;
	if(g_bDrawBulletHit)
	{
//		g_hit[bullet_state].push_back(dbg_bullet_pos);
		g_hit[bullet_state].push_back(end_point);
	}
#endif 

	return true;
}
