1 /**
2 	Airplane
3 	Acrobatic air-vehicle. Capable of firing lead shot.
4 
5 	@author: Ringwaul, Clonkonaut, Maikel
6 */
7 
8 // Airplane is destructible.
9 #include Library_Destructible
10 
11 local dir = DIR_Left;
12 
13 local prop_speed, prop_speed_target, prop_speed_timer; // current and target propeller speed [0, 100]
14 
15 local pilot;
16 local clonkmesh;
17 
18 // What happens on mouse left
19 local main_action = "Cannon"; // default: fire bullets
20 
21 // All available click actions
22 local action_list = ["Cannon", "Rocket", "Bomb", "Spray", "Balloon", "Drop", "Afterburner"];
23 
24 
25 /*-- Engine Callbacks --*/
26 
Construction()27 public func Construction()
28 {
29 	SetR(-90);
30 }
31 
Initialize()32 public func Initialize()
33 {
34 	CreateEffect(FxControlPlaneFlight, 1, 1);
35 	SetAction("Land");
36 }
37 
RejectCollect(id def,object obj)38 public func RejectCollect(id def, object obj)
39 {
40 	var contents_count = ObjectCount(Find_Container(this), Find_Not(Find_OCF(OCF_CrewMember)));
41 	if (contents_count >= MaxContentsCount)
42 		return true;
43 	return false;
44 }
45 
ActivateEntrance(object clonk)46 public func ActivateEntrance(object clonk)
47 {
48 	if (clonk->Contained() == this)
49 		return clonk->Exit();
50 
51 	// Clonks cannot get into the plane if it is underwater
52 	if(GBackLiquid()) return false;
53 
54 	var passengers = ObjectCount(Find_Container(this), Find_OCF(OCF_CrewMember));
55 	if (passengers >= MaxPassengerCount) return false;
56 
57 	clonk->Enter(this);
58 	clonk->SetAction("Walk");
59 	clonk->PlayAnimation("Drive", CLONK_ANIM_SLOT_Movement, Anim_Const(10), Anim_Const(1000));
60 	ShowMainActionTo(clonk);
61 }
62 
Ejection(object obj)63 public func Ejection(object obj)
64 {
65 	if (pilot && obj == pilot)
66 		PlaneDismount(obj);
67 	if (obj->Contained())
68 		Exit();
69 	obj->SetSpeed(this->GetXDir(), this->GetYDir());
70 }
71 
72 
73 /*-- Callbacks --*/
74 
IsContainer()75 public func IsContainer() { return true; }
76 
IsProjectileTarget(object projectile,object shooter)77 public func IsProjectileTarget(object projectile, object shooter) { return true; }
78 
79 
80 /*-- Interface --*/
81 
82 //Quick command for scenario designers. The plane starts facing right instead of left.
FaceRight()83 public func FaceRight()
84 {
85 	SetR(90);
86 	RollPlane(1, true);
87 }
88 
FaceLeft()89 public func FaceLeft()
90 {
91 	SetR(-90);
92 	RollPlane(0, true);
93 }
94 
95 
96 /*-- Interaction --*/
97 
98 // Provides an own interaction menu.
HasInteractionMenu()99 public func HasInteractionMenu() { return true; }
100 
101 // Show settings in interaction menu
GetInteractionMenus(object clonk)102 public func GetInteractionMenus(object clonk)
103 {
104 	var menus = _inherited(clonk, ...) ?? [];
105 
106 	// Do not show the menu if flying and if the clonk is not the pilot.
107 	if (GetAction() == "Fly" && clonk != pilot)
108 		return menus;
109 
110 	var plane_menu =
111 	{
112 		title = "$AirplaneSettings$",
113 		entries_callback = this.GetAirplaneMenuEntries,
114 		callback = nil,
115 		callback_hover = "OnSettingsHover",
116 		callback_target = this,
117 		BackgroundColor = RGB(0, 0, 50),
118 		Priority = 20
119 	};
120 	PushBack(menus, plane_menu);
121 
122 	return menus;
123 }
124 
GetAirplaneMenuEntries(object clonk)125 public func GetAirplaneMenuEntries(object clonk)
126 {
127 	var control_prototype =
128 	{
129 		Bottom = "2em",
130 		BackgroundColor = { Std = 0, Hover = RGB(100, 30, 30) },
131 		OnMouseOut = GuiAction_SetTag("Std"),
132 		OnMouseIn = GuiAction_SetTag("Hover"),
133 		text = { Left = "2em" },
134 		icon = { Right = "2em" }
135 	};
136 
137 	var menu_entries = [];
138 
139 	// main action (= left click)
140 	var main_action_settings = {
141 		Bottom = "2em",
142 		settings = {
143 			Prototype = control_prototype,
144 			Priority = 1000,
145 			Tooltip = GetActionTypeTooltip(main_action),
146 			OnClick = GuiAction_Call(this, "ToggleMainAction", clonk),
147 			text =
148 				{ Prototype = control_prototype.text,
149 				  Style = GUI_TextVCenter,
150 				  Text = Format("$SwitchMainAction$\n%s", GetPlayerControlAssignment(clonk->GetOwner(), CON_Use, true, true), GetActionTypeName(main_action))
151 				},
152 			icon =
153 				{ Prototype = control_prototype.icon,
154 				  Symbol = GetActionTypeInfo(main_action)
155 				}
156 		}
157 	};
158 
159 	PushBack(menu_entries, { symbol = this, extra_data = "MainAction", custom = main_action_settings });
160 
161 	return menu_entries;
162 }
163 
OnSettingsHover(symbol,extra_data,desc_menu_target,menu_id)164 public func OnSettingsHover(symbol, extra_data, desc_menu_target, menu_id)
165 {
166 	if (symbol == nil) return;
167 
168 	var text = "";
169 	if (extra_data == "MainAction")
170 	{
171 		if (main_action == "Cannon")
172 			text = Format("$BulletInfo$", GetBulletAmount());
173 		if (main_action == "Rocket")
174 			text = Format("$RocketInfo$", GetRocketAmount());
175 		if (main_action == "Bomb")
176 			text = Format("$BombInfo$", GetBombAmount());
177 		if (main_action == "Spray")
178 			text = Format("$SprayInfo$", GetBarrelAmount());
179 		if (main_action == "Balloon")
180 			text = Format("$BalloonInfo$", GetBalloonAmount());
181 		if (main_action == "Drop")
182 			text = Format("$DropInfo$");
183 		if (main_action == "Afterburner")
184 			text = Format("$AfterburnerInfo$");
185 		if (text == "")
186 			text = "$Description$";
187 	}
188 
189 	GuiUpdate({ Text = text }, menu_id, 1, desc_menu_target);
190 }
191 
GetActionTypeTooltip(string action)192 private func GetActionTypeTooltip(string action)
193 {
194 	// Show next firing mode
195 	var position = GetIndexOf(action_list, action);
196 	if (position == -1) return ""; // ??
197 
198 	if (position == GetLength(action_list)-1)
199 		position = 0;
200 	else
201 		position++;
202 
203 	return Format("$SwitchTo$", GetActionTypeName(action_list[position]));
204 }
205 
GetActionTypeName(string action)206 private func GetActionTypeName(string action)
207 {
208 	if (action == "Cannon")
209 		return "$Bullet$";
210 	if (action == "Rocket")
211 		return "$Rocket$";
212 	if (action == "Bomb")
213 		return "$Bomb$";
214 	if (action == "Spray")
215 		return "$Spray$";
216 	if (action == "Balloon")
217 		return "$Balloon$";
218 	if (action == "Drop")
219 		return "$Drop$";
220 	if (action == "Afterburner")
221 		return "$Afterburner$";
222 }
223 
GetActionTypeInfo(string action)224 private func GetActionTypeInfo(string action)
225 {
226 	if (action == "Cannon")
227 		return LeadBullet;
228 	if (action == "Rocket")
229 		return Boompack;
230 	if (action == "Bomb")
231 		return IronBomb;
232 	if (action == "Spray")
233 		return Barrel;
234 	if (action == "Balloon")
235 		return Balloon;
236 	if (action == "Drop")
237 		return Icon_LetGo;
238 	if (action == "Afterburner")
239 		return Icon_Flame;
240 }
241 
ToggleMainAction(object clonk)242 public func ToggleMainAction(object clonk)
243 {
244 	var current_action = GetIndexOf(action_list, main_action);
245 	if (current_action == -1)
246 		return; // ??
247 
248 	if (current_action == GetLength(action_list)-1)
249 		current_action = 0;
250 	else
251 		current_action++;
252 
253 	main_action = action_list[current_action];
254 
255 	UpdateInteractionMenus(this.GetAirplaneMenuEntries);
256 
257 	if (clonk)
258 	{
259 		Sound("UI::Click2", false, nil, clonk->GetOwner());
260 		ShowMainActionTo(clonk);
261 	}
262 }
263 
264 
265 /*-- Usage --*/
266 
267 // Main action
ContainedUseStart(object clonk,int x,int y)268 public func ContainedUseStart(object clonk, int x, int y)
269 {
270 	var call = Format("Use%s", main_action);
271 	return this->Call(call, clonk, x, y);
272 }
273 
ContainedUseStop(object clonk,int x,int y)274 public func ContainedUseStop(object clonk, int x, int y)
275 {
276 	var call = Format("Cancel%s", main_action);
277 	return this->Call(call, clonk, x, y);
278 }
279 
ContainedUseCancel(object clonk,int x,int y)280 public func ContainedUseCancel(object clonk, int x, int y)
281 {
282 	var call = Format("Cancel%s", main_action);
283 	return this->Call(call, clonk, x, y);
284 }
285 
286 // Alt Use: toggle firing mode
ContainedUseAltStart(object clonk,int x,int y)287 public func ContainedUseAltStart(object clonk, int x, int y)
288 {
289 	if (clonk != pilot)
290 		return false;
291 
292 	ToggleMainAction(clonk);
293 	return true;
294 }
295 
ContainedUseAltStop(object clonk,int x,int y)296 public func ContainedUseAltStop(object clonk, int x, int y)
297 {
298 	if (clonk != pilot)
299 		return false;
300 
301 	return true;
302 }
303 
ContainedUseAltCancel(object clonk,int x,int y)304 public func ContainedUseAltCancel(object clonk, int x, int y)
305 {
306 	if (clonk != pilot)
307 		return false;
308 
309 	return true;
310 }
311 
312 // Starting the plane.
ContainedUp(object clonk)313 public func ContainedUp(object clonk)
314 {
315 	if (pilot)
316 	{
317 		// For safety, check if the pilot is dead (which is never particularly good).
318 		// During flight, pilot's health is constantly checked by the flying effect.
319 		if (!pilot->GetAlive())
320 			PlaneDismount(pilot);
321 		else if (clonk != pilot)
322 			return false;
323 	}
324 	// Start engine.
325 	if (clonk && GetAction() == "Land")
326 	{
327 		if (!pilot)
328 			PlaneMount(clonk);
329 		StartFlight(15);
330 		return true;
331 	}
332 	return false;
333 }
334 
335 // Stopping the plane.
ContainedDown(object clonk)336 public func ContainedDown(object clonk)
337 {
338 	// Shut down engine.
339 	if (GetAction() == "Fly" && clonk == pilot)
340 	{
341 		CancelFlight();
342 		return true;
343 	}
344 	if (pilot)
345 	{
346 		if (!pilot->GetAlive())
347 			PlaneDismount(pilot);
348 		else if (clonk != pilot)
349 			return false;
350 	}
351 	// Allow reverse pilot.
352 	if (clonk && GetAction() == "Land")
353 	{
354 		if (!pilot)
355 			PlaneMount(clonk);
356 		StartFlight(-5);
357 		return true;
358 	}
359 	return false;
360 }
361 
362 // Steering.
ContainedLeft(object clonk)363 public func ContainedLeft(object clonk)
364 {
365 	if (clonk != pilot)
366 		return false;
367 	var ctrl_fx = GetPlaneControl();
368 	if (ctrl_fx)
369 		ctrl_fx->SetRotationDir(-1);
370 	return true;
371 }
372 
ContainedRight(object clonk)373 public func ContainedRight(object clonk)
374 {
375 	if (clonk != pilot)
376 		return false;
377 	var ctrl_fx = GetPlaneControl();
378 	if (ctrl_fx)
379 		ctrl_fx->SetRotationDir(1);
380 	return true;
381 }
382 
ContainedStop(object clonk)383 public func ContainedStop(object clonk)
384 {
385 	if (clonk != pilot)
386 		return false;
387 	var ctrl_fx = GetPlaneControl();
388 	if (ctrl_fx)
389 		ctrl_fx->SetRotationDir(0);
390 	return true;
391 }
392 
393 
394 /*-- Control --*/
395 
GetFacingDir()396 public func GetFacingDir()
397 {
398 	return dir;
399 }
400 
ControlLeft(object clonk)401 public func ControlLeft(object clonk)
402 {
403 	if (Inside(GetR(), 80, 100) && dir == DIR_Right)
404 		FaceLeft();
405 	return false;
406 }
407 
ControlRight(object clonk)408 public func ControlRight(object clonk)
409 {
410 	if (Inside(GetR(), -100, -80) && dir == DIR_Left)
411 		FaceRight();
412 	return false;
413 }
414 
415 
416 /*-- Bullet firing --*/
417 
UseCannon(object clonk,int x,int y)418 public func UseCannon(object clonk, int x, int y)
419 {
420 	if (clonk != pilot)
421 		return false;
422 
423 	if (!GetBulletAmount())
424 	{
425 		CustomMessage("$NoShots$", this, clonk->GetOwner());
426 		Sound("Objects::Weapons::Blunderbuss::Click?");
427 		return true;
428 	}
429 	AddEffect("FireBullets", this, 100, 12, this);
430 	return true;
431 }
432 
CancelCannon(object clonk,int x,int y)433 public func CancelCannon(object clonk, int x, int y)
434 {
435 	if (clonk != pilot)
436 		return false;
437 
438 	if (GetEffect("FireBullets", this))
439 		RemoveEffect("FireBullets", this);
440 
441 	return true;
442 }
443 
GetBulletAmount()444 public func GetBulletAmount()
445 {
446 	var shots = 0;
447 	var bullets = FindObjects(Find_Container(this), Find_Func("IsBullet"));
448 	for (var bullet in bullets)
449 	{
450 		if (bullet->~GetStackCount())
451 			shots += bullet->GetStackCount();
452 		else
453 			shots++;
454 	}
455 	return shots;
456 }
457 
FxFireBulletsStart(object target,proplist effect,int temp)458 public func FxFireBulletsStart(object target, proplist effect, int temp)
459 {
460 	if (temp)
461 		return FX_OK;
462 	effect.reticle = CreateObject(GUI_Reticle);
463 	effect.reticle->SetOwner(this->GetController());
464 	effect.reticle->SetAction("Show", this);
465 	var ammo = FindObject(Find_Container(this), Find_Func("IsBullet"));
466 	if (!ammo)
467 		return FX_Execute_Kill;
468 	FireBullet(ammo);
469 	return FX_OK;
470 }
471 
FxFireBulletsTimer(object target,proplist effect,int time)472 public func FxFireBulletsTimer(object target, proplist effect, int time)
473 {
474 	var ammo = FindObject(Find_Container(this), Find_Func("IsBullet"));
475 	if (!ammo)
476 		return FX_Execute_Kill;
477 	FireBullet(ammo);
478 	return FX_OK;
479 }
480 
FxFireBulletsStop(object target,proplist effect,int reason,bool temp)481 public func FxFireBulletsStop(object target, proplist effect, int reason, bool temp)
482 {
483 	if (temp)
484 		return FX_OK;
485 	if (effect.reticle)
486 		effect.reticle->RemoveObject();
487 	return FX_OK;
488 }
489 
FireBullet(object ammo)490 public func FireBullet(object ammo)
491 {
492 	var shot = ammo->TakeObject();
493 	var angle = this->GetR();
494 	shot->Launch(this, angle, 35, 200);
495 	Sound("Objects::Weapons::Blunderbuss::GunShoot?");
496 
497 	// Muzzle Flash & gun smoke
498 	var IX = Sin(GetR(), 30);
499 	var IY = -Cos(GetR(), 30);
500 
501 	var x = Sin(angle, 20);
502 	var y = -Cos(angle, 20);
503 	CreateParticle("Smoke", IX, IY, PV_Random(x - 20, x + 20), PV_Random(y - 20, y + 20), PV_Random(40, 60), Particles_Smoke(), 20);
504 
505 	CreateMuzzleFlash(IX, IY, angle, 20);
506 	CreateParticle("Flash", 0, 0, GetXDir(), GetYDir(), 8, Particles_Flash());
507 }
508 
509 
510 /*-- Rocket firing --*/
511 
UseRocket(object clonk,int x,int y)512 public func UseRocket(object clonk, int x, int y)
513 {
514 	if (clonk != pilot)
515 		return false;
516 
517 	if (!GetRocketAmount())
518 	{
519 		CustomMessage("$NoRockets$", this, clonk->GetOwner());
520 		Sound("Objects::Weapons::Blunderbuss::Click?");
521 	}
522 
523 	return true;
524 }
525 
CancelRocket(object clonk,int x,int y)526 public func CancelRocket(object clonk, int x, int y)
527 {
528 	if (clonk != pilot)
529 		return false;
530 
531 	var rocket = FindObject(Find_Container(this), Find_ID(Boompack));
532 	if (!rocket)
533 		return true;
534 	FireRocket(rocket, x, y);
535 
536 	return true;
537 }
538 
GetRocketAmount()539 public func GetRocketAmount()
540 {
541 	return ContentsCount(Boompack);
542 }
543 
FireRocket(object rocket,int x,int y)544 public func FireRocket(object rocket, int x, int y)
545 {
546 	var launch_x = Cos(GetR() - 180 * (1 - dir), 10);
547 	var launch_y = Sin(GetR() - 180 * (1 - dir), 10);
548 	rocket->Exit(launch_x, launch_y, GetR(), GetXDir(), GetYDir());
549 	rocket->Launch(GetR(), nil, this);
550 	var fx = AddEffect("IntControlRocket", rocket, 100, 1, this);
551 	fx.x = GetX() + x;
552 	fx.y = GetY() + y;
553 	rocket->SetDirectionDeviation(0);
554 }
555 
FxIntControlRocketTimer(object target,effect fx,int time)556 public func FxIntControlRocketTimer(object target, effect fx, int time)
557 {
558 	// Remove gravity on rocket.
559 	target->SetYDir(target->GetYDir(100) - GetGravity(), 100);
560 	// Adjust angle to target.
561 	var angle_to_target = Angle(target->GetX(), target->GetY(), fx.x, fx.y);
562 	var angle_rocket = target->GetR();
563 	if (angle_rocket < 0)
564 		angle_rocket += 360;
565 	var angle_delta = angle_rocket - angle_to_target;
566 	if (Inside(angle_delta, -3, 3))
567 		return FX_OK;
568 	if (Inside(angle_delta, 0, 180) || Inside(angle_delta, -360, -180))
569 		target->SetR(target->GetR() - 5);
570 	else if (Inside(angle_delta, -180, 0) || Inside(angle_delta, 180, 360))
571 		target->SetR(target->GetR() + 5);
572 	return FX_OK;
573 }
574 
575 
576 /*-- Bombing --*/
577 
UseBomb(object clonk,int x,int y)578 public func UseBomb(object clonk, int x, int y)
579 {
580 	if (clonk != pilot)
581 		return false;
582 
583 	if (!GetBombAmount())
584 	{
585 		CustomMessage("$NoBombs$", this, clonk->GetOwner());
586 		Sound("Objects::Weapons::Blunderbuss::Click?");
587 	}
588 
589 	return true;
590 }
591 
CancelBomb(object clonk,int x,int y)592 public func CancelBomb(object clonk, int x, int y)
593 {
594 	if (clonk != pilot)
595 		return false;
596 
597 	var bomb = FindObject(Find_Container(this), Find_ID(IronBomb));
598 	if (!bomb)
599 		bomb = FindObject(Find_Container(this), Find_ID(Dynamite));
600 	if (!bomb)
601 	{
602 		bomb = FindObject(Find_Container(this), Find_ID(DynamiteBox), Find_Func("GetDynamiteCount"));
603 		if (bomb)
604 			bomb = bomb->Contents();
605 	}
606 	if (!bomb)
607 		return true;
608 
609 	DropBomb(bomb);
610 
611 	return true;
612 }
613 
GetBombAmount()614 public func GetBombAmount()
615 {
616 	var bombs = ObjectCount(Find_Container(this), Find_Or(Find_ID(IronBomb), Find_ID(Dynamite)));
617 	var boxes = FindObjects(Find_Container(this), Find_ID(DynamiteBox));
618 	for (var box in boxes)
619 		bombs += box->GetDynamiteCount();
620 	return bombs;
621 }
622 
DropBomb(object bomb)623 private func DropBomb(object bomb)
624 {
625 	if (!bomb) return;
626 
627 	bomb->Exit();
628 	bomb->SetPosition(GetX(), GetY() + 12);
629 	bomb->SetXDir(GetXDir());
630 	bomb->SetYDir(GetYDir());
631 	bomb->Fuse(true); // fuse and explode on hit
632 }
633 
634 
635 /*-- Liquid spray --*/
636 
UseSpray(object clonk,int x,int y)637 public func UseSpray(object clonk, int x, int y)
638 {
639 	if (clonk != pilot)
640 		return false;
641 
642 	if (!GetBarrelAmount())
643 	{
644 		CustomMessage("$NoBarrels$", this, clonk->GetOwner());
645 		Sound("Objects::Weapons::Blunderbuss::Click?");
646 	}
647 
648 	return true;
649 }
650 
CancelSpray(object clonk,int x,int y)651 public func CancelSpray(object clonk, int x, int y)
652 {
653 	if (clonk != pilot)
654 		return false;
655 
656 	var barrels = FindObjects(Find_Container(this), Find_Or(Find_ID(Barrel), Find_ID(MetalBarrel)));
657 	var barrel;
658 	for (var b in barrels)
659 		if (GetLength(b->GetLiquidContents()))
660 		{
661 			barrel = b;
662 			break;
663 		}
664 
665 	if (!barrel) return true;
666 
667 	SprayLiquid(barrel, clonk);
668 
669 	return true;
670 }
671 
GetBarrelAmount()672 public func GetBarrelAmount()
673 {
674 	var barrels = FindObjects(Find_Container(this), Find_Or(Find_ID(Barrel), Find_ID(MetalBarrel)));
675 	var count = 0;
676 	for (var barrel in barrels)
677 		if (GetLength(barrel->GetLiquidContents()))
678 			count++;
679 	return count;
680 }
681 
SprayLiquid(object barrel,object clonk)682 private func SprayLiquid(object barrel, object clonk)
683 {
684 	if (!barrel) return;
685 
686 	var added_r = -60;
687 	if (dir == DIR_Right)
688 		added_r = 60;
689 	barrel->EmptyBarrel(GetR() + added_r, 50, clonk);
690 	Sound("Liquids::Splash1");
691 }
692 
693 
694 /*-- Parachuting --*/
695 
UseBalloon(object clonk,int x,int y)696 public func UseBalloon(object clonk, int x, int y)
697 {
698 	if (!GetBalloonAmount())
699 	{
700 		CustomMessage("$NoBalloons$", this, clonk->GetOwner());
701 		Sound("Objects::Weapons::Blunderbuss::Click?");
702 	}
703 
704 	return true;
705 }
706 
CancelBalloon(object clonk,int x,int y)707 public func CancelBalloon(object clonk, int x, int y)
708 {
709 	if (!clonk->Contained()) return false;
710 
711 	var balloon = FindObject(Find_Container(this), Find_ID(Balloon));
712 	if (!balloon)
713 		return true;
714 
715 	Parachute(balloon, x, y, clonk);
716 
717 	return true;
718 }
719 
GetBalloonAmount()720 public func GetBalloonAmount()
721 {
722 	return ContentsCount(Balloon);
723 }
724 
Parachute(object balloon,int x,int y,object clonk)725 private func Parachute(object balloon, int x, int y, object clonk)
726 {
727 	if (!balloon || !clonk) return;
728 
729 	// The balloon has to enter the clonk
730 	if (!balloon->Enter(clonk))
731 	{
732 		// Maybe the clonk should just drop an object?
733 		return CustomMessage("$NoSpaceInInventory$", this, clonk->GetOwner());
734 	}
735 	clonk->Exit();
736 	balloon->ControlUseStart(clonk);
737 }
738 
739 
740 /*-- Object dropping --*/
741 
UseDrop(object clonk,int x,int y)742 public func UseDrop(object clonk, int x, int y)
743 {
744 	if (clonk != pilot)
745 		return false;
746 
747 	if (!GetLength(GetNonClonkContents()))
748 	{
749 		CustomMessage("$NoObjects$", this, clonk->GetOwner());
750 		Sound("Objects::Weapons::Blunderbuss::Click?");
751 	}
752 
753 	return true;
754 }
755 
CancelDrop(object clonk,int x,int y)756 public func CancelDrop(object clonk, int x, int y)
757 {
758 	if (clonk != pilot)
759 		return false;
760 
761 	var object = GetNonClonkContents()[0];
762 	if (!object)
763 		return true;
764 
765 	DropObject(object, x, y, clonk);
766 
767 	return true;
768 }
769 
GetNonClonkContents()770 public func GetNonClonkContents()
771 {
772 	return FindObjects(Find_Container(this), Find_Not(Find_OCF(OCF_CrewMember)));
773 }
774 
DropObject(object to_drop,int x,int y,object clonk)775 private func DropObject(object to_drop, int x, int y, object clonk)
776 {
777 	if (!to_drop) return;
778 
779 	to_drop->Exit();
780 	to_drop->SetXDir(GetXDir());
781 	to_drop->SetYDir(GetYDir());
782 
783 	if (clonk)
784 		to_drop->SetController(clonk->GetOwner());
785 }
786 
787 
788 /*-- Afterburner --*/
789 
UseAfterburner(object clonk,int x,int y)790 public func UseAfterburner(object clonk, int x, int y)
791 {
792 	if (clonk != pilot)
793 		return false;
794 
795 	if (!GetOilBarrel())
796 	{
797 		CustomMessage("$NoOil$", this, clonk->GetOwner());
798 		Sound("Objects::Weapons::Blunderbuss::Click?");
799 		return true;
800 	}
801 	CreateEffect(FxAfterburner, 100, 2);
802 	return true;
803 }
804 
CancelAfterburner(object clonk,int x,int y)805 public func CancelAfterburner(object clonk, int x, int y)
806 {
807 	if (clonk != pilot)
808 		return false;
809 
810 	var fx = GetEffect("FxAfterburner", this);
811 	if (fx)
812 		fx->Remove();
813 	return true;
814 }
815 
GetOilBarrel()816 public func GetOilBarrel()
817 {
818 	var barrels = FindObjects(Find_Container(this), Find_Or(Find_ID(Barrel), Find_ID(MetalBarrel)));
819 	for (var barrel in barrels)
820 		if (barrel->GetLiquidAmount("Oil") > 0)
821 			return barrel;
822 	return;
823 }
824 
IsUsingAfterburner()825 public func IsUsingAfterburner()
826 {
827 	return !!GetEffect("FxAfterburner", this);
828 }
829 
830 local FxAfterburner = new Effect
831 {
func()832 	Construction = func()
833 	{
834 		Target->Sound("Objects::Boompack::Fly", 0, 40, nil, 1);
835 	},
836 	Timer = func(int time)
837 	{
838 		var barrel = Target->GetOilBarrel();
839 		if (!barrel)
840 			return FX_Execute_Kill;
841 		if (barrel->GetLiquidAmount("Oil") <= 0)
842 			return FX_Execute_Kill;
843 		barrel->RemoveLiquid("Oil", 1);
844 		return FX_OK;
845 	},
846 	Destruction = func()
847 	{
848 		Target->Sound("Objects::Boompack::Fly", 0, 40, nil, -1);
849 	}
850 };
851 
852 
853 /*-- Movement --*/
854 
StartFlight(int new_throttle)855 public func StartFlight(int new_throttle)
856 {
857 	SetPropellerSpeedTarget(100);
858 	SetAction("Fly");
859 	var ctrl_fx = GetPlaneControl();
860 	if (ctrl_fx)
861 		ctrl_fx.throttle = new_throttle;
862 	return;
863 }
864 
StartInstantFlight(int angle,int new_throttle)865 public func StartInstantFlight(int angle, int new_throttle)
866 {
867 	if (angle < 0)
868 		angle += 360;
869 	if (angle < 180)
870 		angle -= 10;
871 	else
872 		angle += 10;
873 	SetPropellerSpeed(100);
874 	SetAction("Fly");
875 	var ctrl_fx = GetPlaneControl();
876 	if (ctrl_fx)
877 	{
878 		ctrl_fx.throttle = new_throttle;
879 		ctrl_fx.thrust = new_throttle;
880 	}
881 	SetR(angle);
882 	SetXDir(Sin(angle, new_throttle));
883 	SetYDir(-Cos(angle, new_throttle));
884 	return;
885 }
886 
CancelFlight()887 public func CancelFlight()
888 {
889 	SetPropellerSpeedTarget(0);
890 	SetAction("Land");
891 	var ctrl_fx = GetPlaneControl();
892 	if (ctrl_fx)
893 	{
894 		ctrl_fx.rdir = 0;
895 		ctrl_fx.throttle = 0;
896 	}
897 	return;
898 }
899 
GetPlaneControl()900 public func GetPlaneControl()
901 {
902 	return GetEffect("FxControlPlaneFlight", this);
903 }
904 
RemovePlaneControl()905 public func RemovePlaneControl()
906 {
907 	var ctrl_fx = GetPlaneControl();
908 	if (ctrl_fx)
909 		ctrl_fx->Remove();
910 	return;
911 }
912 
913 // Effect that controls the plane movement and related stuff.
914 local FxControlPlaneFlight = new Effect
915 {
func()916 	Construction = func()
917 	{
918 		this.thrust = 0;
919 		this.throttle = 0;
920 		this.lift = 0;
921 		this.rdir = 0;
922 		this.propanim = Target->PlayAnimation("Propellor", 15, Anim_Const(0));
923 	},
924 	Destruction = func()
925 	{
926 		Target->StopAnimation(this.propanim);
927 	},
928 	SetRotationDir = func(int new_dir)
929 	{
930 		this.rdir = new_dir;
931 	},
932 	Timer = func(int time)
933 	{
934 		// Perform flying actions.
935 		if (Target->GetAction() == "Fly")
936 		{
937 			this->DoPlaneRotation();
938 			this->DoEngineSmoke();
939 			this->StirUpLiquids();
940 		}
941 		// Perform landing actions.
942 		else
943 		{
944 			this->LandPlane();
945 		}
946 		// Perform plane movements.
947 		this->ApplyThrust(time);
948 		this->ApplyDrag();
949 		this->PropellorAnim();
950 		this->PerformSafetyChecks();
951 		return FX_OK;
952 	},
953 	DoPlaneRotation = func()
954 	{
955 		// Perform plane rotation.
956 		var max_rdir = 7;
957 		if (Target->IsUsingAfterburner())
958 			max_rdir = 9;
959 		var current_rdir = Target->GetRDir();
960 		var new_rdir = current_rdir + 3 * this.rdir;
961 		if (this.rdir == 0)
962 			new_rdir = 0;
963 		new_rdir = BoundBy(new_rdir, -max_rdir, max_rdir);
964 		Target->SetRDir(new_rdir);
965 		// Roll plane to movement direction.
966 		if (this.throttle > 0)
967 		{
968 			if (Target->GetXDir() > 10 && Target->GetFacingDir() != DIR_Right)
969 				Target->RollPlane(1);
970 			if (Target->GetXDir() < -10 && Target->GetFacingDir() != DIR_Left)
971 				Target->RollPlane(0);
972 		}
973 	},
974 	// Perform engine smoke.
975 	DoEngineSmoke = func()
976 	{
977 		var colour = 255 - (Target->GetDamage() * 3);
978 		var min = PV_Random(7, 20);
979 		var max = PV_Random(40, 70);
980 		var rot = Target->GetR();
981 		if (Target->GetDamage() >= Target.HitPoints / 2)
982 		{
983 			min = PV_Random(20, 30);
984 			max = PV_Random(70, 100);
985 		}
986 		var particles =
987 		{
988 			Prototype = Particles_Smoke(),
989 			R = colour, G = colour, B = colour,
990 			Size = PV_Linear(min, max)
991 		};
992 		Target->CreateParticle("Smoke", -Sin(rot, 10), Cos(rot, 10), 0, 0, PV_Random(36, 2 * 36), particles, 1);
993 		// Add fire if after burner is turned on.
994 		if (Target->IsUsingAfterburner())
995 		{
996 			var particle_fire = Particles_Fire();
997 			particle_fire.Size = PV_KeyFrames(0, 0, PV_Random(6, 8), 500, 4, 1000, 0);
998 			Target->CreateParticle("Fire", -Sin(rot, 10), Cos(rot, 10), PV_Random(-1, 1), PV_Random(-1, 1), 20 + Random(10), particle_fire, 1);
999 		}
1000 	},
1001 	// Stirs up liquids if flying above a liquid.
1002 	StirUpLiquids = func()
1003 	{
1004 		if (Target->GBackLiquid(0, 40))
1005 		{
1006 			var surface = 40;
1007 			while (surface && Target->GBackSemiSolid(0, surface))
1008 				surface--;
1009 			if (surface > 0 && Target->GBackLiquid(0, surface + 1))
1010 			{
1011 				var phases = PV_Linear(0, 7);
1012 				if (Target->GetFacingDir() == DIR_Left)
1013 					phases = PV_Linear(8, 15);
1014 				var color = GetAverageTextureColor(Target->GetTexture(0, surface + 1));
1015 				var particles =
1016 				{
1017 					Size = 16,
1018 					Phase = phases,
1019 					CollisionVertex = 750,
1020 					OnCollision = PC_Die(),
1021 					R = (color >> 16) & 0xff,
1022 					G = (color >> 8) & 0xff,
1023 					B = (color >> 0) & 0xff,
1024 					Attach = ATTACH_Front,
1025 				};
1026 				Target->CreateParticle("Wave", 0, surface, (RandomX(-5, 5) - (-1 + 2 * Target->GetFacingDir()) * 6) / 4, 0, 16, particles);
1027 			}
1028 		}
1029 	},
1030 	// Lands the plane.
1031 	LandPlane = func()
1032 	{
1033 		// If under or on water, turn upright.
1034 		if (Target->GBackLiquid() || Target->GBackLiquid(0, 21))
1035 		{
1036 			if (Target->GetR() < 0)
1037 			{
1038 				if (!Inside(Target->GetR(), -95, -85))
1039 				{
1040 					Target->SetRDir((90 + Target->GetR()) / Abs(90 + Target->GetR()) * -3);
1041 					Target->RollPlane(0);
1042 				}
1043 			}
1044 			else if (!Inside(Target->GetR(), 85, 95))
1045 			{
1046 				Target->SetRDir((90 - Target->GetR()) / Abs(90 - Target->GetR()) * 3);
1047 				Target->RollPlane(1);
1048 			}
1049 			// Also: slow down because the plane tends to slide across water.
1050 			if (Target->GetXDir() > 0)
1051 				Target->SetXDir(Target->GetXDir() - 1);
1052 			else if (Target->GetXDir() < 0)
1053 				Target->SetXDir(Target->GetXDir() + 1);
1054 		}
1055 		// Decrease thrust rapidly.
1056 		if (this.thrust > 0)
1057 			this.thrust--;
1058 		if (this.thrust < 0)
1059 			this.thrust++;
1060 	},
1061 	ApplyThrust = func(int time)
1062 	{
1063 		// Determine airplane lift.
1064 		this.lift = Distance(0, 0, Target->GetXDir(), Target->GetYDir()) / 2;
1065 		if (this.lift > 20)
1066 			this.lift = 20;
1067 		if (this.throttle < 1)
1068 			this.lift = 0;
1069 		// Modify throttle if afterburner is being used.
1070 		var mod_throttle = this.throttle;
1071 		if (Target->IsUsingAfterburner())
1072 			mod_throttle = 3 * mod_throttle / 2;
1073 		// Throttle-to-thrust lag
1074 		if (time % 10 == 0)
1075 		{
1076 			if (mod_throttle > this.thrust)
1077 				++this.thrust;
1078 			if (mod_throttle < this.thrust)
1079 				--this.thrust;
1080 		}
1081 		// Set airplane velocity.
1082 		Target->SetXDir(Sin(Target->GetR(), this.thrust) + Target->GetXDir(100), 100);
1083 		Target->SetYDir(-Cos(Target->GetR(), this.thrust) + Target->GetYDir(100) - this.lift, 100);
1084 	},
1085 	// Applies drag to the airplane to ensure a maximum speed.
1086 	ApplyDrag = func()
1087 	{
1088 		var maxspeed = 40;
1089 		if (Target->IsUsingAfterburner())
1090 			maxspeed += 18;
1091 		var speed = Distance(0, 0, Target->GetXDir(), Target->GetYDir());
1092 		if (speed > maxspeed)
1093 		{
1094 			Target->SetXDir(Target->GetXDir(100) * maxspeed / speed, 100);
1095 			Target->SetYDir(Target->GetYDir(100) * maxspeed / speed, 100);
1096 		}
1097 	},
1098 	PropellorAnim = func()
1099 	{
1100 		var change = Target->GetAnimationPosition(this.propanim) + this.thrust * 3;
1101 		if (change > Target->GetAnimationLength("Propellor"))
1102 			change = (Target->GetAnimationPosition(this.propanim) + this.thrust * 3) - Target->GetAnimationLength("Propellor");
1103 		if (change < 0)
1104 			change = (Target->GetAnimationLength("Propellor") - this.thrust * 3);
1105 		Target->SetAnimationPosition(this.propanim, Anim_Const(change));
1106 	},
1107 	PerformSafetyChecks = func()
1108 	{
1109 		// Check for pilot.
1110 		if (!Target->GetPilot() && this.throttle != 0)
1111 			Target->CancelFlight();
1112 		if (Target->GetPilot() && !Target->GetPilot()->GetAlive())
1113 			Target->PlaneDismount(Target->GetPilot());
1114 
1115 		// Planes cannot fly underwater!
1116 		if (Target->GBackLiquid())
1117 		{
1118 			if (Target->GetPilot())
1119 				Target->Ejection(Target->GetPilot());
1120 			if (this.throttle != 0)
1121 				Target->CancelFlight();
1122 		}
1123 	}
1124 };
1125 
RollPlane(int rolldir,bool instant)1126 public func RollPlane(int rolldir, bool instant)
1127 {
1128 	if (dir != rolldir)
1129 	{
1130 		var i = 36;
1131 		if (instant)
1132 			i = 1;
1133 		PlayAnimation(Format("Roll%d",rolldir), 10, Anim_Linear(0, 0, GetAnimationLength(Format("Roll%d", rolldir)), i, ANIM_Hold));
1134 		dir = rolldir;
1135 	}
1136 }
1137 
1138 
1139 /*-- Piloting --*/
1140 
PlaneMount(object clonk)1141 public func PlaneMount(object clonk)
1142 {
1143 	pilot = clonk;
1144 	SetOwner(clonk->GetController());
1145 	clonk->PlayAnimation("Stand", 15, nil, Anim_Const(1000));
1146 	clonkmesh = AttachMesh(clonk, "pilot", "skeleton_body", Trans_Mul(Trans_Rotate(180, 1, 0, 0), Trans_Translate(-3000, 1000, 0)), AM_DrawBefore);
1147 	return true;
1148 }
1149 
PlaneDismount(object clonk)1150 public func PlaneDismount(object clonk)
1151 {
1152 	clonk->StopAnimation(clonk->GetRootAnimation(15));
1153 	DetachMesh(clonkmesh);
1154 	clonkmesh = nil;
1155 	// Ensure the current use is cancelled.
1156 	if (clonk == pilot)
1157 		ContainedUseCancel(pilot, 0, 0);
1158 	pilot = nil;
1159 	CancelFlight();
1160 	return true;
1161 }
1162 
GetPilot()1163 public func GetPilot()
1164 {
1165 	return pilot;
1166 }
1167 
1168 
1169 /*-- Effects --*/
1170 
1171 // Instantly set new propeller speed
SetPropellerSpeed(int new_speed)1172 public func SetPropellerSpeed(int new_speed)
1173 {
1174 	if (prop_speed_timer)
1175 	{
1176 		RemoveTimer(this.PropellerSpeedTimer);
1177 		prop_speed_timer = false;
1178 	}
1179 	return SetPropellerSound(prop_speed = prop_speed_target = new_speed);
1180 }
1181 
1182 // Schedule fading to new propeller speed
SetPropellerSpeedTarget(int new_speed_target)1183 public func SetPropellerSpeedTarget(int new_speed_target)
1184 {
1185 	prop_speed_target = new_speed_target;
1186 	if (!prop_speed_timer)
1187 		prop_speed_timer = AddTimer(this.PropellerSpeedTimer, 10);
1188 	return true;
1189 }
1190 
1191 // Execute fading to new propeller speed
PropellerSpeedTimer()1192 public func PropellerSpeedTimer()
1193 {
1194 	prop_speed = BoundBy(prop_speed_target, prop_speed - 10, prop_speed + 4);
1195 	if (prop_speed == prop_speed_target)
1196 	{
1197 		RemoveTimer(this.PropellerSpeedTimer);
1198 		prop_speed_timer = false;
1199 	}
1200 	return SetPropellerSound(prop_speed);
1201 }
1202 
1203 // Set propeller speed sound. 0 = off, 100 = max speed.
SetPropellerSound(int speed)1204 private func SetPropellerSound(int speed)
1205 {
1206 	if (speed <= 0)
1207 		return Sound("Objects::Plane::PropellerLoop", 0, 100, nil, -1);
1208 	else
1209 		return Sound("Objects::Plane::PropellerLoop", 0, 100, nil, 1, 0, (speed - 100) * 2 / 3);
1210 }
1211 
1212 
1213 /*-- Destruction --*/
1214 
1215 // Destroyed by any type of damage.
IsDestroyedByExplosions()1216 public func IsDestroyedByExplosions() { return false; }
1217 
1218 // Custom explosion on callback from destructible library.
OnDestruction(int change,int cause,int by_player)1219 public func OnDestruction(int change, int cause, int by_player)
1220 {
1221 	if (pilot)
1222 		PlaneDismount(pilot);
1223 	SetController(by_player);
1224 	PlaneDeath();
1225 	return true;
1226 }
1227 
PlaneDeath()1228 private func PlaneDeath()
1229 {
1230 	while (Contents(0))
1231 		Contents(0)->Exit();
1232 	Explode(36);
1233 }
1234 
1235 // Inflict damage when hitting something with the plane and already damaged.
Hit(int xdir,int ydir)1236 public func Hit(int xdir, int ydir)
1237 {
1238 	var remaining_hp = this.HitPoints - GetDamage();
1239 	if (remaining_hp < 10)
1240 	{
1241 		var speed = Distance(0, 0, xdir, ydir) / 10;
1242 		if (speed > 4 * remaining_hp)
1243 			DoDamage(speed / 6, FX_Call_DmgScript, GetController());
1244 	}
1245 }
1246 
1247 
1248 /*-- Production --*/
1249 
IsVehicle()1250 public func IsVehicle() { return true; }
IsShipyardProduct()1251 public func IsShipyardProduct() { return true; }
1252 
1253 
1254 /*-- Display --*/
1255 
ShowMainActionTo(object clonk)1256 public func ShowMainActionTo(object clonk)
1257 {
1258 	CustomMessage(Format("$SelectedAction$", GetActionTypeName(main_action), GetActionTypeInfo(main_action)), this, clonk->GetOwner());
1259 }
1260 
Definition(proplist def)1261 public func Definition(proplist def)
1262 {
1263 	def.MeshTransformation = Trans_Mul(Trans_Rotate(90, 0, 0, 1), Trans_Translate(-1000, -3375, 0), Trans_Rotate(25, 0, 1, 0));
1264 	def.PictureTransformation = Trans_Mul(Trans_Rotate(-5, 1, 0, 0), Trans_Rotate(40, 0, 1, 0), Trans_Translate(-20000, -4000, 20000));
1265 	return _inherited(def, ...);
1266 }
1267 
1268 
1269 /*-- Properties --*/
1270 
1271 local ActMap = {
1272 	Fly = {
1273 		Prototype = Action,
1274 		Name = "Fly",
1275 		Procedure = DFA_NONE,
1276 		Directions = 2,
1277 		FlipDir = 0,
1278 		Length = 10,
1279 		Delay = 1,
1280 		X = 0,
1281 		Y = 0,
1282 		Wdt = 40,
1283 		Hgt = 56,
1284 		NextAction = "Fly",
1285 	},
1286 	Land = {
1287 		Prototype = Action,
1288 		Name = "Land",
1289 		Procedure = DFA_NONE,
1290 		Directions = 2,
1291 		FlipDir = 0,
1292 		Length = 1,
1293 		Delay = 2,
1294 		X = 0,
1295 		Y = 0,
1296 		Wdt = 40,
1297 		Hgt = 56,
1298 		NextAction = "Land",
1299 	},
1300 };
1301 
1302 local Name="$Name$";
1303 local Description="$Description$";
1304 local BorderBound = C4D_Border_Sides | C4D_Border_Top | C4D_Border_Bottom;
1305 local HitPoints = 50;
1306 local MaxContentsCount = 20;
1307 local MaxPassengerCount = 3;
1308 local Components = {Metal = 6, Wood = 4};
1309 local Touchable = 1;