Files
Cyd 2b8ca921cb Initial full mirror of c:\VWE (source + assets + toolchain + outputs) via Git LFS
Complete disaster-recovery snapshot: engine/game source, game data assets,
VC6 toolchain + DX SDKs, build outputs, deployed game, and _UNUSED archive.
Large binaries in Git LFS; text preserved byte-for-byte (core.autocrlf=false,
no eol attributes). See RECOVERY.md for the one-clone rebuild procedure.
2026-06-24 21:28:16 -05:00

690 lines
16 KiB
C++

#include "MW4Headers.hpp"
#include "AI_Tactics.hpp"
#include "Helicopter.hpp"
#include "Mech.hpp"
#include "AI_TacticInterface.hpp"
#include "LRMWeaponSubsystem.hpp"
#include "MRMWeaponSubsystem.hpp"
#include "AI_Condition.hpp"
#include "Building.hpp"
#include "SubsystemClassData.hpp"
using namespace Stuff;
using namespace MW4AI;
using namespace MW4AI::Tactics;
using namespace MechWarrior4;
#pragma warning(disable:4239)
const Stuff::Time satisfaction_delta_per_second = 0.08f;
const Stuff::Scalar min_satisfaction_threshold = 0.1f;
const Stuff::Time duration_to_ignore_tactic_satisfaction = 8.0f; // we initially ignore whether or not the tactic is working for this many seconds
const Stuff::Scalar min_tactic_duration = 30.0f; // minimum duration of any tactic, in seconds
const Stuff::Scalar max_tactic_duration = 120.0f; // maximum duration of any tactic, in seconds
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
Tactics::Registrar* Registrar::instance = 0;
void Tactics::Registrar::IncrementRefCount(TacticInterface& i)
{
if (instance == 0)
{
gos_PushCurrentHeap(MechWarrior4::Heap);
instance = new Registrar(i);
gos_PopCurrentHeap();
}
else
{
Verify(instance->m_RefCount > 0);
}
++(instance->m_RefCount);
}
void Tactics::Registrar::DecrementRefCount()
{
Verify(instance != 0);
Verify(instance->m_RefCount > 0);
--(instance->m_RefCount);
if (instance->m_RefCount == 0)
{
delete instance;
instance = 0;
}
}
Tactics::Registrar& Registrar::GetInstance()
{
Verify(instance != 0);
Verify(instance->m_RefCount > 0);
return (*instance);
}
Registrar::Registrar(TacticInterface& iface)
: m_RefCount(0)
{
#define ADD(tactic) \
{ Auto_Ptr<Tactic> t(new tactic); \
m_Tactics.push_back(t); }
ADD(Circle);
ADD(Strafe);
ADD(CircleHover);
ADD(StandGround);
ADD(StopAndFire);
ADD(Ram);
ADD(Joust);
ADD(Rush);
ADD(HitAndRun);
ADD(Front);
ADD(Rear);
ADD(Retreat);
ADD(BackUpAndFire);
ADD(JumpAndShoot);
ADD(Snipe);
ADD(ShootOnly);
ADD(Defend);
ADD(LocalPatrol);
ADD(Surrender);
ADD(Ambush);
ADD(DeathFromAbove);
ADD(HeliPopup);
ADD(DiveBomb);
ADD(FastCircle);
ADD(Stare);
#undef ADD
}
Tactics::TacticID Registrar::SelectTactic(TacticInterface& iface, const std::vector<TacticID>& most_recent_tactics) const
{
// MSL 5.05
if (iface.GetSelf().IsDerivedFrom(Building::DefaultData) == true)
{
return (TACTIC_SHOOT_ONLY);
}
if ((iface.GetSelf().IsDerivedFrom(Vehicle::DefaultData) == false) ||
(iface.IsShootOnlyAI() == true))
{
return (TACTIC_SHOOT_ONLY);
}
if (iface.GetSelf().IsDerivedFrom(Helicopter::DefaultData) == true)
{
return (TACTIC_CIRCLE_HOVER);
}
if (iface.GetSelf().IsDerivedFrom(Airplane::DefaultData) == true)
{
MechWarrior4::Airplane* plane = Cast_Object(MechWarrior4::Airplane*,&(iface.GetSelf()));
const MechWarrior4::Airplane::GameModel* game_model = plane->GetGameModel();
switch (game_model->m_attackType)
{
default:
{
Verify(!"Error: no airplane attack type specified.");
return (TACTIC_DIVE_BOMB);
}
case MechWarrior4::Vehicle::NightShadeAttackType:
{
return (TACTIC_DIVE_BOMB);
}
case MechWarrior4::Vehicle::ShiloneAttackType:
{
return (TACTIC_STRAFE);
}
}
}
if (iface.GetSelf().IsDerivedFrom(Mech::DefaultData) == false)
{
if (iface.GetMood() <= 2)
{
return (TACTIC_RETREAT);
}
return (TACTIC_SNIPE);
}
std::vector<Tactic*> tactics;
{for (tactic_container::const_iterator i = m_Tactics.begin();
i != m_Tactics.end();
++i)
{
if (((*i)->CanBeSelectedAutomatically(iface) == true) &&
(std::find(most_recent_tactics.begin(),most_recent_tactics.end(),(*i)->GetID()) == most_recent_tactics.end()))
{
tactics.push_back(*i);
}
}}
if (tactics.size() == 0)
{
{for (tactic_container::const_iterator i = m_Tactics.begin();
i != m_Tactics.end();
++i)
{
if ((*i)->CanBeSelectedAutomatically(iface) == true)
{
if (((*i)->GetID() != TACTIC_RETREAT) ||
(iface.CanRetreat() == true))
{
tactics.push_back(*i);
}
}
}}
if (tactics.size() == 0)
{
return (TACTIC_CIRCLE_OF_DEATH);
}
}
if (tactics.size() == 1)
{
return ((*(tactics.begin()))->GetID());
}
while (tactics.size() > 2)
{
std::vector<Tactic*>::iterator worst = tactics.end();
Stuff::Scalar worst_score = 100;
{for (std::vector<Tactic*>::iterator i = tactics.begin();
i != tactics.end();
++i)
{
const Stuff::Scalar score((*i)->Evaluate(iface));
if (score <= worst_score)
{
worst_score = score;
worst = i;
}
}}
tactics.erase(worst);
}
return (tactics[Stuff::Random::GetInt() & 0x01]->GetID());
}
Tactic& Registrar::GetTactic(TacticID id) const
{
{for (tactic_container::const_iterator i = m_Tactics.begin();
i != m_Tactics.end();
++i)
{
if ((*i)->GetID() == id)
{
return (**i);
}
}}
Verify(!"Should never get here!");
return (**(m_Tactics.begin()));
}
Auto_Ptr<MW4AI::Tactics::Tactic> MW4AI::Tactics::CreateTactic(TacticInterface& i, TacticID id)
{
Tactic* t = 0;
switch (id)
{
case TACTIC_STOP_AND_FIRE: { t = new StopAndFire(); break; }
case TACTIC_RAM: { t = new Ram(); break; }
case TACTIC_JOUST: { t = new Joust(); break; }
case TACTIC_RUSH: { t = new Rush(); break; }
case TACTIC_HIT_AND_RUN: { t = new HitAndRun(); break; }
case TACTIC_FRONT: { t = new Front(); break; }
case TACTIC_REAR: { t = new Rear(); break; }
case TACTIC_CIRCLE_OF_DEATH: { t = new Circle(); break; }
case TACTIC_RETREAT: { t = new Retreat(); break; }
case TACTIC_BACK_UP_AND_FIRE: { t = new BackUpAndFire(); break; }
case TACTIC_CIRCLE_HOVER: { t = new CircleHover(); break; }
case TACTIC_STRAFE: { t = new Strafe(); break; }
case TACTIC_STAND_GROUND: { t = new StandGround(); break; }
case TACTIC_JUMP_AND_SHOOT: { t = new JumpAndShoot(); break; }
case TACTIC_SNIPE: { t = new Snipe(); break; }
case TACTIC_SHOOT_ONLY: { t = new ShootOnly(); break; }
case TACTIC_DEFEND: { t = new Defend(); break; }
case TACTIC_LOCAL_PATROL: { t = new LocalPatrol(); break; }
case TACTIC_SURRENDER: { t = new Surrender(); break; }
case TACTIC_AMBUSH: { t = new Ambush(); break; }
case TACTIC_DEATH_FROM_ABOVE: { t = new DeathFromAbove(); break; }
case TACTIC_HELI_POPUP: { t = new HeliPopup(); break; }
case TACTIC_DIVE_BOMB: { t = new DiveBomb(); break; }
case TACTIC_FAST_CIRCLE: { t = new FastCircle(); break; }
case TACTIC_STARE: { t = new Stare(); break; }
default:
{
Verify("Tactic not found!" == 0);
}
}
if (t == 0)
{
return (0);
}
return (t);
}
Registrar::~Registrar()
{
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// General tactics functions
//
void VerifyTactic(TacticID id)
{
Verify(id >= TACTIC_FIRST);
Verify(id <= TACTIC_LAST);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Tactic
//
void Tactic::SetExplicit(bool is_explicit)
{
m_Explicit = is_explicit;
}
bool Tactic::GetExplicit() const
{
return (m_Explicit);
}
bool Tactic::CanBeSelectedAutomatically(TacticInterface& iface) const
{
return (false);
}
Tactic::Tactic()
: m_CreationTime((Stuff::Scalar)gos_GetElapsedTime())
, m_CreationMood(-1)
, m_Satisfaction(1.0f)
, m_LastSatisfactionUpdate(0)
, m_TacticToIgnore((TacticID)0)
{
m_AutoEndTime = m_CreationTime + min_tactic_duration + ((max_tactic_duration - min_tactic_duration) * Stuff::Random::GetFraction());
m_CreationTime += Stuff::Random::GetFraction();
}
void Tactic::SetIgnoreExplicitTactic(TacticID tactic)
{
m_TacticToIgnore = tactic;
}
TacticID Tactic::GetIgnoreExplicitTactic() const
{
return (m_TacticToIgnore);
}
Tactic::~Tactic()
{
}
Stuff::Scalar Tactic::GetDuration() const
{
return ((Stuff::Scalar)gos_GetElapsedTime() - m_CreationTime);
}
void Tactic::Update(TacticInterface& i)
{
if (m_CreationMood == -1)
{
m_CreationMood = (Stuff::Scalar)i.GetMood();
}
}
bool Tactic::ShouldAbandon(TacticInterface& i) const
{
if (ShouldAbandonImmediately() == true)
{
m_Explicit = false;
return (true);
}
if ((m_Explicit == true) ||
(GetDuration() < UserConstants::Instance()->Get(UserConstants::min_tactic_time)) ||
(m_CreationMood == -1))
{
return (false);
}
if ((i.GetSelf().IsDerivedFrom(Building::DefaultData) == true) &&
(i.GetSelf().IsDerivedFrom(Airplane::DefaultData) == true))
{
return (false);
}
Stuff::Scalar mood_delta = Stuff::Fabs(m_CreationMood - i.GetMood());
if (mood_delta > 1)
{
return (true);
}
if (gos_GetElapsedTime() > m_AutoEndTime)
{
return (true);
}
return (IsIneffective());
}
bool Tactic::IsIneffective() const
{
if (m_Satisfaction <= min_satisfaction_threshold)
{
return (true);
}
return (false);
}
Stuff::Scalar Tactic::Evaluate(TacticInterface& iface) const
{
return (0);
}
void Tactic::UpdateSatisfaction()
{
if ((m_LastSatisfactionUpdate != 0) &&
(m_CreationTime + duration_to_ignore_tactic_satisfaction < gos_GetElapsedTime()))
{
const Stuff::Time time_delta = gos_GetElapsedTime() - m_LastSatisfactionUpdate;
if (IsHappy() == true)
{
m_Satisfaction += (Stuff::Scalar)(satisfaction_delta_per_second * time_delta);
if (m_Satisfaction > 1.0f)
{
m_Satisfaction = 1.0f;
}
}
else
{
m_Satisfaction -= (Stuff::Scalar)(satisfaction_delta_per_second * time_delta);
if (m_Satisfaction < 0.0f)
{
m_Satisfaction = 0.0f;
}
}
}
m_LastSatisfactionUpdate = gos_GetElapsedTime();
}
bool Tactic::Finished() const
{
if (ShouldAbandonImmediately() == true)
{
return (true);
}
if ((m_TacticToIgnore != (TacticID)0) &&
(GetID() != m_TacticToIgnore))
{
return (true);
}
return (false);
}
bool Tactic::ShouldIgnoreLOS() const
{
return (false);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Tactic
//
inline bool Qualifies(TacticInterface& iface,
UserConstants::ID min_elite, UserConstants::ID max_elite,
UserConstants::ID min_mood, UserConstants::ID max_mood)
{
if ((iface.GetEliteLevel() < UserConstants::Instance()->Get(min_elite)) ||
(iface.GetEliteLevel() > UserConstants::Instance()->Get(max_elite)))
{
return (false);
}
if ((iface.GetMood() < UserConstants::Instance()->Get(min_mood)) ||
(iface.GetMood() > UserConstants::Instance()->Get(max_mood)))
{
return (false);
}
return (true);
}
Stuff::Scalar Score_FasterThanTarget(TacticInterface& iface)
{
Vehicle* v1 = iface.GetSelf_AsVehicle();
Vehicle* v2 = iface.GetTarget_AsVehicle();
Stuff::Scalar my_speed(0);
if (v1 != 0)
{
my_speed = v1->GetGameModel()->maxSpeed;
}
Stuff::Scalar target_speed(0);
if (v2 != 0)
{
target_speed = v2->GetGameModel()->maxSpeed;
}
if (my_speed > target_speed)
{
return (1);
}
return (0);
}
Stuff::Scalar Score_HaveMissiles(TacticInterface& iface)
{
Stuff::ChainIteratorOf<MechWarrior4::Weapon*> i(&(iface.GetSelf().weaponChain));
MechWarrior4::Weapon* weapon = 0;
while ((weapon = i.ReadAndNext()) != 0)
{
if (weapon->GetAmmoCount() > 0) // TODO: comment this back in when ammo # works correctly ...
{
if ((weapon->IsDerivedFrom(LRMWeaponSubsystem::DefaultData) == true) ||
(weapon->IsDerivedFrom(MRMWeaponSubsystem::DefaultData) == true))
{
return (1);
}
}
}
return (0);
}
Stuff::Scalar Score_TargetFacingAway(TacticInterface& iface)
{
if (Conditions::InFrontOfTarget(iface,true) < 0.5f)
{
return (1);
}
return (0);
}
Stuff::Scalar Score_FarFromTarget(TacticInterface& iface)
{
if (GetApproximateLength((Stuff::Point3D)iface.GetSelf().GetLocalToWorld(),
(Stuff::Point3D)iface.GetTarget().GetLocalToWorld()) > 250)
{
return (1);
}
return (0);
}
Stuff::Scalar Score_Gimped(TacticInterface& iface)
{
if (iface.NumLegsDestroyed() == 1)
{
return (1);
}
return (0);
}
Stuff::Scalar Score_WantsToUseVCH(TacticInterface& iface)
{
if (iface.GetCurrentVulnerableComponent() != 0)
{
return (1);
}
return (0);
}
bool Joust::CanBeSelectedAutomatically(TacticInterface& iface) const
{
return (Qualifies(iface,
UserConstants::min_elite_joust,
UserConstants::max_elite_joust,
UserConstants::min_mood_joust,
UserConstants::max_mood_joust));
}
Stuff::Scalar Joust::Evaluate(TacticInterface& iface) const
{
return ( ( 0.5f * Score_FasterThanTarget(iface))
+ (-1.0f * Score_FarFromTarget(iface))
+ (-1.0f * Score_Gimped(iface))
+ (-1.0f * Score_WantsToUseVCH(iface)));
}
bool Rush::CanBeSelectedAutomatically(TacticInterface& iface) const
{
return (Qualifies(iface,
UserConstants::min_elite_rush,
UserConstants::max_elite_rush,
UserConstants::min_mood_rush,
UserConstants::max_mood_rush));
}
Stuff::Scalar Rush::Evaluate(TacticInterface& iface) const
{
return ( (-0.5f * Score_FasterThanTarget(iface))
+ (-1.0f * Score_HaveMissiles(iface))
+ ( 1.0f * Score_TargetFacingAway(iface))
+ (-0.5f * Score_FarFromTarget(iface))
+ ( 1.0f * Score_Gimped(iface))
+ (-0.5f * Score_WantsToUseVCH(iface)));
}
bool HitAndRun::CanBeSelectedAutomatically(TacticInterface& iface) const
{
return (Qualifies(iface,
UserConstants::min_elite_hitandrun,
UserConstants::max_elite_hitandrun,
UserConstants::min_mood_hitandrun,
UserConstants::max_mood_hitandrun));
}
Stuff::Scalar HitAndRun::Evaluate(TacticInterface& iface) const
{
return ( ( 1.0f * Score_FasterThanTarget(iface))
+ (-1.0f * Score_Gimped(iface))
+ (-1.0f * Score_WantsToUseVCH(iface)));
}
bool Rear::CanBeSelectedAutomatically(TacticInterface& iface) const
{
return (Qualifies(iface,
UserConstants::min_elite_rear,
UserConstants::max_elite_rear,
UserConstants::min_mood_rear,
UserConstants::max_mood_rear));
}
Stuff::Scalar Rear::Evaluate(TacticInterface& iface) const
{
return ( ( 1.0f * Score_TargetFacingAway(iface))
+ ( 1.0f * Score_Gimped(iface))
+ ( 0.5f * Score_WantsToUseVCH(iface)));
}
bool Circle::CanBeSelectedAutomatically(TacticInterface& iface) const
{
return (Qualifies(iface,
UserConstants::min_elite_circle,
UserConstants::max_elite_circle,
UserConstants::min_mood_circle,
UserConstants::max_mood_circle));
}
Stuff::Scalar Circle::Evaluate(TacticInterface& iface) const
{
return ( ( 0.5f * Score_FasterThanTarget(iface))
+ ( 1.0f * Score_HaveMissiles(iface))
+ ( 0.5f * Score_FarFromTarget(iface))
+ (-0.5f * Score_Gimped(iface))
+ ( 1.0f * Score_WantsToUseVCH(iface)));
}
bool Retreat::CanBeSelectedAutomatically(TacticInterface& iface) const
{
return (Qualifies(iface,
UserConstants::min_elite_retreat,
UserConstants::max_elite_retreat,
UserConstants::min_mood_retreat,
UserConstants::max_mood_retreat));
}
Stuff::Scalar Retreat::Evaluate(TacticInterface& iface) const
{
return ( ( 1.0f * Score_FasterThanTarget(iface))
+ (-1.0f * Score_TargetFacingAway(iface))
+ (-1.0f * Score_FarFromTarget(iface))
+ (-1.0f * Score_Gimped(iface))
+ (-0.5f * Score_WantsToUseVCH(iface)));
}
bool DeathFromAbove::CanBeSelectedAutomatically(TacticInterface& iface) const
{
if ((iface.CanJump() == false) ||
(iface.GetTarget().IsDerivedFrom(Mech::DefaultData) == false))
{
return (false);
}
return (Qualifies(iface,
UserConstants::min_elite_dfa,
UserConstants::max_elite_dfa,
UserConstants::min_mood_dfa,
UserConstants::max_mood_dfa));
}
Stuff::Scalar DeathFromAbove::Evaluate(TacticInterface& iface) const
{
return ( ( 1.0f * Score_FasterThanTarget(iface))
+ (-1.0f * Score_FarFromTarget(iface))
+ (-2.0f * Score_Gimped(iface))
+ (-0.5f * Score_WantsToUseVCH(iface)));
}