1/*
2 * LegacyClonk
3 *
4 * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design)
5 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
6 *
7 * Distributed under the terms of the ISC license; see accompanying file
8 * "COPYING" for details.
9 *
10 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
11 * See accompanying file "TRADEMARK" for details.
12 *
13 * To redistribute this file separately, substitute the full license texts
14 * for the above references.
15 */
16
17/* Object motion, collision, friction */
18
19#include <C4Include.h>
20#include <C4Object.h>
21
22#include <C4Physics.h>
23#include <C4SolidMask.h>
24#include <C4Wrappers.h>
25
26/* Some physical constants */
27
28const C4Fixed FRedirect = FIXED100(x: 50);
29const C4Fixed FFriction = FIXED100(x: 30);
30const C4Fixed FixFullCircle = itofix(x: 360), FixHalfCircle = FixFullCircle / 2;
31const C4Fixed FloatFriction = FIXED100(x: 2);
32const C4Fixed RotateAccel = FIXED100(x: 20);
33const C4Fixed FloatAccel = FIXED100(x: 10);
34const C4Fixed WalkAccel = FIXED100(x: 50), SwimAccel = FIXED100(x: 20);
35const C4Fixed HitSpeed1 = FIXED100(x: 150); // Hit Event
36const C4Fixed HitSpeed2 = itofix(x: 2); // Cross Check Hit
37const C4Fixed HitSpeed3 = itofix(x: 6); // Scale disable, kneel
38const C4Fixed HitSpeed4 = itofix(x: 8); // Flat
39
40/* Some helper functions */
41
42void RedirectForce(C4Fixed &from, C4Fixed &to, int32_t tdir)
43{
44 C4Fixed fred;
45 fred = (std::min)(a: Abs(val: from), b: FRedirect);
46 from -= fred * Sign(val: from);
47 to += fred * tdir;
48}
49
50void ApplyFriction(C4Fixed &tval, int32_t percent)
51{
52 C4Fixed ffric = FFriction * percent / 100;
53 if (tval > +ffric) { tval -= ffric; return; }
54 if (tval < -ffric) { tval += ffric; return; }
55 tval = 0;
56}
57
58// Compares all Shape.VtxContactCNAT[] CNAT flags to search flag.
59// Returns true if CNAT match has been found.
60
61bool ContactVtxCNAT(C4Object *cobj, uint8_t cnat_dir)
62{
63 int32_t cnt;
64 bool fcontact = false;
65 for (cnt = 0; cnt < cobj->Shape.VtxNum; cnt++)
66 if (cobj->Shape.VtxContactCNAT[cnt] & cnat_dir)
67 fcontact = true;
68 return fcontact;
69}
70
71// Finds first vertex with contact flag set.
72// Returns -1/0/+1 for relation on vertex to object center.
73
74int32_t ContactVtxWeight(C4Object *cobj)
75{
76 int32_t cnt;
77 for (cnt = 0; cnt < cobj->Shape.VtxNum; cnt++)
78 if (cobj->Shape.VtxContactCNAT[cnt])
79 {
80 if (cobj->Shape.VtxX[cnt] < 0) return -1;
81 if (cobj->Shape.VtxX[cnt] > 0) return +1;
82 }
83 return 0;
84}
85
86// ContactVtxFriction: Returns 0-100 friction value of first
87// contacted vertex;
88
89int32_t ContactVtxFriction(C4Object *cobj)
90{
91 int32_t cnt;
92 for (cnt = 0; cnt < cobj->Shape.VtxNum; cnt++)
93 if (cobj->Shape.VtxContactCNAT[cnt])
94 return cobj->Shape.VtxFriction[cnt];
95 return 0;
96}
97
98const char *CNATName(int32_t cnat)
99{
100 switch (cnat)
101 {
102 case CNAT_None: return "None";
103 case CNAT_Left: return "Left";
104 case CNAT_Right: return "Right";
105 case CNAT_Top: return "Top";
106 case CNAT_Bottom: return "Bottom";
107 case CNAT_Center: return "Center";
108 }
109 return "Undefined";
110}
111
112bool C4Object::Contact(int32_t iCNAT)
113{
114 if (Def->ContactFunctionCalls)
115 {
116 return static_cast<bool>(Call(szFunctionCall: std::format(PSF_Contact, args: CNATName(cnat: iCNAT)).c_str()));
117 }
118 return false;
119}
120
121void C4Object::DoMotion(int32_t mx, int32_t my)
122{
123 if (pSolidMaskData) pSolidMaskData->Remove(fCauseInstability: true, fBackupAttachment: true);
124 x += mx; y += my;
125 motion_x += mx; motion_y += my;
126}
127
128void C4Object::TargetBounds(int32_t &ctco, int32_t limit_low, int32_t limit_hi, int32_t cnat_low, int32_t cnat_hi)
129{
130 if (ctco < limit_low)
131 {
132 ctco = limit_low;
133
134 // stop
135 if (cnat_low == CNAT_Left)
136 {
137 xdir = 0;
138 }
139 else
140 {
141 ydir = 0;
142 }
143
144 // do calls
145 Contact(iCNAT: cnat_low);
146 }
147 if (ctco > limit_hi)
148 {
149 ctco = limit_hi;
150
151 // stop
152 if (cnat_hi == CNAT_Right)
153 {
154 xdir = 0;
155 }
156 else
157 {
158 ydir = 0;
159 }
160
161 // do calls
162 Contact(iCNAT: cnat_hi);
163 }
164}
165
166int32_t C4Object::ContactCheck(int32_t iAtX, int32_t iAtY)
167{
168 // Check shape contact at given position
169 Shape.ContactCheck(cx: iAtX, cy: iAtY);
170
171 // Store shape contact values in object t_contact
172 t_contact = Shape.ContactCNAT;
173
174 // Contact script call for the first contacted cnat
175 if (Shape.ContactCNAT)
176 for (int32_t ccnat = 0; ccnat < 4; ccnat++) // Left, right, top bottom
177 if (Shape.ContactCNAT & (1 << ccnat))
178 if (Contact(iCNAT: 1 << ccnat))
179 break; // Will stop on first positive return contact call!
180
181 // Return shape contact count
182 return Shape.ContactCount;
183}
184
185void C4Object::SideBounds(int32_t &ctcox)
186{
187 // layer bounds
188 if (pLayer) if (pLayer->Def->BorderBound & C4D_Border_Layer)
189 if (Action.Act <= ActIdle || Def->ActMap[Action.Act].Procedure != DFA_ATTACH)
190 if (Category & C4D_StaticBack)
191 TargetBounds(ctco&: ctcox, limit_low: pLayer->x + pLayer->Shape.x, limit_hi: pLayer->x + pLayer->Shape.x + pLayer->Shape.Wdt, cnat_low: CNAT_Left, cnat_hi: CNAT_Right);
192 else
193 TargetBounds(ctco&: ctcox, limit_low: pLayer->x + pLayer->Shape.x - Shape.x, limit_hi: pLayer->x + pLayer->Shape.x + pLayer->Shape.Wdt + Shape.x, cnat_low: CNAT_Left, cnat_hi: CNAT_Right);
194 // landscape bounds
195 if (Def->BorderBound & C4D_Border_Sides)
196 TargetBounds(ctco&: ctcox, limit_low: 0 - Shape.x, GBackWdt + Shape.x, cnat_low: CNAT_Left, cnat_hi: CNAT_Right);
197}
198
199void C4Object::VerticalBounds(int32_t &ctcoy)
200{
201 // layer bounds
202 if (pLayer) if (pLayer->Def->BorderBound & C4D_Border_Layer)
203 if (Action.Act <= ActIdle || Def->ActMap[Action.Act].Procedure != DFA_ATTACH)
204 if (Category & C4D_StaticBack)
205 TargetBounds(ctco&: ctcoy, limit_low: pLayer->y + pLayer->Shape.y, limit_hi: pLayer->y + pLayer->Shape.y + pLayer->Shape.Hgt, cnat_low: CNAT_Top, cnat_hi: CNAT_Bottom);
206 else
207 TargetBounds(ctco&: ctcoy, limit_low: pLayer->y + pLayer->Shape.y - Shape.y, limit_hi: pLayer->y + pLayer->Shape.y + pLayer->Shape.Hgt + Shape.y, cnat_low: CNAT_Top, cnat_hi: CNAT_Bottom);
208 // landscape bounds
209 if (Def->BorderBound & C4D_Border_Top)
210 TargetBounds(ctco&: ctcoy, limit_low: 0 - Shape.y, limit_hi: +1000000, cnat_low: CNAT_Top, cnat_hi: CNAT_Bottom);
211 if (Def->BorderBound & C4D_Border_Bottom)
212 TargetBounds(ctco&: ctcoy, limit_low: -1000000, GBackHgt + Shape.y, cnat_low: CNAT_Top, cnat_hi: CNAT_Bottom);
213}
214
215void C4Object::DoMovement()
216{
217 int32_t ctcox, ctcoy, ctcor, ctx, cty, iContact = 0;
218 bool fAnyContact = false; uint32_t iContacts = 0;
219 bool fTurned = false, fRedirectYR = false, fNoAttach = false;
220
221 // Reset motion for this frame
222 motion_x = motion_y = 0;
223
224 // Restrictions
225 if (Def->NoHorizontalMove) xdir = 0;
226
227 // Dig free target area
228 if (Action.Act > ActIdle)
229 if (Def->ActMap[Action.Act].DigFree)
230 {
231 // Shape size square
232 if (Def->ActMap[Action.Act].DigFree == 1)
233 {
234 ctcox = fixtoi(x: fix_x + xdir); ctcoy = fixtoi(x: fix_y + ydir);
235 Game.Landscape.DigFreeRect(tx: ctcox + Shape.x, ty: ctcoy + Shape.y, wdt: Shape.Wdt, hgt: Shape.Hgt, fRequest: Action.Data, pByObj: this);
236 }
237 // Free size round (variable size)
238 else
239 {
240 ctcox = fixtoi(x: fix_x + xdir); ctcoy = fixtoi(x: fix_y + ydir);
241 int32_t rad = Def->ActMap[Action.Act].DigFree;
242 if (Con < FullCon) rad = rad * 6 * Con / 5 / FullCon;
243 Game.Landscape.DigFree(tx: ctcox, ty: ctcoy - 1, rad, fRequest: Action.Data, pByObj: this);
244 }
245 }
246
247 // store previous position
248 int32_t ix0 = x; int32_t iy0 = y;
249
250 // store previous movement and ocf
251 C4Fixed oldxdir(xdir), oldydir(ydir);
252 uint32_t old_ocf = OCF;
253
254 if (!Action.t_attach) // Unattached movement
255
256 {
257 // Horizontal movement
258
259 // Movement target
260 fix_x += xdir; ctcox = fixtoi(x: fix_x);
261
262 // Movement bounds (horiz)
263 SideBounds(ctcox);
264
265 // Move to target
266 while (x != ctcox)
267 {
268 // Next step
269 ctx = x + Sign(val: ctcox - x);
270
271 if (iContact = ContactCheck(iAtX: ctx, iAtY: y))
272 {
273 fAnyContact = true; iContacts |= t_contact;
274 // Abort horizontal movement
275 ctcox = x; fix_x = itofix(x);
276 // Vertical redirection (always)
277 RedirectForce(from&: xdir, to&: ydir, tdir: -1);
278 ApplyFriction(tval&: ydir, percent: ContactVtxFriction(cobj: this));
279 }
280 else // Free horizontal movement
281 DoMotion(mx: ctx - x, my: 0);
282 }
283
284 // Vertical movement
285
286 // Movement target
287 fix_y += ydir; ctcoy = fixtoi(x: fix_y);
288
289 // Movement bounds (vertical)
290 VerticalBounds(ctcoy);
291
292 // Move to target
293 while (y != ctcoy)
294 {
295 // Next step
296 cty = y + Sign(val: ctcoy - y);
297 if (iContact = ContactCheck(iAtX: x, iAtY: cty))
298 {
299 fAnyContact = true; iContacts |= t_contact;
300 ctcoy = y; fix_y = itofix(x: y);
301 // Vertical contact horizontal friction
302 ApplyFriction(tval&: xdir, percent: ContactVtxFriction(cobj: this));
303 // Redirection slide or rotate
304 if (!ContactVtxCNAT(cobj: this, cnat_dir: CNAT_Left))
305 RedirectForce(from&: ydir, to&: xdir, tdir: -1);
306 else if (!ContactVtxCNAT(cobj: this, cnat_dir: CNAT_Right))
307 RedirectForce(from&: ydir, to&: xdir, tdir: +1);
308 else
309 {
310 // living things are always capable of keeping their rotation
311 if (OCF & OCF_Rotate) if (iContact == 1) if (!Alive)
312 {
313 RedirectForce(from&: ydir, to&: rdir, tdir: -ContactVtxWeight(cobj: this));
314 fRedirectYR = true;
315 }
316 ydir = 0;
317 }
318 }
319 else // Free vertical movement
320 DoMotion(mx: 0, my: cty - y);
321 }
322 }
323
324 if (Action.t_attach) // Attached movement
325 {
326 uint8_t at_xovr, at_yovr;
327
328 // Set target position by momentum
329 fix_x += xdir; fix_y += ydir;
330
331 // Movement target
332 ctcox = fixtoi(x: fix_x); ctcoy = fixtoi(x: fix_y);
333
334 // Movement bounds (horiz + verti)
335 SideBounds(ctcox); VerticalBounds(ctcoy);
336
337 // Move to target
338 do
339 {
340 at_xovr = at_yovr = 0; // ctco Attachment override flags
341
342 // Set next step target
343 ctx = x + Sign(val: ctcox - x); cty = y + Sign(val: ctcoy - y);
344
345 // Attachment check
346 if (!Shape.Attach(cx&: ctx, cy&: cty, cnat_pos: Action.t_attach))
347 fNoAttach = true;
348 else
349 {
350 // Attachment change to ctx/cty overrides ctco target
351 if (cty != y + Sign(val: ctcoy - y)) at_yovr = 1;
352 if (ctx != x + Sign(val: ctcox - x)) at_xovr = 1;
353 }
354
355 // Contact check & evaluation
356 if (iContact = ContactCheck(iAtX: ctx, iAtY: cty))
357 {
358 fAnyContact = true; iContacts |= t_contact;
359 // Abort movement
360 ctcox = x; fix_x = itofix(x);
361 ctcoy = y; fix_y = itofix(x: y);
362 }
363 else // Continue free movement
364 DoMotion(mx: ctx - x, my: cty - y);
365
366 // ctco Attachment overrides
367 if (at_xovr) { ctcox = x; xdir = Fix0; fix_x = itofix(x); }
368 if (at_yovr) { ctcoy = y; ydir = Fix0; fix_y = itofix(x: y); }
369 } while ((x != ctcox) || (y != ctcoy));
370 }
371
372 // Rotation
373 if (OCF & OCF_Rotate && !!rdir)
374 {
375 // Set target
376 fix_r += rdir * 5;
377 // Rotation limit
378 if (Def->Rotateable > 1)
379 {
380 if (fix_r > itofix(x: Def->Rotateable))
381 {
382 fix_r = itofix(x: Def->Rotateable); rdir = 0;
383 }
384 if (fix_r < itofix(x: -Def->Rotateable))
385 {
386 fix_r = itofix(x: -Def->Rotateable); rdir = 0;
387 }
388 }
389 ctcor = fixtoi(x: fix_r);
390 ctx = x; cty = y;
391 // Move to target
392 while (r != ctcor)
393 {
394 // Save step undos
395 int32_t lcobjr = r; C4Shape lshape = Shape;
396 // Try next step
397 r += Sign(val: ctcor - r);
398 UpdateShape();
399 // attached rotation: rotate around attachment pos
400 if (Action.t_attach && !fNoAttach)
401 {
402 // more accurately, attachment should be evaluated by a rotation around the attachment vertex
403 // however, as long as this code is only used for some surfaces adjustment for large vehicles,
404 // it's enough to assume rotation around the center
405 ctx = x; cty = y;
406 // evaulate attachment, but do not bother about attachment loss
407 // that will then be done in next execution cycle
408 Shape.Attach(cx&: ctx, cy&: cty, cnat_pos: Action.t_attach);
409 }
410 // check for contact
411 if (iContact = ContactCheck(iAtX: ctx, iAtY: cty)) // Contact
412 {
413 fAnyContact = true; iContacts |= t_contact;
414 // Undo step and abort movement
415 Shape = lshape;
416 r = lcobjr;
417 ctcor = r;
418 fix_r = itofix(x: r);
419 // last UpdateShape-call might have changed sector lists!
420 UpdatePos();
421 // Redirect to y
422 if (iContact == 1) if (!fRedirectYR)
423 RedirectForce(from&: rdir, to&: ydir, tdir: -1);
424 // Stop rotation
425 rdir = 0;
426 }
427 else
428 {
429 fTurned = true;
430 x = ctx; y = cty;
431 }
432 }
433 // Circle bounds
434 if (fix_r < -FixHalfCircle) { fix_r += FixFullCircle; r = fixtoi(x: fix_r); }
435 if (fix_r > +FixHalfCircle) { fix_r -= FixFullCircle; r = fixtoi(x: fix_r); }
436 }
437
438 // Reput solid mask: Might have been removed by motion or
439 // motion might be out of date from last frame.
440 UpdateSolidMask(fRestoreAttachedObjects: true);
441
442 // Misc checks
443
444 // InLiquid check
445 // this equals C4Object::UpdateLiquid, but the "fNoAttach=false;"-line
446 if (IsInLiquidCheck()) // In Liquid
447 {
448 if (!InLiquid) // Enter liquid
449 {
450 if (OCF & OCF_HitSpeed2) if (Mass > 3)
451 Splash(tx: x, ty: y + 1, amt: (std::min)(a: Shape.Wdt * Shape.Hgt / 10, b: 20), pByObj: this);
452 fNoAttach = false;
453 InLiquid = 1;
454 }
455 }
456 else // Out of liquid
457 {
458 if (InLiquid) // Leave liquid
459 InLiquid = 0;
460 }
461
462 // Contact Action
463 if (fAnyContact)
464 {
465 t_contact = iContacts;
466 ContactAction();
467 }
468 // Attachment Loss Action
469 if (fNoAttach)
470 NoAttachAction();
471 // Movement Script Execution
472 if (fAnyContact)
473 {
474 C4AulParSet pars(C4VInt(iVal: fixtoi(x: oldxdir, prec: 100)), C4VInt(iVal: fixtoi(x: oldydir, prec: 100)));
475 if (old_ocf & OCF_HitSpeed1) Call(PSF_Hit, pPars: pars);
476 if (old_ocf & OCF_HitSpeed2) Call(PSF_Hit2, pPars: pars);
477 if (old_ocf & OCF_HitSpeed3) Call(PSF_Hit3, pPars: pars);
478 }
479
480 // Rotation gfx
481 if (fTurned)
482 UpdateFace(bUpdateShape: true);
483 else
484 // pos changed?
485 if ((ix0 - x) | (iy0 - y)) UpdatePos();
486}
487
488void C4Object::Stabilize()
489{
490 // def allows stabilization?
491 if (Def->NoStabilize) return;
492 // normalize angle
493 int32_t nr = r; while (nr < -180) nr += 360; while (nr > 180) nr -= 360;
494 if (nr != 0)
495 if (Inside<int32_t>(ival: nr, lbound: -StableRange, rbound: +StableRange))
496 {
497 // Save step undos
498 int32_t lcobjr = r;
499 C4Shape lshape = Shape;
500 // Try rotation
501 r = 0;
502 UpdateShape();
503 if (ContactCheck(iAtX: x, iAtY: y))
504 {
505 // Undo rotation
506 Shape = lshape;
507 r = lcobjr;
508 }
509 else
510 {
511 // Stabilization okay
512 fix_r = itofix(x: r);
513 UpdateFace(bUpdateShape: true);
514 }
515 }
516}
517
518void C4Object::CopyMotion(C4Object *from)
519{
520 // Designed for contained objects, no static
521 if (x != from->x || y != from->y)
522 {
523 x = from->x; y = from->y;
524 // Resort into sectors
525 UpdatePos();
526 }
527 fix_x = itofix(x); fix_y = itofix(x: y);
528 xdir = from->xdir; ydir = from->ydir;
529}
530
531void C4Object::ForcePosition(int32_t tx, int32_t ty)
532{
533 // always reset fixed
534 fix_x = itofix(x: tx); fix_y = itofix(x: ty);
535 // no movement
536 if (!((x - tx) | (y - ty))) return;
537 x = tx; y = ty;
538 UpdatePos();
539 UpdateSolidMask(fRestoreAttachedObjects: false);
540}
541
542void C4Object::MovePosition(int32_t dx, int32_t dy)
543{
544 // move object position; repositions SolidMask
545 if (pSolidMaskData) pSolidMaskData->Remove(fCauseInstability: true, fBackupAttachment: true);
546 x += dx; y += dy;
547 fix_x += dx;
548 fix_y += dy;
549 UpdatePos();
550 UpdateSolidMask(fRestoreAttachedObjects: true);
551}
552
553bool C4Object::ExecMovement() // Every Tick1 by Execute
554{
555 // Containment check
556 if (Contained)
557 {
558 CopyMotion(from: Contained);
559
560 return true;
561 }
562
563 // General mobility check
564 if (Category & C4D_StaticBack) return false;
565
566 // Movement execution
567 if (Mobile) // Object is moving
568 {
569 // Move object
570 DoMovement();
571 // Demobilization check
572 if ((xdir == 0) && (ydir == 0) && (rdir == 0)) Mobile = 0;
573 // Check for stabilization
574 if (rdir == 0) Stabilize();
575 }
576 else // Object is static
577 {
578 // Check for stabilization
579 Stabilize();
580 // Check for mobilization
581 if (!Tick10)
582 {
583 // Gravity mobilization
584 xdir = ydir = rdir = 0;
585 fix_x = itofix(x); fix_y = itofix(x: y); fix_r = itofix(x: r);
586 Mobile = 1;
587 }
588 }
589
590 // Enforce zero rotation
591 if (!Def->Rotateable) r = 0;
592
593 // Out of bounds check
594 if ((!Inside<int32_t>(ival: x, lbound: 0, GBackWdt) && !(Def->BorderBound & C4D_Border_Sides)) || (y > GBackHgt && !(Def->BorderBound & C4D_Border_Bottom)))
595 // Never remove attached objects: If they are truly outside landscape, their target will be removed,
596 // and the attached objects follow one frame later
597 if (Action.Act < 0 || !Action.Target || Def->ActMap[Action.Act].Procedure != DFA_ATTACH)
598 {
599 bool fRemove = true;
600 // never remove HUD objects
601 if (Category & C4D_Parallax)
602 {
603 fRemove = false;
604 if (x > GBackWdt || y > GBackHgt) fRemove = true; // except if they are really out of the viewport to the right...
605 else if (x < 0 && Local[0].Data) fRemove = true; // ...or it's not HUD horizontally and it's out to the left
606 else if (!Local[0].Data && x < -GBackWdt) fRemove = true; // ...or it's HUD horizontally and it's out to the left
607 }
608 if (fRemove)
609 {
610 AssignDeath(fForced: true);
611 AssignRemoval();
612 }
613 }
614
615 return true;
616}
617
618bool SimFlight(C4Fixed &x, C4Fixed &y, C4Fixed &xdir, C4Fixed &ydir, int32_t iDensityMin, int32_t iDensityMax, int32_t iIter)
619{
620 bool fBreak = false;
621 int32_t ctcox, ctcoy, cx, cy;
622 cx = fixtoi(x); cy = fixtoi(x: y);
623 do
624 {
625 if (!iIter--) return false;
626 // Set target position by momentum
627 x += xdir; y += ydir;
628 // Movement to target
629 ctcox = fixtoi(x); ctcoy = fixtoi(x: y);
630 // Bounds
631 if (!Inside<int32_t>(ival: ctcox, lbound: 0, GBackWdt) || (ctcoy >= GBackHgt)) return false;
632 // Move to target
633 do
634 {
635 // Set next step target
636 cx += Sign(val: ctcox - cx); cy += Sign(val: ctcoy - cy);
637 // Contact check
638 if (Inside(ival: GBackDensity(x: cx, y: cy), lbound: iDensityMin, rbound: iDensityMax))
639 {
640 fBreak = true; break;
641 }
642 } while ((cx != ctcox) || (cy != ctcoy));
643 // Adjust GravAccel once per frame
644 ydir += GravAccel;
645 } while (!fBreak);
646 // write position back
647 x = itofix(x: cx); y = itofix(x: cy);
648 // ok
649 return true;
650}
651
652bool SimFlightHitsLiquid(C4Fixed fcx, C4Fixed fcy, C4Fixed xdir, C4Fixed ydir)
653{
654 // Start in water?
655 if (DensityLiquid(dens: GBackDensity(x: fixtoi(x: fcx), y: fixtoi(x: fcy))))
656 if (!SimFlight(x&: fcx, y&: fcy, xdir, ydir, iDensityMin: 0, iDensityMax: C4M_Liquid - 1, iIter: 10))
657 return false;
658 // Hits liquid?
659 if (!SimFlight(x&: fcx, y&: fcy, xdir, ydir, iDensityMin: C4M_Liquid, iDensityMax: 100, iIter: -1))
660 return false;
661 // liquid & deep enough?
662 return GBackLiquid(x: fixtoi(x: fcx), y: fixtoi(x: fcy)) && GBackLiquid(x: fixtoi(x: fcx), y: fixtoi(x: fcy) + 9);
663}
664