1 /*
2 Standard clonk controls
3 Author: Newton
4
5 This object provides handling of the clonk controls including item
6 management, backpack controls and standard throwing behaviour. It
7 should be included into any clonk/crew definition.
8 The controls in System.ocg/PlayerControl.c only provide basic movement
9 handling, namely the movement left, right, up and down. The rest is
10 handled here:
11 Grabbing, ungrabbing, shifting and pushing vehicles into buildings;
12 entering and exiting buildings; throwing, dropping; backpack control,
13 (object) menu control, hotkey controls, usage and it's callbacks and
14 forwards to script.
15
16 Objects that inherit this object need to return _inherited(...) in the
17 following callbacks (if defined):
18 Construction, Collection2, Ejection, RejectCollect, Departure,
19 Entrance, AttachTargetLost, CrewSelection, Death,
20 Destruction, OnActionChanged
21
22 The following callbacks are made to other objects:
23 *Stop
24 *Left, *Right, *Up, *Down
25 *Use, *UseStop, *UseStart, *UseHolding, *UseCancel
26 wheras * is 'Contained' if the clonk is contained and otherwise (riding,
27 pushing, to self) it is 'Control'. The item in the inventory only gets
28 the Use*-calls. If the callback is handled, you should return true.
29 Currently, this is explained more in detail here:
30 http://forum.openclonk.org/topic_show.pl?tid=337
31 */
32
33 // make use of other sub-libraries
34 #include Library_Inventory
35 #include Library_ClonkInventoryControl
36 #include Library_ClonkInteractionControl
37 #include Library_ClonkMenuControl
38 #include Library_ClonkUseControl
39 #include Library_ClonkGamepadControl
40
41 // used for interaction with objects
42 static const ACTIONTYPE_INVENTORY = 0;
43 static const ACTIONTYPE_VEHICLE = 1;
44 static const ACTIONTYPE_STRUCTURE = 2;
45 static const ACTIONTYPE_SCRIPT = 3;
46 static const ACTIONTYPE_EXTRA = 4;
47
48 // elevators within this range (x) can be called
49 static const ELEVATOR_CALL_DISTANCE = 30;
50
51 // default throwing angle used while the Clonk isn't aiming
52 static const DEFAULT_THROWING_ANGLE = 500;
53
54 /* ++++++++++++++++++++++++ Clonk Inventory Control ++++++++++++++++++++++++ */
55
56 /*
57 used properties
58 this.control.hotkeypressed: used to determine if an interaction has already been handled by a hotkey (space + 1-9)
59
60 this.control.alt: alternate usage by right mouse button
61 this.control.mlastx: last x position of the cursor
62 this.control.mlasty: last y position of the cursor
63 */
64
65
66 /* Item limit */
67 local MaxContentsCount = 5; // Size of the clonks inventory
68 local HandObjects = 1; // Amount of hands to select items
NoStackedContentMenu()69 public func NoStackedContentMenu() { return true; } // Contents-Menu shall display each object in a seperate slot
70
71
72 /* ################################################# */
73
Construction()74 protected func Construction()
75 {
76 if(this.control == nil)
77 this.control = {};
78 this.control.hotkeypressed = false;
79
80 this.control.alt = false;
81 return _inherited(...);
82 }
83
OnActionChanged(string oldaction)84 protected func OnActionChanged(string oldaction)
85 {
86 var old_act = this["ActMap"][oldaction];
87 var act = this["ActMap"][GetAction()];
88 var old_proc = 0;
89 if(old_act) old_proc = old_act["Procedure"];
90 var proc = 0;
91 if(act) proc = act["Procedure"];
92 // if the object's procedure has changed from a non Push/Attach
93 // to a Push/Attach action or the other way round, the usage needs
94 // to be cancelled
95 if (proc != old_proc)
96 {
97 if (proc == DFA_PUSH || proc == DFA_ATTACH
98 || old_proc == DFA_PUSH || old_proc == DFA_ATTACH)
99 {
100 CancelUse();
101 }
102 }
103 return _inherited(oldaction,...);
104 }
105
106 /** Returns additional interactions the clonk possesses as an array of function pointers.
107 Returned Proplist contains:
108 Fn = Name of the function to call
109 Object = Object to call the function in. Will also be displayed on the interaction-button
110 Description = A description of what the interaction does
111 IconID = ID of the definition that contains the icon (like GetInteractionMetaInfo)
112 IconName = Name of the graphic for the icon (like GetInteractionMetaInfo)
113 Priority = Where to sort in in the interaction-list. 0=front, 10=after script, 20=after vehicles, 30=after structures, nil means no preference
114 */
GetExtraInteractions()115 public func GetExtraInteractions()
116 {
117 var functions = _inherited(...) ?? [];
118
119 // flipping construction-preview
120 var effect;
121 if(effect = GetEffect("ControlConstructionPreview", this))
122 {
123 if(effect.flipable)
124 PushBack(functions, {Fn = "Flip", Description=ConstructionPreviewer->GetFlipDescription(), Object=effect.preview, IconID=ConstructionPreviewer_IconFlip, Priority=0});
125 }
126 // call elevator cases
127 var elevators = FindObjects(Find_ID(ElevatorCase), Find_InRect(-ELEVATOR_CALL_DISTANCE, AbsY(0), ELEVATOR_CALL_DISTANCE * 2, GetY() + AbsY(LandscapeHeight())), Find_Func("Ready", this));
128 for (var elevator in elevators)
129 PushBack(functions, { Fn = "CallCase", Object=elevator, Description=elevator->GetCallDescription(), Priority=0 });
130 return functions;
131 }
132
133 /* +++++++++++++++++++++++++++ Clonk Control +++++++++++++++++++++++++++ */
134
135 /* Main control function */
ObjectControl(int plr,int ctrl,int x,int y,int strength,bool repeat,int status)136 public func ObjectControl(int plr, int ctrl, int x, int y, int strength, bool repeat, int status)
137 {
138 if (!this)
139 return false;
140
141 // Contents menu
142 if (ctrl == CON_Contents && status == CONS_Down)
143 {
144 // Close any menu if open.
145 if (GetMenu())
146 {
147 var is_content = GetMenu()->~IsContentMenu();
148 // unclosable menu? bad luck
149 if (!this->~TryCancelMenu()) return true;
150 // If contents menu, don't open new one and return.
151 if (is_content)
152 return true;
153 }
154 // Open contents menu.
155 CancelUse();
156 GUI_ObjectInteractionMenu->CreateFor(this);
157 // the interaction menu calls SetMenu(this) in the clonk
158 // so after this call menu = the created menu
159 if(GetMenu())
160 GetMenu()->~Show();
161 return true;
162 }
163
164 /* aiming with mouse:
165 The CON_Aim control is transformed into a use command. Con_Use if
166 repeated does not bear the updated x,y coordinates, that's why this
167 other control needs to be issued and transformed. CON_Aim is a
168 control which is issued on mouse move but disabled when not aiming
169 or when HoldingEnabled() of the used item does not return true.
170 For the rest of the control code, it looks like the x,y coordinates
171 came from CON_Use.
172 */
173 if (GetUsedObject() && ctrl == CON_Aim)
174 {
175 if (this.control.alt) ctrl = CON_UseAlt;
176 else ctrl = CON_Use;
177
178 repeat = true;
179 status = CONS_Down;
180 }
181 // controls except a few reset a previously given command
182 else if (status != CONS_Moved)
183 SetCommand("None");
184
185 /* aiming with analog pad or keys:
186 This works completely different. There are CON_AimAxis* and CON_Aim*,
187 both work about the same. A virtual cursor is created which moves in a
188 circle around the clonk and is controlled via these CON_Aim* functions.
189 CON_Aim* is normally on the same buttons as the movement and has a
190 higher priority, thus is called first. The aim is always done, even if
191 the clonk is not aiming. However this returns only true (=handled) if
192 the clonk is really aiming. So if the clonk is not aiming, the virtual
193 cursor aims into the direction in which the clonk is running and e.g.
194 CON_Left is still called afterwards. So if the clonk finally starts to
195 aim, the virtual cursor already aims into the direction in which he ran
196 */
197 if (ctrl == CON_AimAxisUp || ctrl == CON_AimAxisDown || ctrl == CON_AimAxisLeft || ctrl == CON_AimAxisRight)
198 {
199 var success = VirtualCursor()->Aim(ctrl,this,strength,repeat,status);
200 // in any case, CON_Aim* is called but it is only successful if the virtual cursor is aiming
201 return success && VirtualCursor()->IsAiming();
202 }
203
204 // Simulate a mouse cursor for gamepads.
205 if (HasVirtualCursor())
206 {
207 x = this.control.mlastx;
208 y = this.control.mlasty;
209 }
210
211 // save last mouse position:
212 // if the using has to be canceled, no information about the current x,y
213 // is available. Thus, the last x,y position needs to be saved
214 else if (ctrl == CON_Use || ctrl == CON_UseAlt)
215 {
216 this.control.mlastx = x;
217 this.control.mlasty = y;
218 }
219
220 var proc = GetProcedure();
221
222 // building, vehicle, mount, contents, menu control
223 var house = Contained();
224 var vehicle = GetActionTarget();
225 // the clonk can have an action target even though he lost his action.
226 // That's why the clonk may only interact with a vehicle if in an
227 // appropiate procedure:
228 if (proc != "ATTACH" && proc != "PUSH")
229 vehicle = nil;
230
231 // menu
232 if (this.control.menu)
233 {
234 return Control2Menu(ctrl, x,y,strength, repeat, status);
235 }
236
237 var contents = this->GetHandItem(0);
238
239 // usage
240 var use = (ctrl == CON_Use || ctrl == CON_UseAlt);
241 if (use)
242 {
243 if (house)
244 {
245 return ControlUse2Script(ctrl, x, y, strength, repeat, status, house);
246 }
247 // control to grabbed vehicle
248 else if (vehicle && proc == "PUSH")
249 {
250 return ControlUse2Script(ctrl, x, y, strength, repeat, status, vehicle);
251 }
252 else if (vehicle && proc == "ATTACH")
253 {
254 /* objects to which clonks are attached (like horses, mechs,...) have
255 a special handling:
256 Use controls are, forwarded to the
257 horse but if the control is considered unhandled (return false) on
258 the start of the usage, the control is forwarded further to the
259 item. If the item then returns true on the call, that item is
260 regarded as the used item for the subsequent ControlUse* calls.
261 BUT the horse always gets the ControlUse*-calls that'd go to the used
262 item, too and before it so it can decide at any time to cancel its
263 usage via CancelUse().
264 */
265
266 if (ControlUse2Script(ctrl, x, y, strength, repeat, status, vehicle))
267 return true;
268 else
269 {
270 // handled if the horse is the used object
271 // ("using" is set to the object in StartUseControl - when the
272 // object returns true on that callback. Exactly what we want)
273 if (GetUsedObject() == vehicle) return true;
274 // has been cancelled (it is not the start of the usage but no object is used)
275 // if (vehicle && !GetUsedObject() && (repeat || status == CONS_Up)) return true;
276 }
277 }
278 // releasing the use-key always cancels shelved commands (in that case no GetUsedObject() exists)
279 if(status == CONS_Up) StopShelvedCommand();
280 // Release commands are always forwarded even if contents is 0, in case we
281 // need to cancel use of an object that left inventory
282 if (contents || (status == CONS_Up && GetUsedObject()))
283 {
284 if (ControlUse2Script(ctrl, x, y, strength, repeat, status, contents))
285 return true;
286 }
287 }
288
289 // A click on throw can also just abort usage without having any other effects.
290 // todo: figure out if wise.
291 var currently_in_use = GetUsedObject() != nil;
292 if (ctrl == CON_Throw && currently_in_use && status == CONS_Down)
293 {
294 CancelUse();
295 return true;
296 }
297
298 // Throwing and dropping
299 // only if not in house, not grabbing a vehicle and an item selected
300 // only act on press, not release
301 if (ctrl == CON_Throw && !house && (!vehicle || proc == "ATTACH" || proc == "PUSH") && status == CONS_Down)
302 {
303 if (contents)
304 {
305 // Object overloaded throw control?
306 // Call this before QueryRejectDeparture to allow alternate use of non-droppable objects
307 if (contents->~ControlThrow(this, x, y))
308 return true;
309
310 // The object does not want to be dropped? Still handle command.
311 if (contents->~QueryRejectDeparture(this))
312 return true;
313
314 // Quick-stash into grabbed vehicle?
315 if (vehicle && proc == "PUSH" && vehicle->~IsContainer())
316 {
317 CancelUse();
318 vehicle->Collect(contents);
319 if (!contents || contents->Contained() != this)
320 Sound("Hits::SoftTouch*", false, nil, GetOwner());
321 return true;
322 }
323
324 // just drop in certain situations
325 var only_drop = proc == "SCALE" || proc == "HANGLE" || proc == "SWIM";
326 // also drop if no throw would be possible anyway
327 if (only_drop || Distance(0, 0, x, y) < 10 || (Abs(x) < 10 && y > 10))
328 only_drop = true;
329 // throw
330 CancelUse();
331
332 if (only_drop)
333 return ObjectCommand("Drop", contents);
334 else
335 {
336 if (HasVirtualCursor() && !VirtualCursor()->IsActive())
337 {
338 var angle = DEFAULT_THROWING_ANGLE * (GetDir()*2 - 1);
339 x = +Sin(angle, CURSOR_Radius, 10);
340 y = -Cos(angle, CURSOR_Radius, 10);
341 }
342 return ObjectCommand("Throw", contents, x, y);
343 }
344 }
345 }
346
347 // Movement controls (defined in PlayerControl.c, partly overloaded here)
348 if (ctrl == CON_Left || ctrl == CON_Right || ctrl == CON_Up || ctrl == CON_Down || ctrl == CON_Jump)
349 {
350 // forward to script...
351 if (house)
352 {
353 return ControlMovement2Script(ctrl, x, y, strength, repeat, status, house);
354 }
355 else if (vehicle)
356 {
357 if (ControlMovement2Script(ctrl, x, y, strength, repeat, status, vehicle)) return true;
358 }
359
360 return ObjectControlMovement(plr, ctrl, strength, status);
361 }
362
363 // Do a roll on landing or when standing. This means that the CON_Down was not handled previously.
364 if (ctrl == CON_Roll && ComDir2XY(GetComDir())[0] != 0)
365 {
366 if (this->IsWalking())
367 {
368 if (this->Stuck())
369 {
370 // Still show some visual feedback for the player.
371 this->DoKneel();
372 }
373 else
374 {
375 this->DoRoll();
376 }
377 return true;
378 }
379 }
380
381 // Fall through half-solid mask
382 if (ctrl == CON_FallThrough)
383 {
384 if(status == CONS_Down)
385 {
386 if (this->IsWalking())
387 {
388 HalfVehicleFadeJumpStart();
389 }
390 }
391 else
392 {
393 HalfVehicleFadeJumpStop();
394 }
395 return true;
396 }
397
398 // hotkeys action bar hotkeys
399 var hot = 0;
400 if (ctrl == CON_InteractionHotkey0) hot = 10;
401 if (ctrl == CON_InteractionHotkey1) hot = 1;
402 if (ctrl == CON_InteractionHotkey2) hot = 2;
403 if (ctrl == CON_InteractionHotkey3) hot = 3;
404 if (ctrl == CON_InteractionHotkey4) hot = 4;
405 if (ctrl == CON_InteractionHotkey5) hot = 5;
406 if (ctrl == CON_InteractionHotkey6) hot = 6;
407 if (ctrl == CON_InteractionHotkey7) hot = 7;
408 if (ctrl == CON_InteractionHotkey8) hot = 8;
409 if (ctrl == CON_InteractionHotkey9) hot = 9;
410
411 if (hot > 0)
412 {
413 this.control.hotkeypressed = true;
414 this->~ControlHotkey(hot-1);
415 this->~StopInteractionCheck(); // for GUI_Controller_ActionBar
416 return true;
417 }
418
419 // Unhandled control
420 return _inherited(plr, ctrl, x, y, strength, repeat, status, ...);
421 }
422
423 // A wrapper to SetCommand to catch special behaviour for some actions.
ObjectCommand(string command,object target,int tx,int ty,object target2,data)424 public func ObjectCommand(string command, object target, int tx, int ty, object target2, /*any*/ data)
425 {
426 // special control for throw and jump
427 // but only with controls, not with general commands
428 if (command == "Throw")
429 return this->~ControlThrow(target, tx, ty);
430 else if (command == "Jump")
431 return this->~ControlJump();
432 // else standard command
433 else
434 {
435 // Make sure to not recollect the item immediately on drops.
436 if (command == "Drop")
437 {
438 // Disable collection for a moment.
439 if (target)
440 this->OnDropped(target);
441 }
442 return SetCommand(command, target, tx, ty, target2, data);
443 }
444 // this function might be obsolete: a normal SetCommand does make a callback to
445 // script before it is executed: ControlCommand(szCommand, pTarget, iTx, iTy)
446 }
447
448 /*
449 Called by the engine before a command is executed.
450 Beware that this is NOT called when SetCommand was called by a script.
451 At this point I am not sure whether we need this callback at all.
452 */
ControlCommand(string command,object target,int tx,int ty)453 public func ControlCommand(string command, object target, int tx, int ty)
454 {
455 if (command == "Drop")
456 {
457 // Disable collection for a moment.
458 if (target) this->OnDropped(target);
459 }
460 return _inherited(command, target, tx, ty, ...);
461 }
462
463 /* ++++++++++++++++++++++++ Movement Controls ++++++++++++++++++++++++ */
464
465 // Control use redirected to script
ControlMovement2Script(int ctrl,int x,int y,int strength,bool repeat,int status,object obj)466 func ControlMovement2Script(int ctrl, int x, int y, int strength, bool repeat, int status, object obj)
467 {
468 // overloads of movement commandos
469 if (ctrl == CON_Left || ctrl == CON_Right || ctrl == CON_Down || ctrl == CON_Up || ctrl == CON_Jump)
470 {
471 var control_string = "Control";
472 if (Contained() == obj)
473 control_string = "Contained";
474
475 if (status == CONS_Up)
476 {
477 // if any movement key has been released, ControlStop is called
478 if (obj->Call(Format("~%sStop", control_string), this, ctrl))
479 return true;
480 }
481 else
482 {
483 // what about gamepad-deadzone?
484 if (strength != nil && strength < CON_Gamepad_Deadzone)
485 return true;
486
487 // Control*
488 if (ctrl == CON_Left) if (obj->Call(Format("~%sLeft",control_string),this)) return true;
489 if (ctrl == CON_Right) if (obj->Call(Format("~%sRight",control_string),this)) return true;
490 if (ctrl == CON_Up) if (obj->Call(Format("~%sUp",control_string),this)) return true;
491 if (ctrl == CON_Down) if (obj->Call(Format("~%sDown",control_string),this)) return true;
492
493 // for attached (e.g. horse: also Jump command
494 if (GetProcedure() == "ATTACH")
495 if (ctrl == CON_Jump) if (obj->Call("ControlJump",this)) return true;
496 }
497 }
498
499 }
500
501 // Effect to free/unfree hands by disabling/enabling scale and hangle procedures
FxIntControlFreeHandsStart(object target,proplist fx,int temp)502 public func FxIntControlFreeHandsStart(object target, proplist fx, int temp)
503 {
504 // Process on non-temp as well in case scale/handle effects need to stack
505 // Stop current action
506 var proc = GetProcedure();
507 if (proc == "SCALE" || proc == "HANGLE") SetAction("Walk");
508 // Make sure ActMap is writable
509 if (this.ActMap == this.Prototype.ActMap) this.ActMap = new this.ActMap{};
510 // Kill scale/hangle effects
511 fx.act_scale = this.ActMap.Scale;
512 this.ActMap.Scale = nil;
513 fx.act_hangle = this.ActMap.Hangle;
514 this.ActMap.Hangle = nil;
515 return FX_OK;
516 }
517
FxIntControlFreeHandsStop(object target,proplist fx,int reason,bool temp)518 public func FxIntControlFreeHandsStop(object target, proplist fx, int reason, bool temp)
519 {
520 // Restore scale/hangle effects (engine will handle re-grabbing walls if needed)
521 if (fx.act_scale) this.ActMap.Scale = fx.act_scale;
522 if (fx.act_hangle) this.ActMap.Hangle = fx.act_hangle;
523 return FX_OK;
524 }
525
526 // returns true if the clonk is able to enter a building (procedurewise)
CanEnter()527 public func CanEnter()
528 {
529 var proc = GetProcedure();
530 if (proc != "WALK" && proc != "SWIM" && proc != "SCALE" &&
531 proc != "HANGLE" && proc != "FLOAT" && proc != "FLIGHT" &&
532 proc != "PUSH") return false;
533 return true;
534 }
535
IsMounted()536 public func IsMounted() { return GetProcedure() == "ATTACH"; }
537
538 /*-- Throwing --*/
539
540 // Throwing
DoThrow(object obj,int angle)541 func DoThrow(object obj, int angle)
542 {
543 // parameters...
544 var iX, iY, iR, iXDir, iYDir, iRDir;
545 iX = 4; if (!GetDir()) iX = -iX;
546 iY = Cos(angle,-4);
547 iR = Random(360);
548 iRDir = RandomX(-10,10);
549
550 iXDir = Sin(angle,this.ThrowSpeed);
551 iYDir = Cos(angle,-this.ThrowSpeed);
552 // throw boost (throws stronger upwards than downwards)
553 if (iYDir < 0) iYDir = iYDir * 13/10;
554 if (iYDir > 0) iYDir = iYDir * 8/10;
555
556 // add own velocity
557 iXDir += GetXDir(100)/2;
558 iYDir += GetYDir(100)/2;
559
560 // throw
561 obj->Exit(iX, iY, iR, 0, 0, iRDir);
562 obj->SetXDir(iXDir,100);
563 obj->SetYDir(iYDir,100);
564
565 // Prevent hitting the thrower.
566 var block_blow = AddEffect("BlockBlowControl", this, 100, 3, this);
567 block_blow.obj = obj;
568 return true;
569 }
570
571 // custom throw
572 // implemented in Clonk.ocd/Animations.ocd
ControlThrow()573 public func ControlThrow() { return _inherited(...); }
574
575 // Effect for blocking a blow by an object.
FxBlockBlowControlTimer()576 public func FxBlockBlowControlTimer()
577 {
578 return FX_Execute_Kill;
579 }
580
FxBlockBlowControlQueryCatchBlow(object target,effect fx,object obj)581 public func FxBlockBlowControlQueryCatchBlow(object target, effect fx, object obj)
582 {
583 if (obj == fx.obj)
584 return true;
585 return false;
586 }
587
588 /*-- Jumping --*/
589
590
591 /*
592 Triggers a regular jump, that means that the speed in y direction
593 is automatically decided, depending on the action of the clonk.
594
595 If you want to execute a jump with a certain speed, use ControlJumpExecute().
596 */
ControlJump()597 public func ControlJump()
598 {
599 var ydir = 0;
600
601 if (GetProcedure() == "WALK")
602 {
603 ydir = this.JumpSpeed;
604 }
605
606 if (InLiquid() && !GBackSemiSolid(0, -5))
607 {
608 ydir = BoundBy(this.JumpSpeed * 3 / 5, 240, 380);
609 }
610
611 // Jump speed of the wall kick is halved.
612 if (GetProcedure() == "SCALE" || GetAction() == "Climb")
613 {
614 ydir = this.JumpSpeed / 2;
615 }
616
617 return ControlJumpExecute(ydir);
618 }
619
620
621 /*
622 Additional function for actually triggering a jump directly.
623
624 The parameter ydir can be decided directly by the user,
625 or you can use the clonk's jump speed by passing this.JumpSpeed
626
627 Returns false if the jump was not successful.
628 */
ControlJumpExecute(int ydir)629 public func ControlJumpExecute(int ydir)
630 {
631 if (ydir && !Stuck())
632 {
633 SetPosition(GetX(), GetY() - 1);
634
635 // Wall kick if scaling or climbing.
636 if (GetProcedure() == "SCALE" || GetAction() == "Climb")
637 {
638 AddEffect("WallKick", this, 1);
639 var xdir;
640 if(GetDir() == DIR_Right)
641 {
642 xdir = -1;
643 SetDir(DIR_Left);
644 }
645 else if(GetDir() == DIR_Left)
646 {
647 xdir = 1;
648 SetDir(DIR_Right);
649 }
650
651 SetYDir(-ydir * GetCon(), 100 * 100);
652 SetXDir(xdir * 17);
653 // Set speed first to have proper animations when jump starts.
654 SetAction("Jump");
655 return true;
656 }
657 //Normal jump
658 else
659 {
660 SetYDir(-ydir * GetCon(), 100 * 100);
661 // Set speed first to have proper animations when jump starts.
662 SetAction("Jump");
663 return true;
664 }
665 }
666 return false;
667 }
668
669
670 // Interaction with clonks is special:
671 // * The clonk opening the menu should always have higher priority so the clonk is predictably selected on the left side even if standing behind e.g. a crate
672 // * Other clonks should be behind because interaction with them is rare but having your fellow players stand in front of a building is very common
673 // (Allies also tend to run in front just when you opened that menu...)
GetInteractionPriority(object target)674 func GetInteractionPriority(object target)
675 {
676 // Self with high priority
677 if (target == this) return 100;
678 // Dead Clonks are shown (for a death message e.g.) but sorted to the bottom.
679 if (!GetAlive()) return -190;
680 var owner = NO_OWNER;
681 if (target) owner = target->GetOwner();
682 // Prefer own clonks for item transfer
683 if (owner == GetOwner()) return -100;
684 // If no own clonk, prefer friendly
685 if (!Hostile(owner, GetOwner())) return -120;
686 // Hostile clonks? Lowest priority.
687 return -200;
688 }
689