Add AI attack mode (weapon) to editor properties

alut-include-path
Sven Eberhardt 2017-02-14 01:35:40 -05:00
parent 60f3e206cc
commit da4f49d7ca
7 changed files with 315 additions and 76 deletions

View File

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

View File

@ -0,0 +1,176 @@
/**
AI Attack Modes
Initializes the AI to have different weapons and attack scripts
@author Sven2
*/
// 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);
var fx_ai = this->GetAI(clonk);
if (!fx_ai)
return false;
var attack_mode = this.AttackModes[attack_mode_identifier];
if (!attack_mode)
return Log("Attack mode %s does not exist.", attack_mode_identifier);
// Stop previous attack mode
if (fx_ai.attack_mode && fx_ai.attack_mode.Destruction) Call(fx_ai.attack_mode.Destruction, fx_ai);
// Set new
fx_ai.attack_mode = attack_mode;
if (fx_ai.attack_mode.Construction) Call(fx_ai.attack_mode.Construction, fx_ai);
// Remember inventory to be destroyed on death
this->BindInventory(clonk);
return true;
}
local AttackModes;
// Attack mode that just creates a weapon and uses the default attack procedures
local SingleWeaponAttackMode = {
Construction = func(effect fx)
{
var weapon = fx.Target->CreateContents(fx.attack_mode.Weapon);
if (weapon)
{
if (fx.attack_mode.Ammo)
{
var ammo = weapon->CreateContents(fx.attack_mode.Ammo);
if (ammo) ammo->~SetInfiniteStackCount();
}
// Do not save in scenario, because it's automatically created through the attack mode setting
AddEffect("IntNoScenarioSave", weapon, 1);
// Automatic fadeout+inventory respawn of e.g. firestones
if (fx.attack_mode.Respawn)
{
weapon->~SetStackCount(1); // Ensure departure is called on every object
weapon.Departure = AI.Departure_WeaponRespawn;
}
}
},
Destruction = func(effect fx)
{
var weapon = fx.Target->FindContents(fx.attack_mode.Weapon);
if (weapon) weapon->RemoveObject();
},
FindWeapon = func(effect fx)
{
if (!(fx.weapon = fx.Target->FindContents(fx.attack_mode.Weapon))) return false;
fx.strategy = fx.attack_mode.Strategy;
return true;
},
GetName = func()
{
if (this.Ammo)
return Format("%s (%s)", this.Weapon->GetName(), this.Ammo->GetName());
else
return this.Weapon->GetName();
},
GetEditorHelp = func()
{
if (this.Ammo)
return Format("$AttackWithAmmo$", this.Weapon->GetName(), this.Ammo->GetName());
else
return Format("$AttackWith$", this.Weapon->GetName());
}
};
private func InitAttackModes()
{
// First-time init of attack mode editor prop structures
if (!this.AttackModes)
{
// All attack modes structures point to the base AI
if (!AI.AttackModes) AI.AttackModes = {};
if (this != AI) this.AttackModes = AI.AttackModes;
}
if (!AI.FxAI.EditorProps.attack_mode)
{
AI.FxAI.EditorProps.attack_mode = {
Name="$AttackMode$",
EditorHelp="$AttackModeHelp$",
Type="enum",
Sorted=true,
Options=[],
Set="SetAttackMode"
};
}
if (!this.FxAI.EditorProps.attack_mode) this.FxAI.EditorProps.attack_mode = AI.FxAI.EditorProps.attack_mode;
}
public func RegisterAttackMode(string identifier, proplist am)
{
// 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;
am.Identifier = identifier;
// Add to editor option for AI effect
var am_option = {
Name = am.Name ?? am->GetName(),
EditorHelp = am.EditorHelp,
Value = am
};
if (!am_option.EditorHelp && am.GetEditorHelp) am_option.EditorHelp = am->GetEditorHelp();
var editor_opts = this.FxAI.EditorProps.attack_mode.Options;
editor_opts[GetLength(editor_opts)] = am_option;
}
private func DefinitionAttackModes(proplist def)
{
// Register presets for all the default weapons usable by the AI
this->InitAttackModes();
// Registration only once for base AI
if (this != AI) return;
def->RegisterAttackMode("Default", { Name = "$Default$", EditorHelp = "$DefaultHelp$", FindWeapon = AI.FindInventoryWeapon });
def->RegisterAttackMode("Sword", new SingleWeaponAttackMode { Weapon = Sword, Strategy = this.ExecuteMelee });
def->RegisterAttackMode("Club", new SingleWeaponAttackMode { Weapon = Club, Strategy = this.ExecuteMelee });
def->RegisterAttackMode("Axe", new SingleWeaponAttackMode { Weapon = Axe, Strategy = this.ExecuteMelee });
def->RegisterAttackMode("BowArrow", new SingleWeaponAttackMode { Weapon = Bow, Ammo = Arrow, FindWeapon = this.FindInventoryWeaponBow });
def->RegisterAttackMode("BowFireArrow", new SingleWeaponAttackMode { Weapon = Bow, Ammo = FireArrow, FindWeapon = this.FindInventoryWeaponBow });
def->RegisterAttackMode("BowBombArrow", new SingleWeaponAttackMode { Weapon = Bow, Ammo = BombArrow, FindWeapon = this.FindInventoryWeaponBow });
def->RegisterAttackMode("GrenadeLauncher", new SingleWeaponAttackMode { Weapon = GrenadeLauncher, Ammo = IronBomb, FindWeapon = this.FindInventoryWeaponGrenadeLauncher });
def->RegisterAttackMode("Blunderbuss", new SingleWeaponAttackMode { Weapon = Blunderbuss, Ammo = LeadBullet, FindWeapon = this.FindInventoryWeaponBlunderbuss });
def->RegisterAttackMode("Javelin", new SingleWeaponAttackMode { Weapon = Javelin, Respawn = true, FindWeapon = this.FindInventoryWeaponJavelin });
def->RegisterAttackMode("Rock", new SingleWeaponAttackMode { Weapon = Rock, Respawn = true, Strategy = this.ExecuteThrow });
def->RegisterAttackMode("Firestone", new SingleWeaponAttackMode { Weapon = Firestone, Respawn = true, Strategy = this.ExecuteThrow });
def->RegisterAttackMode("Lantern", new SingleWeaponAttackMode { Weapon = Lantern, Respawn = true, Strategy = this.ExecuteThrow });
return true;
}
/* Attack with respawning weapons */
func Departure_WeaponRespawn(object container)
{
// Weapon used? Schedule to respawn a new one!
if (container->GetAlive() || container->GetID()==Catapult)
{
container->ScheduleCall(container, AI_AttackModes.DoWeaponRespawn, 5, 1, GetID());
}
// Remove this weapon after a while
// (This function should be save to be called in foreign context)
ScheduleCall(this, Rule_ObjectFade.FadeOutObject, 120, 1, this);
// No double-respawn in case it gets collected
this.Departure = nil;
}
func DoWeaponRespawn(id_weapon)
{
if (GetAlive() || GetID()==Catapult)
{
var re_weapon = CreateContents(id_weapon);
if (re_weapon)
{
re_weapon->~SetStackCount(1); // Ensure departure is called on every object
re_weapon.Departure = AI_AttackModes.Departure_WeaponRespawn;
// Do not save in scenario, because it's automatically created through the attack mode setting
AddEffect("IntNoScenarioSave", re_weapon, 1);
}
return re_weapon;
}
}

View File

@ -0,0 +1,6 @@
AttackMode=Waffe
AttackModeHelp=Legt die Waffe und den Angriffsmodus dieses KI-Gegners fest.
Default=Automatisch
DefaultHelp=Benutzt immer die beste Waffe im Inventar.
AttackWith=Angriff mit %s.
AttackWithAmmo=Angriff mit %s und %s.

View File

@ -0,0 +1,6 @@
AttackMode=Weapon
AttackModeHelp=Selects the weapon and attack mode of this AI enemy.
Default=Automatic
DefaultHelp=Always uses the best weapon in the Clonk's inventory.
AttackWith=Attack with %s.
AttackWithAmmo=Attack with %s and %s.

View File

@ -19,6 +19,7 @@
#include AI_RangedWeapons
#include AI_TargetFinding
#include AI_Vehicles
#include AI_AttackModes
// AI Settings.
@ -58,7 +59,7 @@ public func AddAI(object clonk)
if (!fx_ai)
return;
// Add AI default settings.
BindInventory(clonk);
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);
@ -225,8 +226,6 @@ local FxAI = new Effect
this.Interval = 3;
// Store the definition that controls this AI.
this.control = control_def;
// Init editor properties.
this->InitEditorProps();
// Store the vehicle the AI is using.
if (this.Target->GetProcedure() == "PUSH")
this.vehicle = this.Target->GetActionTarget();
@ -276,17 +275,6 @@ local FxAI = new Effect
this.alert = this.time;
return dmg;
},
InitEditorProps = func()
{
// Editor properties for the AI.
this.EditorProps = {};
this.EditorProps.guard_range = { Name = "$GuardRange$", Type = "rect", Storage = "proplist", Color = 0xff00, Relative = false };
this.EditorProps.ignore_allies = { Name = "$IgnoreAllies$", Type = "bool" };
this.EditorProps.max_aggro_distance = { Name = "$MaxAggroDistance$", Type = "circle", Color = 0x808080 };
this.EditorProps.active = { Name = "$Active$", EditorHelp = "$ActiveHelp$", Type = "bool", Priority = 50, AsyncGet = "GetActive", Set = "SetActive" };
this.EditorProps.auto_search_target = { Name = "$AutoSearchTarget$", EditorHelp = "$AutoSearchTargetHelp$", Type = "bool" };
},
SetActive = func(bool active)
{
this.Interval = 3 * active;
@ -295,7 +283,17 @@ local FxAI = new Effect
{
return this.Interval != 0;
},
SetAttackMode = func(proplist attack_mode)
{
return this.control->SetAttackMode(this.Target, attack_mode.Identifier);
},
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" }
},
// Save this effect and the AI for scenarios.
SaveScen = func(proplist props)
{
@ -304,6 +302,8 @@ local FxAI = new Effect
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.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);
@ -478,9 +478,10 @@ public func ExecuteArm(effect fx)
{
// Find shield.
fx.shield = fx.Target->FindContents(Shield);
// Find a weapon. For now, just search own inventory.
if (this->FindInventoryWeapon(fx) && fx.weapon->Contained() == fx.Target)
// 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;
}
@ -506,57 +507,10 @@ public func FindInventoryWeapon(effect fx)
else
fx.weapon = nil;
}
if (fx.weapon = fx.Target->FindContents(GrenadeLauncher))
{
if (this->HasBombs(fx, fx.weapon))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = 75;
fx.aim_wait = 85;
fx.ammo_check = this.HasBombs;
fx.ranged = true;
return true;
}
else
fx.weapon = nil;
}
if (fx.weapon = fx.Target->FindContents(Bow))
{
if (this->HasArrows(fx, fx.weapon))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = 100;
fx.aim_wait = 0;
fx.ammo_check = this.HasArrows;
fx.ranged = true;
return true;
}
else
fx.weapon = nil;
}
if (fx.weapon = fx.Target->FindContents(Blunderbuss))
{
if (this->HasAmmo(fx, fx.weapon))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = 200;
fx.aim_wait = 85;
fx.ammo_check = this.HasAmmo;
fx.ranged = true;
fx.ranged_direct = true;
return true;
}
else
fx.weapon = nil;
}
if (fx.weapon = fx.Target->FindContents(Javelin))
{
fx.strategy = this.ExecuteRanged;
fx.projectile_speed = fx.Target.ThrowSpeed * fx.weapon.shooting_strength / 100;
fx.aim_wait = 16;
fx.ranged=true;
return true;
}
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)))
{
@ -573,6 +527,73 @@ public func FindInventoryWeapon(effect fx)
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.aim_wait = 85;
fx.ammo_check = this.HasBombs;
fx.ranged = 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.aim_wait = 85;
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.aim_wait = 0;
fx.ammo_check = this.HasArrows;
fx.ranged = true;
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.aim_wait = 16;
fx.ranged=true;
return true;
}
}
/*-- Editor Properties --*/
@ -580,13 +601,17 @@ public func Definition(proplist def)
{
if (!Clonk.EditorProps)
Clonk.EditorProps = {};
Clonk.EditorProps.AI =
if (def == AI) // TODO: Make AI an enum so different AI types can be selected.
{
Type = "has_effect",
Effect = "FxAI",
Set = Format("%i->SetAI", def),
SetGlobal = true
};
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;

View File

@ -8,7 +8,7 @@
Use ChangeEffect properly to save calls
*/
local fade_time;
local fade_time = 18;
protected func Activate(int plr)
{
@ -28,7 +28,6 @@ protected func Initialize()
FindObject(Find_ID(Rule_ObjectFade), Find_Exclude(this))->DoFadeTime(36);
return RemoveObject();
}
fade_time = 18; // 18, because the timer will check once per second, so it's aproximately a second.
AddTimer("Timer");
}
@ -55,6 +54,12 @@ public func FxIntFadeOutCandidateTimer(object target, effect, int time)
return FX_OK;
}
public func FadeOutObject(object target)
{
// Definition or hooked call: Fade out an object (even if rule is not active)
return AddEffect("IntFadeOut", target, 100, 1, nil, Rule_ObjectFade);
}
func CheckFadeConditions(object fade)
{
// Moving objects should not.

View File

@ -46,9 +46,23 @@ global func SaveScenarioObjects(f, duplicate_objects)
// ...Except player crew
var ignore_objs = [];
if (!save_scenario_dup_objects)
{
for (var iplr = 0; iplr < GetPlayerCount(C4PT_User); ++iplr)
{
for (var icrew = 0, crew; crew = GetCrew(GetPlayerByIndex(iplr, C4PT_User), icrew); ++icrew)
{
ignore_objs[GetLength(ignore_objs)] = crew;
}
}
}
// Ignore objects tagged with a no-save effect
for (obj in objs)
{
if (GetEffect("IntNoScenarioSave", obj))
{
ignore_objs[GetLength(ignore_objs)] = obj;
}
}
// Write creation data and properties
var obj_data = SaveScen_Objects(objs, ignore_objs, props_prototype);
// Resolve dependencies
@ -59,12 +73,14 @@ global func SaveScenarioObjects(f, duplicate_objects)
FileWrite(f, "/* Automatically created objects file */\n\n");
// Declare static variables for objects that wish to have them
for (obj in objs)
{
if (obj.StaticSaveVar && !save_scenario_dup_objects)
{
if (!any_written) FileWrite(f, "static "); else FileWrite(f, ", ");
FileWrite(f, obj.StaticSaveVar);
any_written = true;
}
}
if (any_written)
{
FileWrite(f, ";\n\n");