openclonk/planet/Objects.ocd/Goals.ocd/Defense.ocd/DefenseBoomAttack.ocd/Script.c

407 lines
12 KiB
C

/**
Boom Attack
An evil rocket which attacks you, can be ridden as well.
@authors Randrian, Newton, Sven2
*/
public func Construction()
{
SetAction("Fly");
SetComDir(COMD_None);
// Notify friendly fire rule.
GameCallEx("OnCreationRuleNoFF", this);
// Add flight effects.
CreateEffect(FxFlightRotation, 100, 1);
CreateEffect(FxFlight, 100, 2);
return;
}
/*-- Flight --*/
// Rotates the boom attack slowly around its axis.
local FxFlightRotation = new Effect
{
Construction = func()
{
this.rotation = 0;
},
Timer = func(int time)
{
if (Target->GetRider())
return FX_Execute_Kill;
this.rotation += 2;
if (this.rotation >= 360)
this.rotation = 0;
Target.MeshTransformation = Trans_Rotate(this.rotation, 0, 1, 0);
return FX_OK;
}
};
// Controls the boom attack flight by flying through the given waypoints or to the target.
// The way points is a list of coordinates as [{X = ??, Y = ??}, ...]. The waypoints are
// dealt with first and then the target is aimed for.
local FxFlight = new Effect
{
Construction = func()
{
// Add AI for logging purposes.
this.fx_ai = DefenseAI->AddAI(Target);
this.fx_ai->SetActive(false);
// Get a target.
this.target = GetRandomAttackTarget(Target);
// Get the boom attack waypoints from the scenario or make an array.
this.waypoints = GameCall("GetBoomAttackWaypoints", Target) ?? [];
this.current_waypoint = nil;
this.attack_on_way_point_flight = false;
if (this.target)
{
var dx = this.target->GetX() - Target->GetX();
var dy = this.target->GetY() + this.target->GetBottom() - Target->GetY();
Target->SetR(Angle(0, 0, dx, dy));
}
// Immediately start flying unless we don't have a target set yet
// If there's no target, it may be set directly after creation so let the rocket survive for one frame
// by not calling the timer yet.
if (this.target || GetLength(this.waypoints))
{
this->Timer(0);
}
},
Timer = func(int time)
{
// Find target and if not explode.
if (!this.target)
{
this.target = GetRandomAttackTarget(Target);
DefenseAI->LogAI_Info(this.fx_ai, Format("BoomAttack lost target and updated it to %v.", this.target));
if (!this.target && !this.current_waypoint && GetLength(this.waypoints) == 0)
{
Target->DoFireworks(NO_OWNER);
return FX_Execute_Kill;
}
}
// Check if reached current waypoint.
if (this.current_waypoint && Distance(this.current_waypoint.X, this.current_waypoint.Y, Target->GetX(), Target->GetY()) < 8)
{
DefenseAI->LogAI_Info(this.fx_ai, Format("BoomAttack reached waypoint (%d, %d).", this.current_waypoint.X, this.current_waypoint.Y));
this.current_waypoint = nil;
}
// Get relative coordinates to target.
var dx, dy;
// Handle waypoints and get coordinates to move to.
if (this.current_waypoint || GetLength(this.waypoints) > 0)
{
if (!this.current_waypoint)
this.current_waypoint = PopFront(this.waypoints);
// Set relative coordinates to new waypoint.
dx = this.current_waypoint.X - Target->GetX();
dy = this.current_waypoint.Y - Target->GetY();
}
if (this.target)
{
// Explode if close enough to target.
if (ObjectDistance(Target, this.target) < 12)
{
DefenseAI->LogAI_Info(this.fx_ai, Format("BoomAttack is in reach of enemy %v and explodes now.", this.target));
Target->DoFireworks(NO_OWNER);
return FX_Execute_Kill;
}
// Move to a nearby target if path is free and attacking is allowed.
var target_on_path = GetAttackTargetOnWaypointPath();
if (target_on_path && this.current_waypoint)
{
// Give up current and future waypoints and set to new target.
this.current_waypoint = nil;
this.waypoints = [];
this.target = target_on_path;
DefenseAI->LogAI_Info(this.fx_ai, Format("BoomAttack found new target %v on waypoint path.", target_on_path));
}
// Get relative coordinates to target.
if (!this.current_waypoint)
{
dx = this.target->GetX() - Target->GetX();
dy = this.target->GetY() + this.target->GetBottom() - Target->GetY();
// Check if path is free to target, if not try to find a way around using waypoints.
if (!PathFree(this.target->GetX(), this.target->GetY(), Target->GetX(), Target->GetY())/* && !Target->GBackSolid(dx, dy)*/)
{
// Try to set a waypoint half way on a line orthogonal to the current direction.
for (var attempts = 0; attempts < 40; attempts++)
{
var d = Sqrt(dx**2 + dy**2);
var try_dist = Max(20 + 2 * attempts, d * attempts / 80) + RandomX(-10, 10);
var line_dist = (2 * Random(2) - 1) * try_dist;
var way_x = Target->GetX() + dx / 2 + dy * line_dist / d;
var way_y = Target->GetY() + dy / 2 - dx * line_dist / d;
// Path to new waypoint must be free and inside the landscape borders.
if (!PathFree(Target->GetX(), Target->GetY(), way_x, way_y) || !PathFree(this.target->GetX(), this.target->GetY(), way_x, way_y))
continue;
if (!Inside(way_x, 0, LandscapeWidth()) || !Inside(way_y, 0, LandscapeHeight()))
continue;
DefenseAI->LogAI_Info(this.fx_ai, Format("BoomAttack at (%d, %d) is aiming for %v at (%d, %d) takes a new route through (%d, %d).", Target->GetX(), Target->GetY(), this.target, this.target->GetX(), this.target->GetY(), way_x, way_y));
this.current_waypoint = {X = way_x, Y = way_y};
break;
}
}
}
// At this distance, fly horizontally. When getting closer, gradually turn to direct flight into target.
if (!this.current_waypoint)
{
var aim_dist = 600;
dy = dy * (aim_dist - Abs(dx)) / aim_dist;
}
}
var angle_to_target = Angle(0, 0, dx, dy);
var angle_rocket = Target->GetR();
if (angle_rocket < 0)
angle_rocket += 360;
// Gradually update the angle.
var angle_delta = angle_rocket - angle_to_target;
var angle_step = BoundBy(Target.FlySpeed / 25, 4, 10);
if (Inside(angle_delta, 0, 180) || Inside(angle_delta, -360, -180))
Target->SetR(Target->GetR() - Min(angle_step, Abs(angle_delta)));
else if (Inside(angle_delta, -180, 0) || Inside(angle_delta, 180, 360))
Target->SetR(Target->GetR() + Min(angle_step, Abs(angle_delta)));
// Update velocity according to angle.
Target->SetXDir(Sin(Target->GetR(), Target.FlySpeed), 100);
Target->SetYDir(-Cos(Target->GetR(), Target.FlySpeed), 100);
// Create exhaust fire.
var x = -Sin(Target->GetR(), 15);
var y = +Cos(Target->GetR(), 15);
var xdir = Target->GetXDir() / 2;
var ydir = Target->GetYDir() / 2;
Target->CreateParticle("FireDense", x, y, PV_Random(xdir - 4, xdir + 4), PV_Random(ydir - 4, ydir + 4), PV_Random(16, 38), Particles_Thrust(), 5);
return FX_OK;
},
AddWaypoint = func(proplist waypoint)
{
PushBack(this.waypoints, waypoint);
},
SetWaypoints = func(array waypoints)
{
this.waypoints = waypoints;
},
SetTarget = func(object target)
{
this.target = target;
},
SetAttackOnWaypointPath = func(bool on)
{
this.attack_on_way_point_path = on;
},
GetAttackTargetOnWaypointPath = func()
{
var attack_target = GameCall("GiveAttackTargetOnWaypointPath", Target);
if (attack_target && PathFree(Target->GetX(), Target->GetY(), attack_target->GetX(), attack_target->GetY()))
return attack_target;
if (!this.attack_on_way_point_path)
return nil;
return Target->FindObject(Find_Category(C4D_Structure | C4D_Living | C4D_Vehicle), Find_Hostile(Target->GetController()), Find_Distance(100), Target->Find_PathFree(), Sort_Distance());
}
};
/*-- Waypoints & Target --*/
public func AddWaypoint(proplist waypoint)
{
var fx = GetEffect("FxFlight", this);
if (fx)
fx->AddWaypoint(waypoint);
return;
}
public func SetWaypoints(array waypoints)
{
var fx = GetEffect("FxFlight", this);
if (fx)
fx->SetWaypoints(waypoints);
return;
}
public func SetTarget(object target)
{
var fx = GetEffect("FxFlight", this);
if (fx)
fx->SetTarget(target);
return;
}
/*-- Riding --*/
local riderattach;
local rider;
public func SetRider(object to)
{
rider = to;
return;
}
public func GetRider() { return rider; }
public func OnMount(object clonk)
{
SetRider(clonk);
var dir = -1;
if (GetX() > LandscapeWidth() / 2)
dir = 1;
clonk->PlayAnimation("PosRocket", CLONK_ANIM_SLOT_Arms, Anim_Const(0));
riderattach = AttachMesh(clonk, "main", "pos_tool1", Trans_Translate(-1000, 2000 * dir, 2000));
return true;
}
public func OnUnmount(object clonk)
{
clonk->StopAnimation(clonk->GetRootAnimation(10));
DetachMesh(riderattach);
return;
}
/*-- Explosion --*/
// Don't get hit by projectiles shot from own rider.
public func IsProjectileTarget(object projectile, object shooter) { return (!shooter) || (shooter->GetActionTarget() != this); }
public func OnProjectileHit(object shot) { return DoFireworks(shot->GetController()); }
public func ContactBottom() { return Hit(); }
public func ContactTop() { return Hit(); }
public func ContactLeft() { return Hit(); }
public func ContactRight() { return Hit(); }
public func Hit() { return DoFireworks(NO_OWNER); }
public func HitObject(object ) { return DoFireworks(NO_OWNER); }
public func Damage(int change, int cause, int cause_plr)
{
if (change > 0)
return DoFireworks(cause_plr);
return;
}
public func Incineration(int caused_by)
{
if (OnFire())
return DoFireworks(caused_by);
return;
}
private func DoFireworks(int killed_by)
{
if (rider)
{
rider->Fling(RandomX(-5, 5), -5);
rider->SetAction("Walk");
SetRider(nil);
}
SetKiller(killed_by);
Fireworks();
Explode(40);
return;
}
public func Destruction()
{
// Notify defense goal for reward and score.
GameCallEx("OnRocketDeath", this, GetKiller());
// Notify friendly fire rule.
GameCallEx("OnDestructionRuleNoFF", this);
}
public func HasNoNeedForAI() { return true; }
/*-- Enemy spawn registration --*/
public func Definition(def)
{
if (def == DefenseBoomAttack)
{
var spawn_editor_props = { Type="proplist", Name=def->GetName(), EditorProps= {
Rider = new EnemySpawn->GetAICreatureEditorProps(nil, "$NoRiderHelp$") { Name="$Rider$", EditorHelp="$RiderHelp$" },
FlySpeed = { Name="$FlySpeed$", EditorHelp="$FlySpeedHelp$", Type="int", Min=5, Max=10000 },
} };
var spawn_default_values = {
Rider = nil,
FlySpeed = def.FlySpeed,
};
EnemySpawn->AddEnemyDef("BoomAttack", { SpawnType=DefenseBoomAttack, SpawnFunction=def.SpawnBoomAttack, OffsetAttackPathByPos=true, GetInfoString=def.GetSpawnInfoString }, spawn_default_values, spawn_editor_props);
}
}
private func SpawnBoomAttack(array pos, proplist enemy_data, proplist enemy_def, array attack_path, object spawner)
{
// Spawn the boomattack
var boom = CreateObject(DefenseBoomAttack, pos[0], pos[1], g_enemyspawn_player);
if (!boom) return;
// Boomattack settings
boom.FlySpeed = enemy_data.FlySpeed;
var wp0 = attack_path[0];
boom->SetR(Angle(0, 0, wp0.X - pos[0], wp0.Y - pos[1]) + Random(11) - 5);
boom->SetWaypoints(attack_path);
// Rider?
var clonk = EnemySpawn->SpawnAICreature(enemy_data.Rider, pos, enemy_def, [attack_path[-1]], spawner);
if (clonk)
{
clonk->SetAction("Ride", boom);
return [boom, clonk];
}
// Return rider-less boom attack
return boom;
}
private func GetSpawnInfoString(proplist enemy_data)
{
if (enemy_data.Rider && enemy_data.Rider.Type == "Clonk")
{
return Format("{{DefenseBoomAttack}}%s", EnemySpawn->GetAIClonkInfoString(enemy_data.Rider.Properties));
}
else
{
return "{{DefenseBoomAttack}}";
}
}
/*-- Properties --*/
local ActMap = {
Fly = {
Prototype = Action,
Name = "Fly",
Procedure = DFA_FLOAT,
Length = 1,
Delay = 0,
Wdt = 15,
Hgt = 27,
}
};
local Name = "$Name$";
local Description = "$Description$";
local ContactCalls = true;
local FlySpeed = 100;
local BlastIncinerate = 8;
local ContactIncinerate = 8;
local HasNoFriendlyFire = true;