//============================================================================//
// File:	seeker.cpp                                                          	//
// Project: BattleTech																			//
// Contents: A subsystem which guides, seeks out and finds a target           	//
//----------------------------------------------------------------------------//
// Copyright (C) 1995, Virtual World Entertainment, Inc. All Rights reserved  //
//                   	PROPRIETARY AND CONFIDENTIAL                          //
//============================================================================//
//
// RECONSTRUCTED from the shipped binary.  Behaviour follows the Ghidra
// pseudo-C in part_013.c (@004bec34-@004bee70); member / method names follow
// the surviving SEEKER.HPP and MISSILE.TCP.  Each non-trivial method cites the
// originating @ADDR.  Hex float constants converted to decimal:
//      0x3c8efa35 = 0.0174533f (PI/180, deg->rad)   @004be8b8
//      0x43480000 = 200.0f   0x42480000 = 50.0f   0x43960000 = 300.0f
//
// Coverage:
//   confident : ctor @004bec34, LeadTarget @004beae4, dtor thunk @004bf890-chain
//   best-effort: FindTarget (folded into Missile guidance; see header note),
//                ResetToInitialState / TestInstance (inherit Subsystem base)
//   excluded   : the 0x41xxxx engine vtable slots (Subsystem base behaviour)
//
// Helper-function name mapping (engine internals referenced by the decomp):
//      FUN_0041c52c  Subsystem base constructor (owner, id, name, sharedData)
//      FUN_00408644  Vector3D = a - b              (subtract)
//      FUN_004085ec  Vector3D += b                 (accumulate)
//      FUN_004086ac  Vector3D = base + dir*scale    (scaled add)
//      FUN_00408b98  build/normalise direction
//      FUN_00408440  Point3D copy
//      FUN_0040a7f4  Point3D copy (read seeker target into thruster buffer)
//      FUN_004dd138  sqrt()        FUN_0041a1a4  IsDerivedFrom(classDerivations)
//      FUN_0041bbd8  AlarmIndicator::SetLevel
//

#include <bt.hpp>
#pragma hdrstop

#if !defined(SEEKER_HPP)
#	include <seeker.hpp>
#endif
#if !defined(MISSILE_HPP)
#	include <missile.hpp>
#endif
#if !defined(TESTBT_HPP)
#	include <testbt.hpp>
#endif

//
// Lead/guidance tuning constants, read as read-only globals in the decomp.
// (Decoded from .rdata @004bec18..@004bec28.)
//
static const Scalar SeekerLeadMinRange  = 200.0f;	// _DAT_004bec18  start leading past this range
static const Scalar SeekerLeadZero      =   0.0f;	// _DAT_004bec1c
static const Scalar SeekerLeadFloor     =  50.0f;	// _DAT_004bec20
static const Scalar SeekerLeadMaxClamp  = 300.0f;	// _DAT_004bec24  lead distance clamp
static const Scalar SeekerLeadCoef      =   0.1f;	// _DAT_004bec28  (repeating-C double 0.1; loft/lead gain)

//###########################################################################
//###########################################################################
//					Seeker
//###########################################################################
//###########################################################################

//#############################################################################
// Shared Data Support
//
Derivation
	Seeker::ClassDerivations(
		Subsystem::GetClassDerivations(),
		"Seeker"
	);

Receiver::MessageHandlerSet		Seeker::MessageHandlers;

const Seeker::IndexEntry
	Seeker::AttributePointers[] = { { 0, 0, 0 } };	// IndexEntry table (see header @00512d50)

Seeker::AttributeIndexSet		Seeker::AttributeIndex;

Seeker::SharedData
	Seeker::DefaultData(						// resolved as &DAT_00512bec
		&Seeker::ClassDerivations,
		Seeker::MessageHandlers,
		Seeker::AttributeIndex,
		Seeker::StateCount
	);

//#############################################################################
// Construction
//
// @004bec34  Seeker::Seeker(Missile *owner, int id, SubsystemResource*,
//                           Entity *target, const Point3D offset)
//
//   Chains the Subsystem base ctor, stamps the Seeker vtable, then snapshots
//   the firing geometry: creationPoint <- owner's current position, the homing
//   target + aim offset, and the initial range readings.
//
Seeker::Seeker(
	Missile				*owner,
	int					subsystem_ID,
	SubsystemResource	*subsystem_resource,
	Entity				*target,
	const Point3D		offset)
:
	// Subsystem base ctor: owner, id, resource, shared default data.
	Subsystem(owner, subsystem_ID, subsystem_resource, DefaultData)
{
	// vtable installed by the compiler (PTR_LAB_00512ce8)

	// CROSS-FAMILY (Entity/Missile kinematics): the owning Missile's current world
	// position/facing and the target's world position are read from the Entity
	// transform.  Those accessors live in the Entity/missile families; the spawn
	// snapshot below uses the launch geometry available here.
	Point3D ownerPos(0.0f, 0.0f, 0.0f);					// owner + 0x100 (missile spawn position)

	creationPoint = ownerPos;							// @0xF0  this[0x3c..0x3e]

	// homing parameters
	targetEntity  = target;								// @0xFC  this[0x3f]
	targetOffset  = offset;								// @0x100 this[0x40..0x42]

	// Initial aim point: if dumb-fire (target == 0) aim straight ahead from the
	// owner's facing, otherwise aim at the target's mount + offset.
	if (targetEntity == 0)
	{
		// project the aim point a small distance ahead of the launch facing
		targetPosition = targetOffset;					// @004bec34 (target==0 branch)
	}
	else
	{
		Point3D targetPos(0.0f, 0.0f, 0.0f);			// targetEntity + 0x100
		targetPosition.x = targetOffset.x + targetPos.x;
		targetPosition.y = targetOffset.y + targetPos.y;
		targetPosition.z = targetOffset.z + targetPos.z;
	}

	// rangeToTarget      = |targetPosition - ownerPos|   (sampled at spawn)
	// startingRangeToTarget = rangeToTarget
	// rangeFromCreation  = |ownerPos - creationPoint|    (== 0 at spawn)
	Vector3D delta;
	delta.Subtract(targetPosition, ownerPos);
	rangeToTarget         = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z);	// @0x10C
	startingRangeToTarget = rangeToTarget;				// @0x114  this[0x45]

	delta.Subtract(ownerPos, creationPoint);
	rangeFromCreation     = (Scalar)Sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z);	// @0x110
}

//#############################################################################
// Destruction
//
// @004bee44  ~Seeker() -- restores the Seeker vtable then chains the Subsystem
// base destructor.  (Reached through the scalar-deleting thunk FUN_004bee70.)
//
Seeker::~Seeker()
{
	// vtable reset + base ~Subsystem chaining handled by the compiler.
}

//#############################################################################
// Guidance
//
// @004beae4  Seeker::LeadTarget()
//
//   Per-frame homing update.  When a live targetEntity exists, build the
//   vector from the current targetPosition back to the missile, measure the
//   range, and -- once outside SeekerLeadMinRange (200 m) -- nudge the aim
//   point upward/ahead by SeekerLeadCoef * clamp(range-200, .., 300).  The
//   lead is also projected along the target's own velocity so a moving target
//   is intercepted rather than chased.
//
void Seeker::LeadTarget()
{
	if (targetEntity == 0)								// @004beae4  if (this[0xfc] != 0)
		return;

	// CROSS-FAMILY (Entity/Missile kinematics): the owning Missile's world
	// position + bounding radius and the target's velocity / "alive" state are
	// read from the Entity transform layer.  Isolated as documented locals here.
	Point3D  ownerPos(0.0f, 0.0f, 0.0f);				// owner + 0x100
	Scalar   boundingRadius = 1.0f;						// owner + 0x348 (avoid /0)
	Logical  targetTeamValid = True;					// owner team chain (best-effort)

	// vector from aim point to the missile, and its length
	Vector3D toMissile;
	toMissile.Subtract(targetPosition, ownerPos);		// @004beae4  FUN_00408644(.., +0xe4, owner+0x100)
	Scalar range    = (Scalar)Sqrt(toMissile.x*toMissile.x + toMissile.y*toMissile.y + toMissile.z*toMissile.z);
	Scalar approach = range / boundingRadius;			// owner + 0x348

	if (range > SeekerLeadMinRange && targetTeamValid)
	{
		Scalar lead = range - SeekerLeadMinRange;
		if (toMissile.y >= SeekerLeadFloor && lead > SeekerLeadMaxClamp)
			lead = SeekerLeadMaxClamp;					// clamp the loft contribution
		targetPosition.y += SeekerLeadCoef * lead;		// @0xE8  this[0x3a]
	}

	// if the target is itself a mover, advance the aim point along its velocity
	if (targetEntity->TestInstance())					// FUN_0041a1a4(.., 0x4e4518) Mover check
	{
		Vector3D targetVelocity(0.0f, 0.0f, 0.0f);		// targetEntity velocity
		targetPosition.x += targetVelocity.x * approach;	// @004beae4  FUN_004086ac / FUN_004085ec
		targetPosition.y += targetVelocity.y * approach;
		targetPosition.z += targetVelocity.z * approach;
	}
}

//#############################################################################
// FindTarget -- per-frame Performance entry (best-effort)
//
//   The shipped Missile reads Seeker::targetPosition directly during its
//   integration pass (Missile::MoveAndCollide @004bef78 samples this+0x1c4),
//   so the standalone FindTarget reduces to re-leading the target each slice.
//   No distinct @ADDR survives; this mirrors MISSILE.TCP's call site.
//
void Seeker::FindTarget(Scalar /*time_slice*/)
{
	LeadTarget();
}

//#############################################################################
// ResetToInitialState / TestInstance -- inherit the Subsystem base
// implementations (vtable @00512ce8 slots 10/.. are all 0x41xxxx engine code).
//
void Seeker::ResetToInitialState()
{
	// The engine Subsystem base exposes no ResetToInitialState; the Seeker simply
	// re-zeros its transient guidance range readings.
	rangeToTarget         = 0.0f;
	rangeFromCreation     = 0.0f;
	startingRangeToTarget = 0.0f;
}

Logical Seeker::TestInstance() const
{
	return Subsystem::TestInstance();
}
