File: Script.c

package info (click to toggle)
openclonk 8.1-4
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 169,520 kB
  • sloc: cpp: 180,479; ansic: 108,988; xml: 31,371; python: 1,223; php: 767; makefile: 145; sh: 101; javascript: 34
file content (406 lines) | stat: -rw-r--r-- 11,830 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
/**
	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;