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;