forked from Mirrors/openclonk
209 lines
6.1 KiB
C
209 lines
6.1 KiB
C
/**
|
|
AI
|
|
Controls enemy NPC behaviour. Different parts of the AI are in the different include
|
|
files below. This AI can be overloaded by making a new AI object and including this
|
|
one and then add the needed changes. The relevant settings are all local constants
|
|
which can be directly changed in the new AI object, or functions can be overloaded.
|
|
|
|
Optionally, a custom AI can also be implemented by including AI_Controller and
|
|
all desired AI components.
|
|
|
|
@author Sven2, Maikel, Marky
|
|
*/
|
|
|
|
// Include the basic functionality
|
|
#include AI_Controller
|
|
// Include the different parts of the AI.
|
|
#include AI_Appearance
|
|
#include AI_Debugging
|
|
#include AI_HelperFunctions
|
|
#include AI_MeleeWeapons
|
|
#include AI_Movement
|
|
#include AI_Protection
|
|
#include AI_RangedWeapons
|
|
#include AI_TargetFinding
|
|
#include AI_Vehicles
|
|
#include AI_AttackModes
|
|
#include AI_HelperClonk
|
|
#include AI_HomePosition
|
|
#include AI_AttackEnemy
|
|
#include AI_Inventory
|
|
|
|
/*-- Callbacks --*/
|
|
|
|
// Timer interval for the effect
|
|
public func GetTimerInterval() { return 3; }
|
|
|
|
|
|
/*-- Editor Properties --*/
|
|
|
|
// Callback from the Definition()-call
|
|
public func OnDefineAI(proplist def)
|
|
{
|
|
_inherited(def);
|
|
|
|
// Can be added to Clonk
|
|
AddEditorProp_AISelection(Clonk, AI);
|
|
}
|
|
|
|
local EditorHelp = "$EditorHelp$";
|
|
|
|
|
|
/*-- AI Execution --*/
|
|
|
|
public func Execute(effect fx, int time)
|
|
{
|
|
fx.time = time;
|
|
// Evasion, healing etc. if alert.
|
|
if (fx.alert)
|
|
if (this->ExecuteProtection(fx))
|
|
return true;
|
|
// Current command override.
|
|
if (fx.command)
|
|
{
|
|
if (this->Call(fx.command, fx))
|
|
return true;
|
|
else
|
|
fx.command = nil;
|
|
}
|
|
// Find something to fight with.
|
|
if (!fx.weapon)
|
|
{
|
|
fx.can_attack_structures = false;
|
|
this->CancelAiming(fx);
|
|
if (!this->ExecuteArm(fx))
|
|
return this->ExecuteIdle(fx);
|
|
else if (!fx.weapon)
|
|
return true;
|
|
}
|
|
// Weapon out of ammo?
|
|
if (fx.ammo_check && !this->Call(fx.ammo_check, fx, fx.weapon))
|
|
{
|
|
this->LogAI_Warning(fx, Format("Weapon %v is out of ammo, AI won't do anything.", fx.weapon));
|
|
fx.weapon = nil;
|
|
return false;
|
|
}
|
|
// Find an enemy.
|
|
if (fx.target)
|
|
if ((fx.target->GetCategory() & C4D_Living && !fx.target->GetAlive()) || (!fx.ranged && fx.Target->ObjectDistance(fx.target) >= fx.max_aggro_distance))
|
|
{
|
|
this->LogAI_Info(fx, Format("Forgetting target %v, because it is dead or out of range", fx.target));
|
|
fx.target = nil;
|
|
}
|
|
if (!fx.target)
|
|
{
|
|
this->CancelAiming(fx);
|
|
if (!fx.auto_search_target || !(fx.target = this->FindTarget(fx)))
|
|
{
|
|
this->LogAI_Warning(fx, Format("Will call ExecuteIdle, because there is no target. Auto-searching for target %v", fx.auto_search_target));
|
|
return ExecuteIdle(fx);
|
|
}
|
|
// First encounter callback. might display a message.
|
|
if (fx.encounter_cb)
|
|
if (GameCall(fx.encounter_cb, fx.Target, fx.target))
|
|
fx.encounter_cb = nil;
|
|
// Wake up nearby allies.
|
|
if (fx.ally_alert_range)
|
|
{
|
|
var ally_fx;
|
|
for (var ally in fx.Target->FindObjects(Find_Distance(fx.ally_alert_range), Find_Exclude(fx.Target), Find_OCF(OCF_CrewMember), Find_Owner(fx.Target->GetOwner())))
|
|
if (ally_fx = this->GetAI(ally))
|
|
if (!ally_fx.target)
|
|
{
|
|
ally_fx.target = fx.target;
|
|
ally_fx.alert = ally_fx.time;
|
|
if (ally_fx.encounter_cb)
|
|
if (GameCall(ally_fx.encounter_cb, ally, fx.target))
|
|
ally_fx.encounter_cb = nil;
|
|
}
|
|
// Do some messages.
|
|
this->ExecuteIntruderMessage(fx);
|
|
// Waking up works only once. after that, AI might have moved and wake up clonks it shouldn't.
|
|
fx.ally_alert_range = nil;
|
|
}
|
|
}
|
|
// Do stuff on the appearance of the enemy like displaying a message.
|
|
this->ExecuteAppearance(fx);
|
|
// Attack it!
|
|
if (!this->IsWeaponForTarget(fx))
|
|
this->LogAI_Warning(fx, Format("Weapon of type %i is not fit to attack %v (type: %i).", fx.weapon->GetID(), fx.target, fx.target->GetID()));
|
|
|
|
this->LogAI_Info(fx, Format("Calling strategy: %v", fx.strategy));
|
|
return this->Call(fx.strategy, fx);
|
|
}
|
|
|
|
|
|
public func ExecuteThrow(effect fx)
|
|
{
|
|
// Still carrying the weapon to throw?
|
|
if (fx.weapon->Contained() != fx.Target)
|
|
{
|
|
fx.weapon = nil;
|
|
return false;
|
|
}
|
|
// Path to target free?
|
|
var x = fx.Target->GetX(), y = fx.Target->GetY(), tx = fx.target->GetX(), ty = fx.target->GetY();
|
|
if (PathFree(x, y, tx, ty))
|
|
{
|
|
var throw_speed = fx.Target.ThrowSpeed;
|
|
var rx = (throw_speed * throw_speed) / (100 * GetGravity()); // horizontal range for 45 degree throw if enemy is on same height as we are
|
|
var ry = throw_speed * 7 / (GetGravity() * 10); // vertical range of 45 degree throw
|
|
var dx = tx - x, dy = ty - y + 15 * fx.Target->GetCon() / 100; // distance to target. Reduce vertical distance a bit because throwing exit point is not at center
|
|
// Check range
|
|
// Could calculate the optimal parabulum here, but that's actually not very reliable on moving targets
|
|
// It's usually better to throw straight at the target and only throw upwards a bit if the target stands on high ground or is far away
|
|
// Also ignoring speed added by own velocity, etc...
|
|
if (Abs(dx) * ry - Min(dy) * rx <= rx * ry)
|
|
{
|
|
// We're in range. Can throw?
|
|
if (!this->CheckHandsAction(fx))
|
|
return true;
|
|
// OK. Calc throwing direction.
|
|
dy -= dx * dx / rx;
|
|
// And throw!
|
|
fx.Target->SetCommand("None");
|
|
fx.Target->SetComDir(COMD_Stop);
|
|
this->SelectItem(fx, fx.weapon);
|
|
return fx.Target->ControlThrow(fx.weapon, dx, dy);
|
|
}
|
|
}
|
|
// Can't reach target yet. Walk towards it.
|
|
if (!fx.Target->GetCommand() || !Random(3))
|
|
fx.Target->SetCommand("MoveTo", fx.target);
|
|
return true;
|
|
}
|
|
|
|
|
|
public func ExecuteArm(effect fx)
|
|
{
|
|
// Find shield.
|
|
fx.shield = fx.Target->FindContents(Shield);
|
|
// Vehicle control overrides all other weapons
|
|
if (fx.weapon = fx.vehicle)
|
|
{
|
|
if (this->CheckVehicleAmmo(fx, fx.weapon))
|
|
{
|
|
this->LogAI_Info(fx, "Vehicle ammo is ok");
|
|
fx.strategy = this.ExecuteVehicle;
|
|
fx.ranged = true;
|
|
fx.aim_wait = 20;
|
|
fx.ammo_check = this.CheckVehicleAmmo;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
this->LogAI_Info(fx, "Vehicle ammo is not ok. Weapon is %v, vehicle is %v", fx.weapon, fx.vehicle);
|
|
fx.weapon = nil;
|
|
}
|
|
}
|
|
// Find a weapon. Depends on attack mode
|
|
if (Call(fx.attack_mode.FindWeapon, fx))
|
|
{
|
|
// Select unless it's e.g. a vehicle or a spell
|
|
SelectItem(fx, fx.weapon);
|
|
return true;
|
|
}
|
|
// No weapon.
|
|
return false;
|
|
}
|