More modular AI

The AI was split up into a basic controller object that contains just the AI control effect and basic logic. The other components should work individually, and the final implementation of the AI can import these components individually.

Fixed AI catapults, so that they now fire even if the clonk has no items in the inventory.

Merged from gitMarky/openclonk, branch ai_backport.
install-platforms
Mark 2017-06-24 12:00:22 +02:00
commit 04b85e0719
51 changed files with 1180 additions and 708 deletions

View File

@ -17,7 +17,7 @@ public func FindTarget(effect fx)
{
var target = _inherited(fx, ...);
// Focus on defense target if normal target is too far away.
if (!target || ObjectDistance(target, fx.Target) > fx.control.AltTargetDistance)
if (!target || ObjectDistance(target, fx.Target) > fx->GetControl().AltTargetDistance)
{
if (fx.is_siege)
target = GetRandomSiegeTarget(fx.Target);

View File

@ -14,7 +14,7 @@ local AttackMessageRate = 80; // Likelihood of displaying an attack message in p
public func ExecuteAppearance(effect fx)
{
// Do a random attack message.
if (!Random(5) && Random(100) >= 100 - fx.control.AttackMessageRate)
if (!Random(5) && Random(100) >= 100 - fx->GetControl().AttackMessageRate)
this->ExecuteAttackMessage(fx);
return true;
}
@ -32,7 +32,7 @@ public func ExecuteAttackMessage(effect fx)
var ai = clonk->~GetAI();
if (!ai)
continue;
if (ai.last_message != nil && FrameCounter() - ai.last_message < fx.control.AttackMessageWaitTime)
if (ai.last_message != nil && FrameCounter() - ai.last_message < fx->GetControl().AttackMessageWaitTime)
continue;
group_cnt++;
}

View File

@ -0,0 +1,5 @@
[DefCore]
id=AI_AttackEnemy
Version=8,0
Category=C4D_StaticBack
HideInCreator=true

View File

@ -0,0 +1,159 @@
/**
Basic AI component for attacking enemies.
@author Sven2, Maikel
*/
// Enemy spawn definition depends on this
local DefinitionPriority = 50;
// AI Settings.
local MaxAggroDistance = 200; // Lose sight to target if it is this far away (unless we're ranged - then always guard the range rect).
local GuardRangeX = 300; // Search targets this far away in either direction (searching in rectangle).
local GuardRangeY = 150; // Search targets this far away in either direction (searching in rectangle).
/*-- Public interface --*/
public func SetAllyAlertRange(object clonk, int new_range)
{
AssertDefinitionContext(Format("SetAllyAlertRange(%v, %d)", clonk, new_range));
var fx_ai = this->GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.ally_alert_range = new_range;
return true;
}
// Set callback function name to be called in game script when this AI is first encountered
// Callback function first parameter is (this) AI clonk, second parameter is player clonk.
// The callback should return true to be cleared and not called again. Otherwise, it will be called every time a new target is found.
public func SetEncounterCB(object clonk, string cb_fn)
{
AssertDefinitionContext(Format("SetEncounterCB(%v, %s)", clonk, cb_fn));
var fx_ai = this->GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.encounter_cb = cb_fn;
return true;
}
// Enable/disable auto-searching of targets.
public func SetAutoSearchTarget(object clonk, bool new_auto_search_target)
{
AssertDefinitionContext(Format("SetAutoSearchTarget(%v, %v)", clonk, new_auto_search_target));
var fx_ai = this->GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.auto_search_target = new_auto_search_target;
return true;
}
// Set the guard range to the provided rectangle.
public func SetGuardRange(object clonk, int x, int y, int wdt, int hgt)
{
AssertDefinitionContext(Format("SetGuardRange(%v, %d, %d, %d, %d)", clonk, x, y, wdt, hgt));
var fx_ai = this->GetAI(clonk);
if (!fx_ai)
return false;
// Clip to landscape size.
if (x < 0)
{
wdt += x;
x = 0;
}
if (y < 0)
{
hgt += y;
y = 0;
}
wdt = Min(wdt, LandscapeWidth() - x);
hgt = Min(hgt, LandscapeHeight() - y);
fx_ai.guard_range = {x = x, y = y, wdt = wdt, hgt = hgt};
return true;
}
// Set the maximum distance the enemy will follow an attacking clonk.
public func SetMaxAggroDistance(object clonk, int max_dist)
{
AssertDefinitionContext(Format("SetMaxAggroDistance(%v, %d)", clonk, max_dist));
var fx_ai = this->GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.max_aggro_distance = max_dist;
return true;
}
/*-- Callbacks --*/
// Callback from the effect Construction()-call
public func OnAddAI(proplist fx_ai)
{
_inherited(fx_ai);
// Add AI default settings.
SetGuardRange(fx_ai.Target, fx_ai.home_x - fx_ai.GuardRangeX, fx_ai.home_y - fx_ai.GuardRangeY, fx_ai.GuardRangeX * 2, fx_ai.GuardRangeY * 2);
SetMaxAggroDistance(fx_ai.Target, fx_ai.MaxAggroDistance);
SetAutoSearchTarget(fx_ai.Target, true);
// Store whether the enemy is controlled by a commander.
fx_ai.commander = fx_ai.Target.commander;
}
// Callback from the Definition()-call
public func OnDefineAI(proplist def)
{
_inherited(def);
// Set the additional editor properties
var additional_props =
{
ignore_allies = { Name = "$IgnoreAllies$", Type = "bool" },
guard_range = { Name = "$GuardRange$", Type = "rect", Storage = "proplist", Color = 0xff00, Relative = false },
max_aggro_distance = { Name = "$MaxAggroDistance$", Type = "circle", Color = 0x808080 },
auto_search_target = { Name = "$AutoSearchTarget$", EditorHelp = "$AutoSearchTargetHelp$", Type = "bool" },
};
AddProperties(def->GetControlEffect().EditorProps, additional_props);
}
// Callback from the effect SaveScen()-call
public func OnSaveScenarioAI(proplist fx_ai, proplist props)
{
_inherited(fx_ai, props);
if (fx_ai.ally_alert_range)
props->AddCall(SAVESCEN_ID_AI, fx_ai->GetControl(), "SetAllyAlertRange", fx_ai.Target, fx_ai.ally_alert_range);
props->AddCall(SAVESCEN_ID_AI, fx_ai->GetControl(), "SetGuardRange", fx_ai.Target, fx_ai.guard_range.x, fx_ai.guard_range.y, fx_ai.guard_range.wdt, fx_ai.guard_range.hgt);
if (fx_ai.max_aggro_distance != fx_ai->GetControl().MaxAggroDistance)
props->AddCall(SAVESCEN_ID_AI, fx_ai->GetControl(), "SetMaxAggroDistance", fx_ai.Target, fx_ai.max_aggro_distance);
if (!fx_ai.auto_search_target)
props->AddCall(SAVESCEN_ID_AI, fx_ai->GetControl(), "SetAutoSearchTarget", fx_ai.Target, false);
if (fx_ai.encounter_cb)
props->AddCall(SAVESCEN_ID_AI, fx_ai->GetControl(), "SetEncounterCB", fx_ai.Target, Format("%v", fx_ai.encounter_cb));
}
/*-- Editor Properties --*/
// Gets an evaluator for the editor: Clonks
public func UserAction_EnemyEvaluator()
{
var enemy_evaluator = UserAction->GetObjectEvaluator("IsClonk", "$Enemy$", "$EnemyHelp$");
enemy_evaluator.Priority = 100;
return enemy_evaluator;
}
// Gets an evaluator for the editor: Attack target
public func UserAction_AttackTargetEvaluator()
{
return UserAction->GetObjectEvaluator("IsClonk", "$AttackTarget$", "$AttackTargetHelp$");
}

View File

@ -0,0 +1,9 @@
IgnoreAllies=Mitspieler ignorieren
GuardRange=Bewachter Bereich
MaxAggroDistance=Angriffsradius
AutoSearchTarget=Ziel automatisch suchen
AutoSearchTargetHelp=Wenn wahr, sucht die KI automatisch Gegner. Ansonsten muss der Gegner per Sequenzbefehl gegeben werden.
AttackTarget=Angriffsziel
AttackTargetHelp=Wenn angegeben, greift der KI-Clonk direkt diesen Gegner an.
Enemy=Gegner
EnemyHelp=Welcher Gegner angegriffen werden soll. Wenn nicht angegeben, greift der Gegner den nächsten Clonk an.

View File

@ -0,0 +1,9 @@
IgnoreAllies=Ignore allies
GuardRange=Guarded area
MaxAggroDistance=Attack radius
AutoSearchTarget=Auto-search target
AutoSearchTargetHelp=Looks for enemies automatically if true. If false, enemies must be provided via a script sequence.
AttackTarget=Attack target
AttackTargetHelp=If specified, this enemy is attacked by the AI clonk. If not specified, the nearest enemy will be attacked.
Enemy=Enemy
EnemyHelp=Which enemy to attack. If unspecified, attacks the nearest clonk.

View File

@ -5,11 +5,57 @@
@author Sven2
*/
/*-- Callbacks --*/
// Callback from the effect Construction()-call
public func OnAddAI(proplist fx_ai)
{
_inherited(fx_ai);
// Add AI default settings.
SetAttackMode(fx_ai.Target, "Default"); // also binds inventory
}
// Callback from the effect SaveScen()-call
public func OnSaveScenarioAI(proplist fx_ai, proplist props)
{
_inherited(fx_ai, props);
if (fx_ai.attack_mode.Identifier != "Default")
props->AddCall(SAVESCEN_ID_AI, fx_ai->GetControl(), "SetAttackMode", fx_ai.Target, Format("%v", fx_ai.attack_mode.Identifier));
}
/*-- Editor Properties --*/
// Callback from the Definition()-call
public func OnDefineAI(proplist def)
{
_inherited(def);
def->GetControlEffect().SetAttackMode = this.EditorDelegate_SetAttackMode;
// Set the other options
def->DefinitionAttackModes(def);
}
public func EditorDelegate_SetAttackMode(proplist attack_mode)
{
// Called by editor delegate when attack mode is changed.
// For now, attack mode parameter delegates are not supported. Just set by name.
return this->GetControl()->SetAttackMode(this.Target, attack_mode.Identifier);
}
/*-- Internals --*/
// Set attack mode / spell to control attack behaviour
public func SetAttackMode(object clonk, string attack_mode_identifier)
{
if (GetType(this) != C4V_Def)
Log("WARNING: SetAttackMode(%v, %s) not called from definition context but from %v", clonk, attack_mode_identifier, this);
AssertDefinitionContext(Format("SetAttackMode(%v, %s)", clonk, attack_mode_identifier));
var fx_ai = this->GetAI(clonk);
if (!fx_ai)
return false;
@ -26,8 +72,6 @@ public func SetAttackMode(object clonk, string attack_mode_identifier)
return true;
}
local AttackModes = {}; // empty pre-init to force proplist ownership in base AI
// Attack mode that just creates a weapon and uses the default attack procedures
local SingleWeaponAttackMode = {
Construction = func(effect fx)
@ -57,7 +101,7 @@ local SingleWeaponAttackMode = {
var respawning_object = ammo ?? weapon;
respawning_object->~SetStackCount(1); // Ensure departure is called on every object
respawning_object.WeaponRespawn_Departure = respawning_object.Departure;
respawning_object.Departure = AI.Departure_WeaponRespawn;
respawning_object.Departure = AI_AttackModes.Departure_WeaponRespawn;
fx.has_ammo_respawn = true;
}
}
@ -102,11 +146,11 @@ local SingleWeaponAttackMode = {
private func InitAttackModes()
{
// First-time init of attack mode editor prop structures
// All attack modes structures point to the base AI
this.AttackModes = AI.AttackModes;
if (!AI.FxAI.EditorProps.attack_mode)
// The attack mode structures are defined in every AI that includes this library
if (!this.AttackModes) this.AttackModes = {};
if (!this->GetControlEffect().EditorProps.attack_mode)
{
AI.FxAI.EditorProps.attack_mode = {
this->GetControlEffect().EditorProps.attack_mode = {
Name="$AttackMode$",
EditorHelp="$AttackModeHelp$",
Type="enum",
@ -115,7 +159,6 @@ private func InitAttackModes()
Set="SetAttackMode"
};
}
this.FxAI.EditorProps.attack_mode = AI.FxAI.EditorProps.attack_mode;
}
public func RegisterAttackMode(string identifier, proplist am, proplist am_default_values)
@ -123,8 +166,8 @@ public func RegisterAttackMode(string identifier, proplist am, proplist am_defau
// Definition call during Definition()-initialization:
// Register a new attack mode selectable for the AI clonk
// Add to attack mode info structure
if (!AttackModes) this->InitAttackModes();
AttackModes[identifier] = am;
if (!this.AttackModes) this->InitAttackModes();
this.AttackModes[identifier] = am;
am.Identifier = identifier;
if (!am_default_values) am_default_values = { Identifier=identifier };
// Add to editor option for AI effect
@ -134,7 +177,7 @@ public func RegisterAttackMode(string identifier, proplist am, proplist am_defau
Value = am_default_values
};
if (!am_option.EditorHelp && am.GetEditorHelp) am_option.EditorHelp = am->GetEditorHelp();
var editor_opts = this.FxAI.EditorProps.attack_mode.Options;
var editor_opts = this->GetControlEffect().EditorProps.attack_mode.Options;
editor_opts[GetLength(editor_opts)] = am_option;
}
@ -142,10 +185,8 @@ private func DefinitionAttackModes(proplist def)
{
// Make sure attack mode structures are initialized
this->InitAttackModes();
// Registration only once for base AI
if (this != AI) return;
// Register presets for all the default weapons usable by the AI
def->RegisterAttackMode("Default", { Name = "$Default$", EditorHelp = "$DefaultHelp$", FindWeapon = AI.FindInventoryWeapon });
def->RegisterAttackMode("Default", { Name = "$Default$", EditorHelp = "$DefaultHelp$", FindWeapon = this.FindInventoryWeapon });
def->RegisterAttackMode("Sword", new SingleWeaponAttackMode { Weapon = Sword, Strategy = this.ExecuteMelee });
def->RegisterAttackMode("Axe", new SingleWeaponAttackMode { Weapon = Axe, Strategy = this.ExecuteMelee });
def->RegisterAttackMode("Club", new SingleWeaponAttackMode { Weapon = Club, Strategy = this.ExecuteClub });
@ -198,3 +239,112 @@ func DoWeaponRespawn(id_weapon)
return re_weapon;
}
}
/*-- Finding weapons --*/
public func FindInventoryWeapon(effect fx)
{
// Find weapon in inventory, mark it as equipped and set according strategy, etc.
fx.ammo_check = nil;
fx.ranged = false;
if (FindInventoryWeaponGrenadeLauncher(fx)) return true;
if (FindInventoryWeaponBlunderbuss(fx)) return true;
if (FindInventoryWeaponBow(fx)) return true;
if (FindInventoryWeaponJavelin(fx)) return true;
// Throwing weapons.
if ((fx.weapon = fx.Target->FindContents(Firestone)) || (fx.weapon = fx.Target->FindContents(Rock)) || (fx.weapon = fx.Target->FindContents(Lantern)))
{
fx.can_attack_structures = fx.weapon->~HasExplosionOnImpact();
fx.strategy = this.ExecuteThrow;
return true;
}
// Melee weapons.
if ((fx.weapon = fx.Target->FindContents(Sword)) || (fx.weapon = fx.Target->FindContents(Axe))) // Sword attacks aren't 100% correct for Axe, but work well enough
{
fx.strategy = this.ExecuteMelee;
return true;
}
if ((fx.weapon = fx.Target->FindContents(PowderKeg)))
{
fx.strategy = this.ExecuteBomber;
return true;
}
if ((fx.weapon = fx.Target->FindContents(Club)))
{
fx.strategy = this.ExecuteClub;
return true;
}
// No weapon.
return false;
}
private func FindInventoryWeaponGrenadeLauncher(effect fx)
{
if (fx.weapon = fx.Target->FindContents(GrenadeLauncher))
{
if (this->HasBombs(fx, fx.weapon))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = 75;
fx.ammo_check = this.HasBombs;
fx.ranged = true;
fx.can_attack_structures = true;
return true;
}
else
fx.weapon = nil;
}
}
private func FindInventoryWeaponBlunderbuss(effect fx)
{
if (fx.weapon = fx.Target->FindContents(Blunderbuss))
{
if (this->HasAmmo(fx, fx.weapon))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = 200;
fx.ammo_check = this.HasAmmo;
fx.ranged = true;
fx.ranged_direct = true;
return true;
}
else
fx.weapon = nil;
}
}
private func FindInventoryWeaponBow(effect fx)
{
if (fx.weapon = fx.Target->FindContents(Bow))
{
if (this->HasArrows(fx, fx.weapon))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = 100;
fx.ammo_check = this.HasArrows;
fx.ranged = true;
var arrow = fx.weapon->Contents(0) ?? FindObject(Find_Container(fx.Target), Find_Func("IsArrow"));
fx.can_attack_structures = arrow && arrow->~IsExplosive();
return true;
}
else
fx.weapon = nil;
}
}
private func FindInventoryWeaponJavelin(effect fx)
{
if (fx.weapon = fx.Target->FindContents(Javelin))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = fx.Target.ThrowSpeed * fx.weapon.shooting_strength / 100;
fx.ranged=true;
return true;
}
}

View File

@ -0,0 +1,5 @@
[DefCore]
id=AI_HelperClonk
Version=8,0
Category=C4D_StaticBack
HideInCreator=true

View File

@ -0,0 +1,48 @@
/**
AI helper functions: Clonk
Contains functions that are related to the clonk.
@author Sven2, Maikel
*/
// Selects an item the clonk is about to use.
public func SelectItem(effect fx, object item)
{
if (!item)
return;
if (item->Contained() != fx.Target)
return;
fx.Target->SetHandItemPos(0, fx.Target->GetItemPos(item));
}
public func CancelAiming(effect fx)
{
if (fx.aim_weapon)
{
fx.aim_weapon->~ControlUseCancel(fx.Target);
fx.aim_weapon = nil;
}
else
{
// Also cancel aiming done outside AI control.
fx.Target->~CancelAiming();
}
return true;
}
public func CheckHandsAction(effect fx)
{
// Can use hands?
if (fx.Target->~HasHandAction())
return true;
// Can't throw: Is it because e.g. we're scaling?
if (!fx.Target->HasActionProcedure())
{
this->ExecuteStand(fx);
return false;
}
// Probably hands busy. Just wait.
return false;
}

View File

@ -0,0 +1,29 @@
/**
AI Debugging
Functionality that helps to debug AI control.
@author Maikel
*/
// Callback from the Definition()-call
public func OnDefineAI(proplist def)
{
_inherited(def);
def->GetControlEffect().DebugLoggingOn = false; // Whether or not debug logging is turned on.
}
public func LogAI_Warning(effect fx, string message)
{
if (fx.DebugLoggingOn)
Log("[%d] AI WARNING (%v): %s", FrameCounter(), fx.Target, message);
return;
}
// Logs AI info
public func LogAI_Info(proplist fx, string message)
{
if (fx.DebugLoggingOn)
Log("[%d] AI INFO (%v): %s", FrameCounter(), fx.Target, message);
return;
}

View File

@ -0,0 +1,5 @@
[DefCore]
id=AI_HomePosition
Version=8,0
Category=C4D_StaticBack
HideInCreator=true

View File

@ -0,0 +1,114 @@
/**
Set the home position the Clonk returns to if he has no target.
@author Sven2, Maikel
*/
/*-- Public interface --*/
// Set the home position the Clonk returns to if he has no target.
public func SetHome(object clonk, int x, int y, int dir)
{
AssertDefinitionContext(Format("SetHome(%v, %d, %d, %d)", clonk, x, y, dir));
var fx_ai = this->GetAI(clonk);
if (!fx_ai)
return false;
// nil/nil defaults to current position.
x = x ?? clonk->GetX();
y = y ?? clonk->GetY();
dir = dir ?? clonk->GetDir();
fx_ai.home_x = x;
fx_ai.home_y = y;
fx_ai.home_dir = dir;
return true;
}
/*-- Callbacks --*/
// Callback from the effect Construction()-call
public func OnAddAI(proplist fx_ai)
{
_inherited(fx_ai);
// Add AI default settings.
SetHome(fx_ai.Target);
}
// Callback from the effect SaveScen()-call
public func OnSaveScenarioAI(proplist fx_ai, proplist props)
{
_inherited(fx_ai, props);
if (fx_ai.home_x != fx_ai.Target->GetX() || fx_ai.home_y != fx_ai.Target->GetY() || fx_ai.home_dir != fx_ai.Target->GetDir())
props->AddCall(SAVESCEN_ID_AI, fx_ai->GetControl(), "SetHome", fx_ai.Target, fx_ai.home_x, fx_ai.home_y, GetConstantNameByValueSafe(fx_ai.home_dir, "DIR_"));
}
/*-- Editor Properties --*/
// Callback from the Definition()-call
public func OnDefineAI(proplist def)
{
_inherited(def);
// Add AI user actions.
UserAction->AddEvaluator("Action", "Clonk", "$SetAINewHome$", "$SetAINewHomeHelp$", "ai_set_new_home", [def, def.EvalAct_SetNewHome],
{
Enemy = nil,
HomePosition = nil,
Status = {
Function = "bool_constant",
Value = true
}
},
{
Type = "proplist",
Display = "{{Enemy}} -> {{NewHome}}",
EditorProps = {
Enemy = this->~UserAction_EnemyEvaluator(),
NewHome = new UserAction.Evaluator.Position {
Name = "$NewHome$",
EditorHelp = "$NewHomeHelp$"
},
NewHomeDir = {
Type = "enum",
Name = "$NewHomeDir$",
EditorHelp = "$NewHomeDirHelp$",
Options = [
{ Name = "$Unchanged$" },
{ Name = "$Left$", Value = DIR_Left },
{ Name = "$Right$", Value = DIR_Right }
]
},
}
}
);
}
public func EvalAct_SetNewHome(proplist props, proplist context)
{
// User action: Set new home.
var enemy = UserAction->EvaluateValue("Object", props.Enemy, context);
var new_home = UserAction->EvaluatePosition(props.NewHome, context);
var new_home_dir = props.NewHomeDir;
if (!enemy)
return;
// Ensure enemy AI exists.
var fx = this->GetAI(enemy); // TODO: Streamline this
if (!fx)
{
fx = this->AddAI(enemy, this);
if (!fx || !enemy)
return;
// Create without attack command.
this->~SetAutoSearchTarget(enemy, false);
}
fx.command = this.ExecuteIdle;
fx.home_x = new_home[0];
fx.home_y = new_home[1];
if (GetType(new_home_dir))
fx.home_dir = new_home_dir;
}

View File

@ -0,0 +1,9 @@
SetAINewHome=KI Position setzen
SetAINewHomeHelp=Lässt einen KI-Gegner an eine neue Position laufen.
NewHome=Neue Position
NewHomeHelp=Wohin der Clonk laufen soll.
NewHomeDir=Richtung
NewHomeDirHelp=In welche Richtung der KI-Clonk schauen soll, wenn kein Gegner in der Nähe ist.
Unchanged=Ungeändert
Left=Links
Right=Rechts

View File

@ -0,0 +1,9 @@
SetAINewHome=AI set position
SetAINewHomeHelp=Lets an AI clonk walk to another position.
NewHome=New position
NewHomeHelp=Where to send the AI clonk.
NewHomeDir=Direction
NewHomeDirHelp=In which direction the AI clonk will look if no enemy is nearby.
Unchanged=Unchanged
Left=Left
Right=Right

View File

@ -0,0 +1,5 @@
[DefCore]
id=AI_Inventory
Version=8,0
Category=C4D_StaticBack
HideInCreator=true

View File

@ -0,0 +1,41 @@
/**
AI helper functions: Inventory
Contains functions that are related AI inventory.
@author Sven2, Maikel
*/
/*-- Public interface --*/
// Set the current inventory to be removed when the clonk dies. Only works if clonk has an AI.
public func BindInventory(object clonk)
{
AssertDefinitionContext(Format("BindInventory(%v)", clonk));
var fx_ai = this->GetAI(clonk);
if (!fx_ai)
return false;
var cnt = clonk->ContentsCount();
fx_ai.bound_weapons = CreateArray(cnt);
for (var i = 0; i < cnt; ++i)
fx_ai.bound_weapons[i] = clonk->Contents(i);
return true;
}
/*-- Callbacks --*/
// Callback from the effect Destruction()-call
public func OnRemoveAI(proplist fx_ai, int reason)
{
_inherited(fx_ai, reason);
// Remove weapons on death.
if (reason == FX_Call_RemoveDeath)
{
if (fx_ai.bound_weapons)
for (var obj in fx_ai.bound_weapons)
if (obj && obj->Contained() == fx_ai.Target)
obj->RemoveObject();
}
}

View File

@ -6,6 +6,67 @@
*/
/*-- Public interface --*/
// Set attack path
public func SetAttackPath(object clonk, array new_attack_path)
{
AssertDefinitionContext(Format("SetAttackPath(%v, %v)", clonk, new_attack_path));
var fx_ai = this->GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.attack_path = new_attack_path;
return true;
}
/*-- Callbacks --*/
// Callback from the effect SaveScen()-call
public func OnSaveScenarioAI(proplist fx_ai, proplist props)
{
_inherited(fx_ai, props);
if (fx_ai.attack_path)
props->AddCall(SAVESCEN_ID_AI, fx_ai->GetControl(), "SetAttackPath", fx_ai.Target, fx_ai.attack_path);
}
/*-- Editor Properties --*/
// Callback from the Definition()-call
public func OnDefineAI(proplist def)
{
_inherited(def);
def->GetControlEffect().SetAttackPath = this.EditorDelegate_SetAttackPath;
// Set the additional editor properties
var additional_props =
{
attack_path = { Name = "$AttackPath$", EditorHelp = "$AttackPathHelp$", Type = "enum", Set = "SetAttackPath", Options = [
{ Name="$None$" },
{ Name="$AttackPath$", Type=C4V_Array, Value = [{X = 0, Y = 0}], Delegate =
{ Name="$AttackPath$", EditorHelp="$AttackPathHelp$", Type="polyline", StartFromObject=true, DrawArrows=true, Color=0xdf0000, Relative=false }
}
] },
};
AddProperties(def->GetControlEffect().EditorProps, additional_props);
}
func EditorDelegate_SetAttackPath(array attack_path)
{
// Called by editor delegate when attack mode is changed.
// For now, attack mode parameter delegates are not supported. Just set by name.
return this->GetControl()->SetAttackPath(this.Target, attack_path);
}
/*-- Internals --*/
// Tries to make sure the clonk stands: i.e. scales down or let's go when hangling.
public func ExecuteStand(effect fx)
{
@ -51,7 +112,7 @@ public func ExecuteStand(effect fx)
}
else
{
this->LogAI(fx, Format("ExecuteStand has no idea what to do for action %v and procedure %v.", fx.Target->GetAction(), fx.Target->GetProcedure()));
this->~LogAI_Warning(fx, Format("ExecuteStand has no idea what to do for action %v and procedure %v.", fx.Target->GetAction(), fx.Target->GetProcedure()));
// Hm. What could it be? Let's just hope it resolves itself somehow...
fx.Target->SetComDir(COMD_Stop);
}
@ -188,4 +249,4 @@ public func ExecuteLookAtTarget(effect fx)
else
fx.Target->SetDir(DIR_Left);
return true;
}
}

View File

@ -0,0 +1,3 @@
AttackPath=Angriffspfad
AttackPathHelp=Pfad, entlang dessen sich der KI-Gegner berwegt. Befindet sich ein Gebaeude auf einem Eckpunkt des Pfades, greift der Clonk das Gebaeude an.
None=Keiner

View File

@ -0,0 +1,3 @@
AttackPath=Attack path
AttackPathHelp=Path along which the AI clonk moves. If a vertex of the attack path is placed on a structure (e.g. a gate), the clonk will attack the structure.
None=None

View File

@ -85,7 +85,7 @@ public func ExecuteProtection(effect fx)
fx.target = nil; //this->FindEmergencyTarget(fx);
if (fx.target)
fx.alert = fx.time;
else if (fx.time - fx.alert > fx.control.AlertTime)
else if (fx.time - fx.alert > fx->GetControl().AlertTime)
fx.alert = nil;
// If not evading the AI may try to heal.
if (ExecuteHealing(fx))
@ -126,7 +126,7 @@ public func ExecuteHealing(effect fx)
var hp_needed = fx.Target->GetMaxEnergy() - hp;
// Only heal when alert if health drops below the healing threshold.
// If not alert also heal if more than 40 hitpoints of health are lost.
if (hp >= fx.control.HealingHitPointsThreshold && (fx.alert || hp_needed <= 40))
if (hp >= fx->GetControl().HealingHitPointsThreshold && (fx.alert || hp_needed <= 40))
return false;
// Don't heal if already healing. One can speed up healing by healing multiple times, but we don't.
if (GetEffect("HealingOverTime", fx.Target))

View File

@ -53,6 +53,10 @@ public func CheckTargetInGuardRange(effect fx)
public func HasWeaponForTarget(effect fx, object target)
{
target = target ?? fx.target;
// Already has a weapon, or a vehicle
if (fx.weapon && this->IsWeaponForTarget(fx, fx.weapon, target))
return true;
// Look for weapons in the inventory
for (var weapon in FindObjects(Find_Container(fx.Target)))
if (this->IsWeaponForTarget(fx, weapon, target))
return true;
@ -100,4 +104,4 @@ public func IsVehicleForTarget(effect fx, object vehicle, object target)
if (vehicle->GetID() == Catapult)
return true;
return false;
}
}

View File

@ -12,6 +12,34 @@ local AirshipBoardDistance = 100; // How near must an airship be to the target t
local AirshipLostDistance = 50; // How far the pilot must be away from an airship for it to find a new pilot.
local AirshipOccludedTargetMaxDistance = 250; // IF a target is further than this and occluded, search for a new target
/*-- Public interface --*/
// Set controlled vehicle
public func SetVehicle(object clonk, object new_vehicle)
{
AssertDefinitionContext(Format("SetVehicle(%v, %v)", clonk, new_vehicle));
var fx_ai = this->GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.vehicle = new_vehicle;
return true;
}
/*-- Callbacks --*/
// Callback from the effect Construction()-call
public func OnAddAI(proplist fx_ai)
{
_inherited(fx_ai);
// Store the vehicle the AI is using.
if (fx_ai.Target->GetProcedure() == "PUSH")
fx_ai.vehicle = fx_ai.Target->GetActionTarget();
}
/*-- General Vehicle --*/
private func ExecuteVehicle(effect fx)
@ -130,7 +158,7 @@ public func ExecuteAirship(effect fx)
if (fx.Target->GetProcedure() != "PUSH" || fx.Target->GetActionTarget() != fx.vehicle)
{
// Try to find a new pilot if the current pilot lost the airship.
if (fx.Target->ObjectDistance(fx.vehicle) > fx.control.AirshipLostDistance)
if (fx.Target->ObjectDistance(fx.vehicle) > fx->GetControl().AirshipLostDistance)
{
this->PromoteNewAirshipCaptain(fx);
fx.strategy = nil;
@ -161,8 +189,8 @@ public func ExecuteAirship(effect fx)
tx = fx.home_x;
ty = fx.home_y;
}
if (Distance(fx.vehicle->GetX(), fx.vehicle->GetY(), tx, ty) < fx.control.AirshipBoardDistance
&& Inside(fx.vehicle->GetY() - ty, -fx.control.AirshipBoardDistance / 2, 0))
if (Distance(fx.vehicle->GetX(), fx.vehicle->GetY(), tx, ty) < fx->GetControl().AirshipBoardDistance
&& Inside(fx.vehicle->GetY() - ty, -fx->GetControl().AirshipBoardDistance / 2, 0))
{
// Unboard the crew and let go of airship.
for (var clonk in this->GetCommanderCrew(fx))

View File

@ -0,0 +1,5 @@
[DefCore]
id=AI_Controller
Version=8,0
Category=C4D_None | C4D_MouseIgnore
HideInCreator=true

View File

@ -0,0 +1,394 @@
/**
AI
Controls bots.
@author Marky
@credits Original AI structure/design by Sven2, Maikel
*/
static const SAVESCEN_ID_AI = "AI";
/*-- Engine callbacks --*/
public func Definition(proplist type)
{
this->OnDefineAI(type);
}
/*-- Public interface --*/
// Change whether target Clonk has an AI (used by editor).
public func SetAI(object clonk, id type)
{
if (type)
{
return type->AddAI(clonk, type); // call from the definition there
}
else
{
return RemoveAI(clonk);
}
}
// Add AI execution timer to target Clonk.
public func AddAI(object clonk, id type)
{
AssertDefinitionContext(Format("AddAI(%v, %v)", clonk, type));
AssertNotNil(clonk);
var fx_ai = GetAI(clonk) ?? clonk->CreateEffect(FxAI, 1, 1, type ?? this);
return fx_ai;
}
// Remove the AI execution timer
public func RemoveAI(object clonk)
{
AssertDefinitionContext(Format("RemoveAI(%v)", clonk));
var fx_ai = GetAI(clonk);
if (fx_ai)
{
fx_ai->Remove();
}
}
public func GetAI(object clonk)
{
AssertDefinitionContext(Format("GetAI(%v)", clonk));
if (clonk)
{
return clonk->~GetAI();
}
else
{
return nil;
}
}
public func GetControlEffect()
{
return this.FxAI;
}
// Set active state: Enables/Disables timer
public func SetActive(object clonk, bool active)
{
AssertDefinitionContext(Format("SetActive(%v, %v)", clonk, active));
var fx_ai = GetAI(clonk);
if (fx_ai)
{
if (!active)
{
// Inactive: Stop any activity.
clonk->SetCommand("None");
clonk->SetComDir(COMD_Stop);
}
return fx_ai->SetActive(active);
}
else
{
return false;
}
}
/*-- AI Effect --*/
// The AI effect stores a lot of information about the AI clonk. This includes its state, enemy target, alertness etc.
// Each of the functions which are called in the AI definition pass the effect and can then access these variables.
// The most important variables are:
// fx.Target - The AI clonk.
// fx.target - The current target the AI clonk will attack.
// fx.alert - Whether the AI clonk is alert and aware of enemies around it.
// fx.weapon - Currently selected weapon by the AI clonk.
// fx.ammo_check - Function that is called to check ammunition for fx.weapon.
// fx.commander - Is commanded by another AI clonk.
// fx.control - Definition controlling this AI, all alternative AIs should include the basic AI.
local FxAI = new Effect
{
Construction = func(id control_def)
{
// Execute AI every 3 frames.
this.Interval = control_def->~GetTimerInterval() ?? 1;
// Store the definition that controls this AI.
this.control = control_def;
// Give the AI a helper function to get the AI control effect.
this.Target.ai = this;
this.Target.GetAI = this.GetAI;
// Callback to the controller
this.control->~OnAddAI(this);
return FX_OK;
},
GetAI = func()
{
return this.ai;
},
GetControl = func()
{
return this.control;
},
Timer = func(int time)
{
// Execute the AI in the clonk.
this.control->Execute(this, time);
return FX_OK;
},
Destruction = func(int reason)
{
// Callback to the controller
this.control->~OnRemoveAI(this, reason);
// Remove AI reference.
if (Target && Target.ai == this)
Target.ai = nil;
return FX_OK;
},
Damage = func(int dmg, int cause)
{
// AI takes damage: Make sure we're alert so evasion and healing scripts are executed!
// It might also be feasible to execute encounter callbacks here (in case an AI is shot from a position it cannot see).
// However, the attacking clonk is not known and the callback might be triggered e.g. by an unfortunate meteorite or lightning blast.
// So let's just keep it at alert state for now.
if (dmg < 0)
this.alert = this.time;
this.control->~OnDamageAI(this, dmg, cause);
return dmg;
},
SetActive = func(bool active)
{
this.Interval = (this.control->~GetTimerInterval() ?? 1) * active;
if (active)
{
this.control->~OnActivateAI(this);
}
else
{
this.control->~OnDeactivateAI(this);
}
},
GetActive = func()
{
return this.Interval != 0;
},
EditorProps = {
active = { Name = "$Active$", EditorHelp = "$ActiveHelp$", Type = "bool", Priority = 50, AsyncGet = "GetActive", Set = "SetActive" },
},
// Save this effect and the AI for scenarios.
SaveScen = func(proplist props)
{
if (this.Target)
{
props->AddCall(SAVESCEN_ID_AI, this.control, "AddAI", this.Target);
if (!this.Interval)
{
props->AddCall(SAVESCEN_ID_AI, this.control, "SetActive", this.Target, false);
}
this.control->~OnSaveScenarioAI(this, props);
return true;
}
else
{
return false;
}
}
};
/*-- AI Execution --*/
public func Execute(effect fx, int time) // TODO: Adjust
{
return this->Call(fx.strategy, fx);
}
/*-- Editor Properties --*/
// Adds an AI to the selection list in the editor
public func AddEditorProp_AISelection(proplist type, id ai_type)
{
InitEditorProp_AISelection(type);
PushBack(type.EditorProps.AI_Controller.Options, EditorProp_AIType(ai_type));
}
// Initializes the selection property
private func InitEditorProp_AISelection(proplist type)
{
// ensure that the poperties exist
if (!type.EditorProps)
{
type.EditorProps = {};
}
// ensure that the list exists
if (!type.EditorProps.AI_Controller)
{
type.EditorProps.AI_Controller =
{
Type = "enum",
Name = "$ChooseAI$",
Options = [],
Set = Format("%i->SetAI", AI_Controller), // this is the AI_Controller on purpose
SetGlobal = true
};
}
// add default options
if (GetLength(type.EditorProps.AI_Controller.Options) == 0)
{
PushBack(type.EditorProps.AI_Controller.Options, EditorProp_AIType(nil));
}
}
// Gets an AI type entry for the selection list
private func EditorProp_AIType(id type)
{
if (type)
{
var option_ai = {
Name = type.Name ?? type->GetName(),
EditorHelp = type.EditorHelp,
Value = type
};
if (!option_ai.EditorHelp && type.GetEditorHelp) option_ai.EditorHelp = type->GetEditorHelp();
return option_ai;
}
else
{
return {
Name = "$NoAI$",
EditorHelp = "$NoAIEditorHelp$",
Value = nil,
};
}
}
/*-- Properties --*/
local Plane = 300;
/*-- Callbacks --*/
// Callback from the effect Construction()-call
public func OnAddAI(proplist fx_ai)
{
// called by the effect Construction()
_inherited(fx_ai);
}
// Callback from the effect Destruction()-call
public func OnRemoveAI(proplist fx_ai, int reason)
{
// called by the effect Destruction()
_inherited(fx_ai, reason);
}
// Callback when the AI is activated by a trigger
public func OnActivateAI(proplist fx_ai)
{
_inherited(fx_ai);
}
// Callback when the AI is deactivated by a trigger
public func OnDectivateAI(proplist fx_ai)
{
_inherited(fx_ai);
}
// Callback when the AI is damaged
public func OnDamageAI(proplist fx_ai, int damage, int cause)
{
_inherited(fx_ai, damage, cause);
}
// Callback from the effect SaveScen()-call
public func OnSaveScenarioAI(proplist fx_ai, proplist props)
{
// called by the effect SaveScen()
_inherited(fx_ai, props);
}
// Callback from the Definition()-call
public func OnDefineAI(proplist def)
{
_inherited(def);
// Add AI user actions.
UserAction->AddEvaluator("Action", "Clonk", "$SetAIActivated$", "$SetAIActivatedHelp$", "ai_set_activated", [def, def.EvalAct_SetActive],
{
Enemy = nil,
AttackTarget = {
Function= "triggering_clonk"
},
Status = {
Function = "bool_constant",
Value = true
}
},
{
Type = "proplist",
Display = "{{Enemy}}: {{Status}} ({{AttackTarget}})",
EditorProps = {
Enemy = this->~UserAction_EnemyEvaluator(),
AttackTarget = this->~UserAction_AttackTargetEvaluator(),
Status = new UserAction.Evaluator.Boolean { Name = "$Status$" }
}
}
);
}
/*-- Editor Properties --*/
public func EvalAct_SetActive(proplist props, proplist context)
{
// User action: Activate enemy AI.
var enemy = UserAction->EvaluateValue("Object", props.Enemy, context);
var attack_target = UserAction->EvaluateValue("Object", props.AttackTarget, context);
var status = UserAction->EvaluateValue("Boolean", props.Status, context);
if (!enemy)
return;
// Ensure enemy AI exists
var fx = GetAI(enemy);
if (!fx)
{
// Deactivated? Then we don't need an AI effect.
if (!status)
return;
fx = AddAI(enemy);
if (!fx || !enemy)
return;
}
// Set activation.
fx->SetActive(status);
// Set specific target if desired.
if (attack_target)
fx.target = attack_target;
}

View File

@ -0,0 +1,9 @@
ChooseAI=KI-Auswahl
NoAI=Ohne KI
NoAIEditorHelp=Entfernt die KI von diesem Objekt.
Active=Aktiv
ActiveHelp=Ob die KI aktiv den Clonk momentan steuert. Eine inaktive KI kann über die Aktion 'KI aktivieren' zum Beispiel in einer Sequenz wieder aktiviert werden.
SetAIActivated=KI aktivieren
SetAIActivatedHelp=Aktiviert die Gegner-KI für ein Objekt. Wenn keine KI aktiviert ist, wird sie für das Objekt erstellt.
Status=Aktiviert
StatusHelp=Ob die KI aktiviert oder deaktiviert wird.

View File

@ -0,0 +1,9 @@
ChooseAI=AI selection
NoAI=No AI
NoAIEditorHelp=Removes the AI from this object.
Active=Active
ActiveHelp=Whether the AI controls the clonk currently. An inactive KI can be reactivated via the 'AI activate', for example in a sequence.
SetAIActivated=AI activate
SetAIActivatedHelp=Activates an enemy AI for a clonk. AI will be created if necessary.
Status=Active
StatusHelp=Whether to activate or deactivate the AI.

View File

@ -1,18 +0,0 @@
/**
AI Debugging
Functionality that helps to debug AI control.
@author Maikel
*/
// AI Settings.
local DebugLoggingOn = false; // Whether or not debug logging is turned on.
public func LogAI(effect fx, string message)
{
if (fx.control.DebugLoggingOn)
Log("[%d]AI WARNING (%v): %s", FrameCounter(), fx.Target, message);
return;
}

View File

@ -4,11 +4,15 @@
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
@author Sven2, Maikel, Marky
*/
// Include the basic functionality
#include AI_Controller
// Include the different parts of the AI.
#include AI_Appearance
#include AI_Debugging
@ -20,345 +24,29 @@
#include AI_TargetFinding
#include AI_Vehicles
#include AI_AttackModes
#include AI_HelperClonk
#include AI_HomePosition
#include AI_AttackEnemy
#include AI_Inventory
// Enemy spawn definition depends on this
local DefinitionPriority = 50;
/*-- Callbacks --*/
// AI Settings.
local MaxAggroDistance = 200; // Lose sight to target if it is this far away (unless we're ranged - then always guard the range rect).
local GuardRangeX = 300; // Search targets this far away in either direction (searching in rectangle).
local GuardRangeY = 150; // Search targets this far away in either direction (searching in rectangle).
// Timer interval for the effect
public func GetTimerInterval() { return 3; }
/*-- Public interface --*/
/*-- Editor Properties --*/
// Change whether target Clonk has an AI (used by editor).
public func SetAI(object clonk, bool has_ai)
// Callback from the Definition()-call
public func OnDefineAI(proplist def)
{
var ai = GetAI(clonk);
if (has_ai)
{
// Only add if it doesn't have the effect yet.
if (!ai)
ai = AddAI(clonk);
return ai;
}
else
{
if (ai)
ai->Remove();
}
}
// Add AI execution timer to target Clonk.
public func AddAI(object clonk)
{
if (GetType(this) != C4V_Def)
Log("WARNING: AddAI(%v) not called from definition context but from %v", clonk, this);
var fx_ai = GetAI(clonk);
if (!fx_ai)
fx_ai = clonk->CreateEffect(FxAI, 1, 3, this);
if (!fx_ai)
return;
// Add AI default settings.
SetAttackMode(clonk, "Default"); // also binds inventory
SetHome(clonk);
SetGuardRange(clonk, fx_ai.home_x - this.GuardRangeX, fx_ai.home_y - this.GuardRangeY, this.GuardRangeX * 2, this.GuardRangeY * 2);
SetMaxAggroDistance(clonk, this.MaxAggroDistance);
SetAutoSearchTarget(clonk, true);
return fx_ai;
}
public func GetAI(object clonk)
{
if (GetType(this) != C4V_Def)
Log("WARNING: GetAI(%v) not called from definition context but from %v", clonk, this);
if (!clonk)
return nil;
return clonk->~GetAI();
}
// Set the current inventory to be removed when the clonk dies. Only works if clonk has an AI.
public func BindInventory(object clonk)
{
if (GetType(this) != C4V_Def)
Log("WARNING: BindInventory(%v) not called from definition context but from %v", clonk, this);
var fx_ai = GetAI(clonk);
if (!fx_ai)
return false;
var cnt = clonk->ContentsCount();
fx_ai.bound_weapons = CreateArray(cnt);
for (var i = 0; i < cnt; ++i)
fx_ai.bound_weapons[i] = clonk->Contents(i);
return true;
}
// Set the home position the Clonk returns to if he has no target.
public func SetHome(object clonk, int x, int y, int dir)
{
if (GetType(this) != C4V_Def)
Log("WARNING: SetHome(%v, %d, %d, %d) not called from definition context but from %v", clonk, x, y, dir, this);
var fx_ai = GetAI(clonk);
if (!fx_ai)
return false;
// nil/nil defaults to current position.
if (!GetType(x))
x = clonk->GetX();
if (!GetType(y))
y = clonk->GetY();
if (!GetType(dir))
dir = clonk->GetDir();
fx_ai.home_x = x;
fx_ai.home_y = y;
fx_ai.home_dir = dir;
return true;
}
// Set active state: Enables/Disables timer
public func SetActive(object clonk, bool active)
{
if (GetType(this) != C4V_Def)
Log("WARNING: SetActive(%v, %v) not called from definition context but from %v", clonk, active, this);
var fx_ai = GetAI(clonk);
if (!fx_ai)
return false;
if (!active)
{
// Inactive: Stop any activity.
clonk->SetCommand("None");
clonk->SetComDir(COMD_Stop);
}
return fx_ai->SetActive(active);
}
// Enable/disable auto-searching of targets.
public func SetAutoSearchTarget(object clonk, bool new_auto_search_target)
{
if (GetType(this) != C4V_Def)
Log("WARNING: SetAutoSearchTarget(%v, %v) not called from definition context but from %v", clonk, new_auto_search_target, this);
var fx_ai = GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.auto_search_target = new_auto_search_target;
return true;
}
// Set the guard range to the provided rectangle.
public func SetGuardRange(object clonk, int x, int y, int wdt, int hgt)
{
if (GetType(this) != C4V_Def)
Log("WARNING: SetGuardRange(%v, %d, %d, %d, %d) not called from definition context but from %v", clonk, x, y, wdt, hgt, this);
var fx_ai = GetAI(clonk);
if (!fx_ai)
return false;
// Clip to landscape size.
if (x < 0)
{
wdt += x;
x = 0;
}
if (y < 0)
{
hgt += y;
y = 0;
}
wdt = Min(wdt, LandscapeWidth() - x);
hgt = Min(hgt, LandscapeHeight() - y);
fx_ai.guard_range = {x = x, y = y, wdt = wdt, hgt = hgt};
return true;
}
// Set the maximum distance the enemy will follow an attacking clonk.
public func SetMaxAggroDistance(object clonk, int max_dist)
{
if (GetType(this) != C4V_Def)
Log("WARNING: SetMaxAggroDistance(%v, %d) not called from definition context but from %v", clonk, max_dist, this);
var fx_ai = GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.max_aggro_distance = max_dist;
return true;
}
// Set range in which, on first encounter, allied AI clonks get the same aggro target set.
public func SetAllyAlertRange(object clonk, int new_range)
{
if (GetType(this) != C4V_Def)
Log("WARNING: SetAllyAlertRange(%v, %d) not called from definition context but from %v", clonk, new_range, this);
var fx_ai = GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.ally_alert_range = new_range;
return true;
}
// Set callback function name to be called in game script when this AI is first encountered
// Callback function first parameter is (this) AI clonk, second parameter is player clonk.
// The callback should return true to be cleared and not called again. Otherwise, it will be called every time a new target is found.
public func SetEncounterCB(object clonk, string cb_fn)
{
if (GetType(this) != C4V_Def)
Log("WARNING: SetEncounterCB(%v, %s) not called from definition context but from %v", clonk, cb_fn, this);
var fx_ai = GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.encounter_cb = cb_fn;
return true;
}
// Set attack path
public func SetAttackPath(object clonk, array new_attack_path)
{
if (GetType(this) != C4V_Def)
Log("WARNING: SetAttackPath(%v, %v) not called from definition context but from %v", clonk, new_attack_path, this);
var fx_ai = GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.attack_path = new_attack_path;
return true;
}
// Set controlled vehicle
public func SetVehicle(object clonk, object new_vehicle)
{
if (GetType(this) != C4V_Def)
Log("WARNING: SetVehicle(%v, %v) not called from definition context but from %v", clonk, new_vehicle, this);
var fx_ai = GetAI(clonk);
if (!fx_ai)
return false;
fx_ai.vehicle = new_vehicle;
return true;
}
/*-- AI Effect --*/
// The AI effect stores a lot of information about the AI clonk. This includes its state, enemy target, alertness etc.
// Each of the functions which are called in the AI definition pass the effect and can then access these variables.
// The most important variables are:
// fx.Target - The AI clonk.
// fx.target - The current target the AI clonk will attack.
// fx.alert - Whether the AI clonk is alert and aware of enemies around it.
// fx.weapon - Currently selected weapon by the AI clonk.
// fx.ammo_check - Function that is called to check ammunition for fx.weapon.
// fx.commander - Is commanded by another AI clonk.
// fx.control - Definition controlling this AI, all alternative AIs should include the basic AI.
local FxAI = new Effect
{
Construction = func(id control_def)
{
// Execute AI every 3 frames.
this.Interval = 3;
// Store the definition that controls this AI.
this.control = control_def;
// Store the vehicle the AI is using.
if (this.Target->GetProcedure() == "PUSH")
this.vehicle = this.Target->GetActionTarget();
// Store whether the enemy is controlled by a commander.
this.commander = this.Target.commander;
// Give the AI a helper function to get the AI control effect.
this.Target.ai = this;
this.Target.GetAI = this.GetAI;
return FX_OK;
},
_inherited(def);
GetAI = func()
{
return this.ai;
},
// Can be added to Clonk
AddEditorProp_AISelection(Clonk, AI);
}
Timer = func(int time)
{
// Execute the AI in the clonk.
this.control->Execute(this, time);
return FX_OK;
},
Destruction = func(int reason)
{
// Remove weapons on death.
if (reason == FX_Call_RemoveDeath)
{
if (this.bound_weapons)
for (var obj in this.bound_weapons)
if (obj && obj->Contained() == Target)
obj->RemoveObject();
}
// Remove AI reference.
if (Target && Target.ai == this)
Target.ai = nil;
return FX_OK;
},
Damage = func(int dmg, int cause)
{
// AI takes damage: Make sure we're alert so evasion and healing scripts are executed!
// It might also be feasible to execute encounter callbacks here (in case an AI is shot from a position it cannot see).
// However, the attacking clonk is not known and the callback might be triggered e.g. by an unfortunate meteorite or lightning blast.
// So let's just keep it at alert state for now.
if (dmg < 0)
this.alert = this.time;
return dmg;
},
SetActive = func(bool active)
{
this.Interval = 3 * active;
},
GetActive = func()
{
return this.Interval != 0;
},
SetAttackMode = func(proplist attack_mode)
{
// Called by editor delegate when attack mode is changed.
// For now, attack mode parameter delegates are not supported. Just set by name.
return this.control->SetAttackMode(this.Target, attack_mode.Identifier);
},
SetAttackPath = func(array attack_path)
{
// Called by editor delegate when attack path is changed.
return this.control->SetAttackPath(this.Target, attack_path);
},
EditorProps = {
guard_range = { Name = "$GuardRange$", Type = "rect", Storage = "proplist", Color = 0xff00, Relative = false },
ignore_allies = { Name = "$IgnoreAllies$", Type = "bool" },
max_aggro_distance = { Name = "$MaxAggroDistance$", Type = "circle", Color = 0x808080 },
active = { Name = "$Active$", EditorHelp = "$ActiveHelp$", Type = "bool", Priority = 50, AsyncGet = "GetActive", Set = "SetActive" },
auto_search_target = { Name = "$AutoSearchTarget$", EditorHelp = "$AutoSearchTargetHelp$", Type = "bool" },
attack_path = { Name = "$AttackPath$", EditorHelp = "$AttackPathHelp$", Type = "enum", Set = "SetAttackPath", Options = [
{ Name="$None$" },
{ Name="$AttackPath$", Type=C4V_Array, Value = [{X = 0, Y = 0}], Delegate =
{ Name="$AttackPath$", EditorHelp="$AttackPathHelp$", Type="polyline", StartFromObject=true, DrawArrows=true, Color=0xdf0000, Relative=false }
}
] }
},
// Save this effect and the AI for scenarios.
SaveScen = func(proplist props)
{
if (!this.Target)
return false;
props->AddCall("AI", this.control, "AddAI", this.Target);
if (!this.Interval)
props->AddCall("AI", this.control, "SetActive", this.Target, false);
if (this.attack_mode.Identifier != "Default")
props->AddCall("AI", this.control, "SetAttackMode", this.Target, Format("%v", this.attack_mode.Identifier));
if (this.attack_path)
props->AddCall("AI", this.control, "SetAttackPath", this.Target, this.attack_path);
if (this.home_x != this.Target->GetX() || this.home_y != this.Target->GetY() || this.home_dir != this.Target->GetDir())
props->AddCall("AI", this.control, "SetHome", this.Target, this.home_x, this.home_y, GetConstantNameByValueSafe(this.home_dir, "DIR_"));
props->AddCall("AI", this.control, "SetGuardRange", this.Target, this.guard_range.x, this.guard_range.y, this.guard_range.wdt, this.guard_range.hgt);
if (this.max_aggro_distance != this.control.MaxAggroDistance)
props->AddCall("AI", this.control, "SetMaxAggroDistance", this.Target, this.max_aggro_distance);
if (this.ally_alert_range)
props->AddCall("AI", this.control, "SetAllyAlertRange", this.Target, this.ally_alert_range);
if (!this.auto_search_target)
props->AddCall("AI", this.control, "SetAutoSearchTarget", this.Target, false);
if (this.encounter_cb)
props->AddCall("AI", this.control, "SetEncounterCB", this.Target, Format("%v", this.encounter_cb));
return true;
}
};
local EditorHelp = "$EditorHelp$";
/*-- AI Execution --*/
@ -391,19 +79,25 @@ public func Execute(effect fx, int time)
// 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;
this->LogAI(fx, Format("weapon %v is out of ammo, AI won't do anything.", fx.weapon));
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))
@ -432,34 +126,12 @@ public func Execute(effect fx, int time)
this->ExecuteAppearance(fx);
// Attack it!
if (!this->IsWeaponForTarget(fx))
this->LogAI(fx, Format("weapon of type %i is not fit to attack %v (type: %i).", fx.weapon->GetID(), fx.target, fx.target->GetID()));
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);
}
// Selects an item the clonk is about to use.
public func SelectItem(effect fx, object item)
{
if (!item)
return;
if (item->Contained() != fx.Target)
return;
fx.Target->SetHandItemPos(0, fx.Target->GetItemPos(item));
}
public func CancelAiming(effect fx)
{
if (fx.aim_weapon)
{
fx.aim_weapon->~ControlUseCancel(fx.Target);
fx.aim_weapon = nil;
}
else
{
// Also cancel aiming done outside AI control.
fx.Target->~CancelAiming();
}
return true;
}
public func ExecuteThrow(effect fx)
{
@ -501,20 +173,6 @@ public func ExecuteThrow(effect fx)
return true;
}
public func CheckHandsAction(effect fx)
{
// Can use hands?
if (fx.Target->~HasHandAction())
return true;
// Can't throw: Is it because e.g. we're scaling?
if (!fx.Target->HasActionProcedure())
{
this->ExecuteStand(fx);
return false;
}
// Probably hands busy. Just wait.
return false;
}
public func ExecuteArm(effect fx)
{
@ -525,6 +183,7 @@ public func ExecuteArm(effect fx)
{
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;
@ -532,7 +191,10 @@ public func ExecuteArm(effect fx)
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))
@ -544,236 +206,3 @@ public func ExecuteArm(effect fx)
// No weapon.
return false;
}
public func FindInventoryWeapon(effect fx)
{
// Find weapon in inventory, mark it as equipped and set according strategy, etc.
fx.ammo_check = nil;
fx.ranged = false;
if (FindInventoryWeaponGrenadeLauncher(fx)) return true;
if (FindInventoryWeaponBlunderbuss(fx)) return true;
if (FindInventoryWeaponBow(fx)) return true;
if (FindInventoryWeaponJavelin(fx)) return true;
// Throwing weapons.
if ((fx.weapon = fx.Target->FindContents(Firestone)) || (fx.weapon = fx.Target->FindContents(Rock)) || (fx.weapon = fx.Target->FindContents(Lantern)))
{
fx.can_attack_structures = fx.weapon->~HasExplosionOnImpact();
fx.strategy = this.ExecuteThrow;
return true;
}
// Melee weapons.
if ((fx.weapon = fx.Target->FindContents(Sword)) || (fx.weapon = fx.Target->FindContents(Axe))) // Sword attacks aren't 100% correct for Axe, but work well enough
{
fx.strategy = this.ExecuteMelee;
return true;
}
if ((fx.weapon = fx.Target->FindContents(PowderKeg)))
{
fx.strategy = this.ExecuteBomber;
return true;
}
if ((fx.weapon = fx.Target->FindContents(Club)))
{
fx.strategy = this.ExecuteClub;
return true;
}
// No weapon.
return false;
}
private func FindInventoryWeaponGrenadeLauncher(effect fx)
{
if (fx.weapon = fx.Target->FindContents(GrenadeLauncher))
{
if (this->HasBombs(fx, fx.weapon))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = 75;
fx.ammo_check = this.HasBombs;
fx.ranged = true;
fx.can_attack_structures = true;
return true;
}
else
fx.weapon = nil;
}
}
private func FindInventoryWeaponBlunderbuss(effect fx)
{
if (fx.weapon = fx.Target->FindContents(Blunderbuss))
{
if (this->HasAmmo(fx, fx.weapon))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = 200;
fx.ammo_check = this.HasAmmo;
fx.ranged = true;
fx.ranged_direct = true;
return true;
}
else
fx.weapon = nil;
}
}
private func FindInventoryWeaponBow(effect fx)
{
if (fx.weapon = fx.Target->FindContents(Bow))
{
if (this->HasArrows(fx, fx.weapon))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = 100;
fx.ammo_check = this.HasArrows;
fx.ranged = true;
var arrow = fx.weapon->Contents(0) ?? FindObject(Find_Container(fx.Target), Find_Func("IsArrow"));
fx.can_attack_structures = arrow && arrow->~IsExplosive();
return true;
}
else
fx.weapon = nil;
}
}
private func FindInventoryWeaponJavelin(effect fx)
{
if (fx.weapon = fx.Target->FindContents(Javelin))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = fx.Target.ThrowSpeed * fx.weapon.shooting_strength / 100;
fx.ranged=true;
return true;
}
}
/*-- Editor Properties --*/
public func Definition(proplist def)
{
if (!Clonk.EditorProps)
Clonk.EditorProps = {};
if (def == AI) // TODO: Make AI an enum so different AI types can be selected.
{
Clonk.EditorProps.AI =
{
Type = "has_effect",
Effect = "FxAI",
Set = Format("%i->SetAI", def),
SetGlobal = true
};
}
def->DefinitionAttackModes(def);
// Add AI user actions.
var enemy_evaluator = UserAction->GetObjectEvaluator("IsClonk", "$Enemy$", "$EnemyHelp$");
enemy_evaluator.Priority = 100;
UserAction->AddEvaluator("Action", "Clonk", "$SetAIActivated$", "$SetAIActivatedHelp$", "ai_set_activated", [def, def.EvalAct_SetActive],
{
Enemy = nil,
AttackTarget = {
Function= "triggering_clonk"
},
Status = {
Function = "bool_constant",
Value = true
}
},
{
Type = "proplist",
Display = "{{Enemy}}: {{Status}} ({{AttackTarget}})",
EditorProps = {
Enemy = enemy_evaluator,
AttackTarget = UserAction->GetObjectEvaluator("IsClonk", "$AttackTarget$", "$AttackTargetHelp$"),
Status = new UserAction.Evaluator.Boolean { Name = "$Status$" }
}
}
);
UserAction->AddEvaluator("Action", "Clonk", "$SetAINewHome$", "$SetAINewHomeHelp$", "ai_set_new_home", [def, def.EvalAct_SetNewHome],
{
Enemy = nil,
HomePosition = nil,
Status = {
Function = "bool_constant",
Value = true
}
},
{
Type = "proplist",
Display = "{{Enemy}} -> {{NewHome}}",
EditorProps = {
Enemy = enemy_evaluator,
NewHome = new UserAction.Evaluator.Position {
Name = "$NewHome$",
EditorHelp = "$NewHomeHelp$"
},
NewHomeDir = {
Type = "enum",
Name = "$NewHomeDir$",
EditorHelp = "$NewHomeDirHelp$",
Options = [
{ Name = "$Unchanged$" },
{ Name = "$Left$", Value = DIR_Left },
{ Name = "$Right$", Value = DIR_Right }
]
},
}
}
);
}
public func EvalAct_SetActive(proplist props, proplist context)
{
// User action: Activate enemy AI.
var enemy = UserAction->EvaluateValue("Object", props.Enemy, context);
var attack_target = UserAction->EvaluateValue("Object", props.AttackTarget, context);
var status = UserAction->EvaluateValue("Boolean", props.Status, context);
if (!enemy)
return;
// Ensure enemy AI exists
var fx = GetAI(enemy);
if (!fx)
{
// Deactivated? Then we don't need an AI effect.
if (!status)
return;
fx = AddAI(enemy);
if (!fx || !enemy)
return;
}
// Set activation.
fx->SetActive(status);
// Set specific target if desired.
if (attack_target)
fx.target = attack_target;
}
public func EvalAct_SetNewHome(proplist props, proplist context)
{
// User action: Set new home.
var enemy = UserAction->EvaluateValue("Object", props.Enemy, context);
var new_home = UserAction->EvaluatePosition(props.NewHome, context);
var new_home_dir = props.NewHomeDir;
if (!enemy)
return;
// Ensure enemy AI exists.
var fx = GetAI(enemy);
if (!fx)
{
fx = AddAI(enemy);
if (!fx || !enemy)
return;
// Create without attack command.
SetAutoSearchTarget(enemy, false);
}
fx.command = this.ExecuteIdle;
fx.home_x = new_home[0];
fx.home_y = new_home[1];
if (GetType(new_home_dir))
fx.home_dir = new_home_dir;
}
/*-- Properties --*/
local Plane = 300;

View File

@ -1,27 +1 @@
GuardRange=Bewachter Bereich
MaxAggroDistance=Angriffsradius
IgnoreAllies=Mitspieler ignorieren
Active=Aktiv
ActiveHelp=Ob die KI aktiv den Clonk momentan steuert. Eine inaktive KI kann über die Aktion 'KI aktivieren' zum Beispiel in einer Sequenz wieder aktiviert werden.
AutoSearchTarget=Ziel automatisch suchen
AutoSearchTargetHelp=Wenn wahr, sucht die KI automatisch Gegner. Ansonsten muss der Gegner per Sequenzbefehl gegeben werden.
Enemy=Gegner
EnemyHelp=Welcher Gegner angegriffen werden soll. Wenn nicht angegeben, greift der Gegner den nächsten Clonk an.
SetAIActivated=KI aktivieren
SetAIActivatedHelp=Aktiviert die Gegner-KI für ein Objekt. Wenn keine KI aktiviert ist, wird sie für das Objekt erstellt.
AttackTarget=Angriffsziel
AttackTargetHelp=Wenn angegeben, greift der KI-Clonk direkt diesen Gegner an.
Status=Aktiviert
StatusHelp=Ob die KI aktiviert oder deaktiviert wird.
SetAINewHome=KI Position setzen
SetAINewHomeHelp=Lässt einen KI-Gegner an eine neue Position laufen.
NewHome=Neue Position
NewHomeHelp=Wohin der Clonk laufen soll.
NewHomeDir=Richtung
NewHomeDirHelp=In welche Richtung der KI-Clonk schauen soll, wenn kein Gegner in der Nähe ist.
Unchanged=Ungeändert
Left=Links
Right=Rechts
AttackPath=Angriffspfad
AttackPathHelp=Pfad, entlang dessen sich der KI-Gegner bewegt. Befindet sich ein Gebaeude auf einem Eckpunkt des Pfades, greift der Clonk das Gebaeude an.
None=Keiner
EditorHelp=Dieser Clonk nutzt die Standard-Gegner-KI

View File

@ -1,27 +1 @@
GuardRange=Guarded area
MaxAggroDistance=Attack radius
IgnoreAllies=Ignore allies
Active=Active
ActiveHelp=Whether the AI controls the clonk currently. An inactive KI can be reactivated via the 'AI activate', for example in a sequence.
AutoSearchTarget=Auto-search target
AutoSearchTargetHelp=Looks for enemies automatically if true. If false, enemies must be provided via a script sequence.
Enemy=Enemy
EnemyHelp=Which enemy to attack. If unspecified, attacks the nearest clonk.
SetAIActivated=AI activate
SetAIActivatedHelp=Activates an enemy AI for a clonk. AI will be created if necessary.
AttackTarget=Attack target
AttackTargetHelp=If specified, this enemy is attacked by the AI clonk. If not specified, the nearest enemy will be attacked.
Status=Active
StatusHelp=Whether to activate or deactivate the AI.
SetAINewHome=AI set position
SetAINewHomeHelp=Lets an AI clonk walk to another position.
NewHome=New position
NewHomeHelp=Where to send the AI clonk.
NewHomeDir=Direction
NewHomeDirHelp=In which direction the AI clonk will look if no enemy is nearby.
Unchanged=Unchanged
Left=Left
Right=Right
AttackPath=Attack path
AttackPathHelp=Path along which the AI clonk moves. If a vertex of the attack path is placed on a structure (e.g. a gate), the clonk will attack the structure.
None=None
EditorHelp=This clonk uses the default enemy AI

View File

@ -549,10 +549,10 @@ public func GetAIClonkEditorProps()
if (!this.AIClonkEditorProps)
{
var props = {};
props.AttackMode = new AI.FxAI.EditorProps.attack_mode { Set=nil, Priority=100 };
props.AttackMode = new AI->GetControlEffect().EditorProps.attack_mode { Set=nil, Priority=100 };
props.GuardRange = { Name="$AttackRange$", EditorHelp="$AttackRangeHelp$", Type="enum", Options = [
{ Name="$Automatic$", EditorHelp="$AutomaticGuardRangeHelp$"},
{ Name="$Custom$", Type=C4V_PropList, Value={}, DefaultValueFunction=this.GetDefaultAIRect, Delegate=AI.FxAI.EditorProps.guard_range }
{ Name="$Custom$", Type=C4V_PropList, Value={}, DefaultValueFunction=this.GetDefaultAIRect, Delegate=AI->GetControlEffect().EditorProps.guard_range }
] };
props.Color = { Name="$Color$", Type="color" };
props.Bounty = { Name="$Bounty$", EditorHelp="$BountyHelp$", Type="int", Min=0, Max=100000 };

View File

@ -43,7 +43,7 @@ protected func InitializePlayer(int plr)
// Add test control effect.
var fx = AddEffect("IntTestControl", nil, 100, 2);
fx.testnr = 11;
fx.testnr = 1;
fx.launched = false;
fx.plr = plr;
return;