-
Notifications
You must be signed in to change notification settings - Fork 96
/
building_query.cpp
3070 lines (2813 loc) · 168 KB
/
building_query.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// 3D World - Building Query Functions
// by Frank Gennari 10/09/21
#include "3DWorld.h"
#include "function_registry.h"
#include "buildings.h"
extern bool draw_building_interiors, camera_in_building, player_near_toilet, player_in_unlit_room, building_has_open_ext_door, player_on_escalator, ctrl_key_pressed;
extern bool player_is_hiding, player_wait_respawn, had_building_interior_coll, player_in_int_elevator, player_in_walkway, player_on_house_stairs, player_on_moving_ww;
extern bool player_in_tunnel;
extern int camera_surf_collide, frame_counter, animate2, player_in_closet, player_in_elevator, player_in_basement, player_in_attic, player_in_water;
extern float CAMERA_RADIUS, C_STEP_HEIGHT, NEAR_CLIP, fticks, building_bcube_expand;
extern double camera_zh;
extern building_params_t global_building_params;
extern bldg_obj_type_t bldg_obj_types[];
extern building_t const *player_building;
float get_railing_height(room_object_t const &c);
cylinder_3dw get_railing_cylinder(room_object_t const &c);
bool sphere_vert_cylin_intersect_with_ends(point ¢er, float radius, cylinder_3dw const &c, vector3d *cnorm);
void register_in_closed_bathroom_stall();
pair<cube_t, colorRGBA> car_bcube_color_from_parking_space(room_object_t const &o);
void force_player_height(double height);
bool is_player_model_female();
bool get_sphere_poly_int_val(point const &sc, float sr, point const *const points, unsigned npoints, vector3d const &normal, float thickness, float &val, vector3d &cnorm);
float get_player_move_dist();
void get_shelf_brackets(room_object_t const &c, cube_t shelves[4], unsigned num_shelves, vect_cube_with_ix_t &brackets);
// assumes player is in this building; handles windows and exterior doors but not attics and basements
bool building_t::player_can_see_outside() const {
vector3d const xlate(get_tiled_terrain_model_xlate());
point const camera_pos(get_camera_pos()), camera_bs(camera_pos - xlate);
float const floor_spacing(get_window_vspace());
if (!has_int_windows()) { // no windows looking out
// should we check if the door is visible?
if (building_has_open_ext_door && !doors.empty()) { // maybe can see out a door
// maybe can see out open door on first floor or a walkway
if (have_walkway_ext_door || (camera_pos.z < (ground_floor_z1 + floor_spacing) && camera_pos.z > ground_floor_z1)) return 1;
if (interior) { // check roof access stairs
point camera_bot_bs(camera_bs);
camera_bot_bs.z -= get_bldg_player_height();
for (stairwell_t const &s : interior->stairwells) {
if (s.roof_access && !s.is_u_shape() && s.contains_pt(camera_bot_bs)) return 1;
}
}
}
for (cube_t const &skylight : skylights) { // check skylights
cube_t test_area(skylight);
test_area.expand_by_xy(2.0*floor_spacing);
test_area.z1() -= floor_spacing; // include the floor below
if (test_area.contains_pt(camera_bs)) return 1; // outside may be visible through the skylight
}
return 0;
}
// maybe can see out a window
if (!interior || !has_basement() || camera_pos.z > ground_floor_z1) return 1; // no interior, or not in the basement
if (camera_pos.z < ground_floor_z1 - floor_spacing) return 0; // on lower level of parking garage or extended basement
if (point_in_extended_basement_not_basement(camera_bs)) return 0; // not visible from extended basement; not for rotated buildings
for (auto const &s : interior->stairwells) { // check basement stairs
if (s.z1() >= ground_floor_z1 || s.z2() <= ground_floor_z1) continue; // not basement stairs
if (s.in_mall || s.in_ext_basement) continue; // mall and extended basement stairs don't count
if (s.is_u_shape()) continue; // can't see around the bend
if (!is_rot_cube_visible(s, xlate)) continue; // VFC
if (!s.contains_pt(camera_bs) && s.stairs_door_ix >= 0 && get_door(s.stairs_door_ix).open_amt == 0.0) continue; // closed stairs door, not visible
if (!has_parking_garage) return 1; // assume exterior may be visible through normal stairs, but not parking garage stairs
cube_t stairs_exp(s);
stairs_exp.expand_by_xy(floor_spacing);
if (!get_basement().contains_cube_xy(stairs_exp)) return 1; // stairs near the edge of the building basement - window may be visible
} // for s
if (has_pg_ramp() && !interior->ignore_ramp_placement) {} // what about ramp?
return 0;
}
bool player_in_windowless_building() {return (player_building != nullptr && !player_building->player_can_see_outside());}
bool player_cant_see_outside_building() {
if (player_in_basement >= 3 || player_in_attic == 2) return 1; // player in extended basement or windowless attic
if (player_in_windowless_building()) return 1;
return 0;
}
bool building_t::check_bcube_overlap_xy(building_t const &b, float expand_rel, float expand_abs) const {
if (expand_rel == 0.0 && expand_abs == 0.0 && !bcube.intersects(b.bcube)) return 0;
if (!is_rotated() && !b.is_rotated()) return 1; // above check is exact, top-level bcube check up to the caller
if (b.bcube.contains_pt_xy(bcube.get_cube_center()) || bcube.contains_pt_xy(b.bcube.get_cube_center())) return 1; // slightly faster to include this check
return (check_bcube_overlap_xy_one_dir(b, expand_rel, expand_abs) || b.check_bcube_overlap_xy_one_dir(*this, expand_rel, expand_abs));
}
// Note: only checks for point (x,y) value contained in one cube/N-gon/cylinder; assumes pt has already been rotated into local coordinate frame
bool building_t::check_part_contains_pt_xy(cube_t const &part, unsigned part_id, point const &pt) const {
if (!part.contains_pt_xy(pt)) return 0; // check bounding cube
if (is_cube()) return 1; // that's it
vector<point> const &points(get_part_ext_verts(part_id));
return point_in_polygon_2d(pt.x, pt.y, points.data(), points.size()); // 2D x/y containment
}
bool building_t::check_part_contains_cube_xy(cube_t const &part, unsigned part_id, cube_t const &c) const { // for placing roof objects
for (unsigned i = 0; i < 4; ++i) { // check cylinder/ellipse
point const pt(c.d[0][i&1], c.d[1][i>>1], 0.0); // XY only
if (!check_part_contains_pt_xy(part, part_id, pt)) return 0;
}
return 1;
}
bool building_t::cube_int_parts_no_sec(cube_t const &c) const {
for (auto p = parts.begin(); p != get_real_parts_end(); ++p) {
if (p->intersects_no_adj(c)) return 1;
}
return 0;
}
bool building_t::check_pt_in_retail_room(point const &p) const {
if (!has_retail() || !interior || interior->rooms.empty()) return 0;
return get_retail_room().contains_pt(p);
}
bool building_t::check_pt_in_or_near_walkway(point const &p, bool owned_only, bool inc_open_door, bool inc_conn_room) const {
for (building_walkway_t const &w : walkways) {
if (owned_only && !w.is_owner) continue;
if (inc_open_door && w.has_skyway_conn()) { // test skyway walkway connection
cube_t vis_area(w.skyway_conn);
vis_area.expand_in_dim(w.dim, (player_in_walkway ? 2.0 : 0.1)*w.get_length()); // extend to include other nearby walkways
if (vis_area.contains_pt(p)) return 1; // player in nearby skyway area or opposing walkway
}
if (p.z > w.bcube.z1() && p.z < w.bcube.z2()) { // check for Z overlap
if ((inc_open_door ? w.get_bcube_inc_open_door() : w.bcube).contains_pt(p)) return 1;
// check adjacent rooms if connected, walkway is visible through windows, and pos is off the ends of the walkways; forms a dog bone shape
if (inc_conn_room && has_int_windows() && (p[w.dim] < w.bcube.d[w.dim][0] || p[w.dim] > w.bcube.d[w.dim][1]) && w.bcube_inc_rooms.contains_pt(p)) return 1;
}
if (!w.elevator_bcube.is_all_zeros() && w.elevator_bcube.contains_pt(p)) return 1; // check elevator connected to walkway
} // for walkways
return 0;
}
bool building_t::is_connected_with_walkway(building_t const &target, float zval) const {
for (building_walkway_t const &w : walkways) {
if (zval != 0.0 && (zval < w.bcube.z1() || zval > w.bcube.z2())) continue; // apply zval filter
if (w.conn_bldg == &target) return 1;
}
return 0;
}
cube_t building_walkway_t::get_bcube_inc_open_door() const {
cube_t bc(bcube);
bc.expand_in_dim(dim, get_door_open_dist());
return bc;
}
bool building_t::check_bcube_overlap_xy_one_dir(building_t const &b, float expand_rel, float expand_abs) const { // can be called before levels/splits are created
// Note: easy cases are handled by check_bcube_overlap_xy() above
point const center1(b.bcube.get_cube_center()), center2(bcube.get_cube_center());
for (auto p1 = b.parts.begin(); p1 != b.parts.end(); ++p1) {
point pts[9]; // {center, 00, 10, 01, 11, x0, x1, y0, y1}
if (b.parts.size() == 1) {pts[0] = center1;} // single cube: we know we're rotating about its center
else {
pts[0] = p1->get_cube_center();
b.do_xy_rotate(center1, pts[0]); // rotate into global space
}
cube_t c_exp(*p1);
c_exp.expand_by_xy(expand_rel*p1->get_size() + vector3d(expand_abs, expand_abs, expand_abs));
for (unsigned i = 0; i < 4; ++i) { // {00, 10, 01, 11}
pts[i+1].assign(c_exp.d[0][i&1], c_exp.d[1][i>>1], 0.0); // XY only
b.do_xy_rotate(center1, pts[i+1]); // rotate into global space
}
for (unsigned i = 0; i < 5; ++i) {do_xy_rotate_inv(center2, pts[i]);} // inverse rotate into local coord space - negate the sine term
cube_t c_exp_rot(pts+1, 4); // use points 1-4
pts[5] = 0.5*(pts[1] + pts[3]); // x0 edge center
pts[6] = 0.5*(pts[2] + pts[4]); // x1 edge center
pts[7] = 0.5*(pts[1] + pts[2]); // y0 edge center
pts[8] = 0.5*(pts[3] + pts[4]); // y1 edge center
for (auto p2 = parts.begin(); p2 != parts.end(); ++p2) {
if (c_exp_rot.contains_pt_xy(p2->get_cube_center())) return 1; // quick and easy test for heavy overlap
for (unsigned i = 0; i < 9; ++i) {
if (check_part_contains_pt_xy(*p2, (p2 - parts.begin()), pts[i])) return 1; // Note: building geometry is likely not yet generated, this check should be sufficient
//if (p2->contains_pt_xy(pts[i])) return 1;
}
}
} // for p1
return 0;
}
bool do_sphere_coll_polygon_sides(point &pos, cube_t const &part, float radius, bool interior_coll, vector<point> const &points, vector3d *cnorm) {
point quad_pts[4]; // quads
bool coll(0);
for (unsigned S = 0; S < points.size(); ++S) { // generate vertex data quads
for (unsigned d = 0, ix = 0; d < 2; ++d) {
point const &p(points[(S+d)%points.size()]);
for (unsigned e = 0; e < 2; ++e) {quad_pts[ix++].assign(p.x, p.y, part.d[2][d^e]);}
}
vector3d const normal((interior_coll ? -1.0 : 1.0)*get_poly_norm(quad_pts)); // invert to get interior normal
float const rdist(dot_product_ptv(normal, pos, quad_pts[0]));
if (rdist < 0.0 || rdist >= radius) continue; // too far or wrong side
if (!sphere_poly_intersect(quad_pts, 4, pos, normal, rdist, radius)) continue;
pos += normal*(radius - rdist);
if (cnorm) {*cnorm = normal;}
if (!interior_coll) return 1; // interior coll must test against all sides
coll = 1;
} // for S
return coll;
}
// called for players and butterfiles
bool building_t::test_coll_with_sides(point &pos, point const &p_last, float radius, vector3d const &xlate, cube_t const &part, unsigned part_id, vector3d *cnorm) const {
vect_point const &points(get_part_ext_verts(part_id));
unsigned const num_steps(max(1U, min(100U, (unsigned)ceil(2.0*p2p_dist_xy(pos, p_last)/radius))));
vector3d const step_delta((pos - p_last)/num_steps);
pos = p_last - xlate;
// if the player is moving too quickly, the intersection with a side polygon may be missed, which allows the player to travel through the building;
// so we split the test into multiple smaller sphere collision steps
for (unsigned step = 0; step < num_steps; ++step) {
pos += step_delta;
if (do_sphere_coll_polygon_sides(pos, part, radius, 0, points, cnorm)) {pos += xlate; return 1;} // interior_coll=0
}
if (max(pos.z, p_last.z) > part.z2() && point_in_polygon_2d(pos.x, pos.y, points.data(), num_sides)) { // test top plane (sphere on top of polygon?)
pos.z = part.z2() + radius; // make sure it doesn't intersect the roof
pos += xlate;
if (cnorm) {*cnorm = plus_z;}
return 1;
}
pos += xlate;
return 0;
}
cube_t building_t::get_interior_bcube(bool inc_ext_basement) const { // Note: called for indir lighting; could cache z2
cube_t int_bcube(inc_ext_basement ? get_bcube_inc_extensions() : bcube);
int_bcube.z2() = interior_z2;
return int_bcube;
}
void building_t::union_with_coll_bcube(cube_t const &c) {
coll_bcube.union_with_cube(c);
for (unsigned d = 0; d < 2; ++d) {max_eq(building_bcube_expand, max((bcube.d[d][0] - c.d[d][0]), (c.d[d][1] - bcube.d[d][1])));}
}
void accumulate_shared_xy_area(cube_t const &c, cube_t const &sc, float &area) {
if (c.intersects_xy(sc)) {area += (min(c.x2(), sc.x2()) - max(c.x1(), sc.x1()))*(min(c.y2(), sc.y2()) - max(c.y1(), sc.y1()));}
}
void apply_speed_factor(point &pos, point const &p_last, float speed_factor) {
for (unsigned d = 0; d < 2; ++d) {pos[d] = speed_factor*pos[d] + (1.0 - speed_factor)*p_last[d];}
}
// Note: used for the player when check_interior=1; pos and p_last are in camera space
bool building_t::check_sphere_coll(point &pos, point const &p_last, vector3d const &xlate, float radius, bool xy_only, vector3d *cnorm_ptr, bool check_interior) const {
if (!is_valid()) return 0; // invalid building
if (radius > 0.0) {
// player can't be in multiple buildings at once; if they were in some other building last frame, they can't be in this building this frame
if (check_interior && camera_in_building && player_building != this) return 0;
// skip cube intersection test if point (camera) is in the mall elevator; use p_last for this test since we want to know where the player was the last frame
if (!check_interior || !point_in_mall_elevator_entrance((p_last - xlate), 1)) {
// skip zval check when the player is in the building because pos.z may be placed on the mesh,
// which could be above the top of the building when the player is in the extended basement;
// this should be legal because the player can't exit the building in the Z direction; maybe better to use p_last.z?
bool const sc_xy_only(xy_only || (check_interior && camera_in_building && player_building == this));
cube_t test_cube(coll_bcube);
point const pos_bs(pos - xlate);
if (check_interior && has_ext_basement()) {test_cube.union_with_cube(interior->basement_ext_bcube);} // include the extended basement for interior checks
if (!(sc_xy_only ? sphere_cube_intersect_xy(pos_bs, radius, test_cube) : sphere_cube_intersect(pos_bs, radius, test_cube))) return 0;
}
}
if (check_sphere_coll_inner(pos, p_last, xlate, radius, xy_only, cnorm_ptr, check_interior)) return 1;
if (!check_interior) return 0;
building_t const *const other_bldg(get_conn_bldg_for_pt((point(pos.x, pos.y, min(pos.z, p_last.z)) - xlate), radius)); // Note: not handling pos rotation
if (other_bldg == nullptr) return 0; // no connected building at this location
return other_bldg->check_sphere_coll_inner(pos, p_last, xlate, radius, xy_only, cnorm_ptr, check_interior); // check connected building
}
bool building_t::check_sphere_coll_inner(point &pos, point const &p_last, vector3d const &xlate, float radius, bool xy_only, vector3d *cnorm_ptr, bool check_interior) const {
float const xy_radius(radius*global_building_params.player_coll_radius_scale);
point pos2(pos), p_last2(p_last), center;
bool had_coll(0), is_interior(0), is_in_attic(0), allow_outside_building(0), on_ext_stair(0), reduce_speed(0);
float part_z2(bcube.z2());
if (is_rotated()) {
center = bcube.get_cube_center() + xlate;
do_xy_rotate_inv(center, pos2); // inverse rotate - negate the sine term
do_xy_rotate_inv(center, p_last2);
}
if (check_interior && draw_building_interiors && interior != nullptr) { // check for interior case first
// this is the real zval for use in collsion detection, in building space
float zval(((p_last2.z < ground_floor_z1) ? min(pos2.z, p_last2.z) : max(pos2.z, p_last2.z)) - xlate.z); // min/max away from the ground floor
// if the player is in flight mode and enables collision detection at the ground floor or basement with their feet just below the floor,
// clamp to Z1 to keep them inside the building rather than putting them on the roof
if (zval < bcube.z1() && zval+camera_zh > bcube.z1() && bcube.contains_pt_xy(pos2 - xlate) && p_last == p_last2) {zval = bcube.z1();}
point const pos2_bs(pos2 - xlate), query_pt(pos2_bs.x, pos2_bs.y, zval);
// first check uses min of the two zvals to reject the basement, which is actually under the mesh
if ((min(pos2.z, p_last2.z) + radius) > ground_floor_z1) {
unsigned const floor_ix((zval - ground_floor_z1)/get_window_vspace());
if (have_walkway_ext_door || (floor_ext_door_mask & (1 << floor_ix))) { // check for exterior doors on this floor
for (auto d = doors.begin(); d != doors.end(); ++d) { // exterior doors
if (d->type == tquad_with_ix_t::TYPE_RDOOR) continue; // doesn't apply to roof door
cube_t bc(d->get_bcube());
if (zval < bc.z1() || zval > bc.z2()) continue; // no Z overlap
bool const door_dim(bc.dy() < bc.dx());
bc.expand_in_dim( door_dim, 1.1*radius); // expand by radius plus some tolerance in door dim
bc.expand_in_dim(!door_dim, -0.5*xy_radius); // shrink slightly in the other dim to prevent the player from clipping through the wall next to the door
bc.z1() -= max(radius, (float)camera_zh); // account for player on steep slope up to door - require player head above doorframe bottom
if (!bc.contains_pt(point(pos2_bs.x, pos2_bs.y, zval))) continue; // check if door can be used
//if (floor_ix == 0) return 0; // ground floor, disable collsion to allow the player to walk through
allow_outside_building = 1; // allow the player to exit, but for now still in the building
} // for d
}
}
// check for sphere contained in parts and maybe clamp to parts
cube_t sc; sc.set_from_sphere(query_pt, radius); // sphere bounding cube; zvals are ignored
for (auto i = parts.begin(); i != get_real_parts_end_inc_sec(); ++i) { // include garages and sheds
float cont_area(0.0);
cube_t clamp_part(*i);
if (is_basement(i)) {
bool const in_ext_basement(point_in_extended_basement_not_basement(query_pt));
if (in_ext_basement) {clamp_part = interior->basement_ext_bcube;}
else if (!i->contains_pt(query_pt)) continue; // not in basement
accumulate_shared_xy_area(*i, sc, cont_area);
if (has_ext_basement() && zval < interior->basement_ext_bcube.z2()) { // below ext basement top (not in upper level of two level parking garage)
// use the ext basement hallway if pos is in the basement, otherwise use the entire ext basement
cube_t const &basement_cube(in_ext_basement ? interior->basement_ext_bcube : get_ext_basement_entrance());
accumulate_shared_xy_area(basement_cube, sc, cont_area);
}
}
else {
if (!i->contains_pt(query_pt)) continue; // not interior to this part
if (!allow_outside_building && !is_cube() && zval > i->z1() && zval < i->z2()) { // non-cube shaped building, in Z bounds, clamp_part is conservative
//if (use_cylinder_coll()) {}
vect_point const &points(get_part_ext_verts(i - parts.begin()));
point const p_last2_bs(p_last2 - xlate);
if (!point_in_polygon_2d(p_last2_bs.x, p_last2_bs.y, points.data(), points.size())) continue; // outside the building, even though inside part bcube
pos2 -= xlate; // temporarily convert to building space
bool const had_int(do_sphere_coll_polygon_sides(pos2, *i, radius, 1, points, cnorm_ptr));
pos2 += xlate; // convert back to camera space
if (had_int) {is_interior = had_coll = 1; break;} // interior_coll=1
}
for (auto p = parts.begin(); p != get_real_parts_end(); ++p) {
if (zval < p->z1() || zval > p->z2()) continue; // wrong floor/part in stack
accumulate_shared_xy_area(*p, sc, cont_area);
}
}
if (!allow_outside_building && cont_area < 0.99*sc.get_area_xy()) { // sphere bounding cube not contained in union of parts - partially outside the building
cube_t c(clamp_part + xlate); // convert to camera space
c.expand_by_xy(-radius); // shrink part by sphere radius
c.clamp_pt_xy(pos2); // force pos2 into interior of the cube to prevent the sphere from intersecting the part
had_coll = 1;
}
is_interior = 1;
break; // flag for interior collision detection
} // for i
if (point_in_attic(query_pt)) {is_interior = is_in_attic = 1;}
is_interior |= point_in_mall_elevator_entrance(query_pt, 0); // inc_front_space=0
if (!is_interior && has_mall() && interior->mall_info->city_elevator_ix >= 0) { // handle mall elevator exterior coll when doors are closed
elevator_t const &e(get_elevator(interior->mall_info->city_elevator_ix));
if (e.open_amt < 0.5) {had_coll |= sphere_cube_int_update_pos(pos2, radius, (e + xlate), p_last2, xy_only, cnorm_ptr);}
}
if (!is_interior) { // not interior to a part - check roof access
float const floor_thickness(get_floor_thickness());
for (auto i = interior->stairwells.begin(); i != interior->stairwells.end(); ++i) {
if (!i->roof_access) continue;
cube_t test_cube(*i);
test_cube.expand_by_xy(-0.5f*radius);
if (!test_cube.contains_pt_xy(pos2 - xlate)) continue; // pos not over stairs
if (zval < i->z2() + radius + floor_thickness) {is_interior = 1; break;} // Note: don't have to check zval > i->z2() because we know that !is_interior
}
}
}
if (!xy_only && min(pos2.z, p_last2.z) > ground_floor_z1) { // only when above ground; skip if in basement
// check for collision with exterior stairs, since they apply to both the interior and exterior case
for (ext_step_t const &s : ext_steps) { // Note: includes balconies
cube_t const c(s + xlate);
if (s.enclosed && !c.contains_pt_xy(pos2)) continue; // don't clip through walls onto balconies
if (!sphere_cube_intersect_xy(pos2, radius, c)) continue;
float const zval(max(pos2.z, p_last2.z));
// use 2x radius for stability: too low and we fall through the step some frames; too high and we get stuck on stairs below
if (zval + radius < c.z1() || zval - 2.0*radius > c.z2()) continue; // no collision in Z
if (!s.at_door && !s.is_base && (pos2[!s.dim] < c.d[!s.dim][0] || pos2[!s.dim] > c.d[!s.dim][1])) { // player to the side
had_coll |= sphere_cube_int_update_pos(pos2, radius, c, p_last2, xy_only, cnorm_ptr); // check collision with sides
continue;
}
float &wval(pos2[!s.dim]), &lval(pos2[s.dim]); // position relative to the {width, length) of the stairs
if (c.contains_pt_xy(pos2)) {
// handle collision with railing and building wall
unsigned skip_dir(0);
if (s.at_door) {skip_dir |= (1 << unsigned( s.wall_dir));} // allow entering/exiting from house
if (s.is_base) {skip_dir |= (1 << unsigned(1-s.wall_dir));} // allow entering/exiting from ground
if (!(skip_dir & 1)) {max_eq(wval, (c.d[!s.dim][0] + xy_radius));}
if (!(skip_dir & 2)) {min_eq(wval, (c.d[!s.dim][1] - xy_radius));}
if (s.at_door || s.enclosed) { // handle collision with top end railing(s)
if ( s.step_dir || s.enclosed) {max_eq(lval, (c.d[s.dim][0] + xy_radius));}
if (!s.step_dir || s.enclosed) {min_eq(lval, (c.d[s.dim][1] - xy_radius));}
}
}
if (zval + radius > c.z1()) { // step up
if (s.step_up || c.contains_pt_xy(pos2)) {max_eq(pos2.z, (c.z2() + radius));} // only step up if on this stair, unless it's a wall we can climb
if (wval > c.d[!s.dim][0] && wval < c.d[!s.dim][1]) {on_ext_stair = reduce_speed = 1;} // not entering or leaving from the sides
had_coll = 1;
}
else {had_coll |= sphere_cube_int_update_pos(pos2, radius, c, p_last2, xy_only, cnorm_ptr);} // check collision with sides
} // for i
if (!ladder.is_all_zeros() && sphere_cube_intersect(pos2, radius, ladder)) {
//had_coll = sphere_cube_int_update_pos(pos2, radius, ladder, p_last2, xy_only, cnorm_ptr); // no, don't want to collide when player is on the roof
pos2.z = p_last2.z + 0.5*get_player_move_dist()*cview_dir.z; // move up/down based on player vertical view (looking up vs. down)
pos2.z = min((ladder.z2() + radius), max((ladder.z1() + radius), pos2.z)); // clamp to ladder height range
had_coll = 1;
}
}
if (player_wait_respawn) {pos2.x = p_last2.x; pos2.y = p_last2.y;} // player can't move
else if (reduce_speed) {apply_speed_factor(pos2, p_last2, 0.6);} // slow down to 60% when on exterior stairs or balconies
if (on_ext_stair) { // only need to check for blockers at the bottom of stairs
for (auto const &i : details) {
if (i.type == DETAIL_OBJ_SHAD_ONLY) continue; // not a collider
had_coll |= sphere_cube_int_update_pos(pos2, radius, (i + xlate), p_last2, xy_only, cnorm_ptr); // treat as cubes
}
}
else if (is_interior) {
point pos2_bs(pos2 - xlate);
if (check_sphere_coll_interior(pos2_bs, (p_last2 - xlate), radius, is_in_attic, xy_only, cnorm_ptr)) {pos2 = pos2_bs + xlate; had_coll = 1;}
had_building_interior_coll = 1; // required to skip city object collisions
}
else { // exterior
if (allow_outside_building) return 0; // entering an exterior door - no collision with building exterior
float const r_bias(0.5*get_window_vspace()); // enough to allow some falling but not pick up the floor below
for (auto i = parts.begin(); i != parts.end(); ++i) {
if (xy_only) {
if (i->z1() < ground_floor_z1) continue; // skip basements, since they should be contained in the union of the ground floor (optimization)
if (i->z1() > ground_floor_z1) { // only check ground floor
if (has_complex_floorplan) {continue;} else {break;}
}
}
// zval must include p_last2 for correct part selection in complex building floorplans (with flat roofs), but is not correct for walking on sloped roofs
float const zval((roof_type == ROOF_TYPE_FLAT) ? max(pos2.z, p_last2.z) : pos2.z);
cube_t const part_bc(*i + xlate);
if (!xy_only && ((zval + radius < part_bc.z1()) || (zval - radius - r_bias > part_bc.z2()))) continue; // test z overlap
if (radius == 0.0 && !(xy_only ? part_bc.contains_pt_xy(pos2) : part_bc.contains_pt(point(pos2.x, pos2.y, zval)))) continue; // no intersection; ignores p_last
unsigned const part_id(i - parts.begin());
bool part_coll(0);
if (use_cylinder_coll()) {
point const cc(part_bc.get_cube_center());
float const crx(0.5*i->dx()), cry(0.5*i->dy()), r_sum(radius + max(crx, cry));
if (!dist_xy_less_than(pos2, cc, r_sum)) continue; // no intersection
if (fabs(crx - cry) < radius) { // close to a circle
if (p_last2.z > part_bc.z2() && dist_xy_less_than(pos2, cc, max(crx, cry))) {
pos2.z = part_bc.z2() + radius; // make sure it doesn't intersect the roof
if (cnorm_ptr) {*cnorm_ptr = plus_z;}
}
else { // side coll
vector2d const d((pos2.x - cc.x), (pos2.y - cc.y));
float const mult(r_sum/d.mag());
pos2.x = cc.x + mult*d.x;
pos2.y = cc.y + mult*d.y;
if (cnorm_ptr) {*cnorm_ptr = vector3d(d.x, d.y, 0.0).get_norm();} // no z-component
}
part_coll = 1;
}
else {
part_coll |= test_coll_with_sides(pos2, p_last2, radius, xlate, part_bc, part_id, cnorm_ptr); // use polygon collision test
}
}
else if (num_sides != 4) { // triangle, hexagon, octagon, etc.
part_coll |= test_coll_with_sides(pos2, p_last2, radius, xlate, part_bc, part_id, cnorm_ptr);
}
else if (!xy_only && part_bc.contains_pt_xy_exp(pos2, radius) && p_last2.z > (i->z2() + xlate.z)) { // on top of building
pos2.z = i->z2() + xlate.z + radius;
if (cnorm_ptr) {*cnorm_ptr = plus_z;}
part_coll = 1;
}
else if (sphere_cube_int_update_pos(pos2, radius, part_bc, p_last2, xy_only, cnorm_ptr)) { // cube
part_coll = 1; // flag as colliding, continue to look for more collisions (inside corners)
}
if (part_coll && pos2.z < part_bc.z1()) {pos2.z = part_bc.z2() + radius;} // can't be under a building - make it on top of the building instead
if (part_coll) {part_z2 = i->z2();}
had_coll |= part_coll;
} // for i
if (has_chimney == 2) {had_coll |= sphere_cube_int_update_pos(pos2, radius, (get_fireplace() + xlate), p_last2, xy_only, cnorm_ptr);} // exterior fireplace
for (cube_t const &fence : fences) {had_coll |= sphere_cube_int_update_pos(pos2, radius, (fence + xlate), p_last2, xy_only, cnorm_ptr);}
// Note: driveways are handled elsewhere in the control flow
if (!xy_only) { // don't need to check details and roof in xy_only mode because they're contained in the XY footprint of the parts (except balconies and stairs)
for (auto const &i : details) {
if (i.type == DETAIL_OBJ_SHAD_ONLY) continue; // not a collider
if (i.type == ROOF_OBJ_WTOWER && pos2.z < i.z1() + 0.25*i.dz()) { // below custom water tower collision
if (!sphere_cube_intersect(pos2, radius, (i + xlate))) continue;
float const hwidth(0.25*(i.dx() + i.dy()));
cube_t inner(i);
inner.expand_by_xy(-0.1*hwidth);
for (unsigned n = 0; n < 4; ++n) {
bool const xd(n & 1), yd(n >> 1);
cube_t leg(i);
leg.d[0][!xd] = inner.d[0][xd];
leg.d[1][!yd] = inner.d[1][yd];
had_coll |= sphere_cube_int_update_pos(pos2, radius, (leg + xlate), p_last2, xy_only, cnorm_ptr);
}
cylinder_3dw const cylin(cube_bot_center(i)+xlate, cube_top_center(i)+xlate, 0.1*hwidth, 0.1*hwidth);
had_coll |= sphere_vert_cylin_intersect(pos2, radius, cylin, cnorm_ptr);
}
else {had_coll |= sphere_cube_int_update_pos(pos2, radius, (i + xlate), p_last2, xy_only, cnorm_ptr);} // treat as cubes
} // for i
for (auto i = roof_tquads.begin(); i != roof_tquads.end(); ++i) {
point const pos_xlate(pos2 - xlate);
if (check_interior && had_coll && pos2.z - xlate.z > part_z2) { // player standing on top of a building with a sloped roof or roof access cover
if (point_in_polygon_2d(pos_xlate.x, pos_xlate.y, i->pts, i->npts)) {
vector3d const normal(i->get_norm());
if (normal.z == 0.0) continue; // skip vertical sides as the player can't stand on them
float const rdist(dot_product_ptv(normal, pos_xlate, i->pts[0]));
if (draw_building_interiors && i->type == tquad_with_ix_t::TYPE_ROOF_ACC) { // don't allow walking on roof access tquads
if (rdist < -radius*normal.z) continue; // player is below this tquad
else {pos2.x = p_last2.x; pos2.y = p_last2.y; break;} // block the player from walking here (can only walk through raised opening)
}
pos2.z += (radius - rdist)/normal.z; // determine the distance we need to move vertically to achieve this diag separation
}
}
else { // normal case for bouncing object, etc.
vector3d const normal(i->get_norm());
float const rdist(dot_product_ptv(normal, pos_xlate, i->pts[0]));
if (fabs(rdist) < radius && sphere_poly_intersect(i->pts, i->npts, pos_xlate, normal, rdist, radius)) {
pos2 += normal*(radius - rdist); // update current pos
if (cnorm_ptr) {*cnorm_ptr = ((normal.z < 0.0) ? -1.0 : 1.0)*normal;} // make sure normal points up
had_coll = 1; // flag as colliding
break; // only use first colliding tquad
}
}
} // for i
}
if (is_house && has_porch() && sphere_cube_int_update_pos(pos2, radius, (porch + xlate), p_last2, xy_only, cnorm_ptr)) {had_coll = 1;} // porch
} // end !is_interior case
if (!had_coll) return 0; // Note: no collisions with windows or doors, since they're colinear with walls
if (is_rotated()) {
do_xy_rotate(center, pos2); // rotate back around center
if (cnorm_ptr) {do_xy_rotate_normal(*cnorm_ptr);} // rotate normal back
}
pos = pos2;
return 1;
}
float room_object_t::get_radius() const {
if (shape == SHAPE_CYLIN || shape == SHAPE_VERT_TORUS) {return 0.25f*(dx() + dy());} // vertical cylinder or torus: return average of x/y diameter
if (shape == SHAPE_SPHERE) {return 0.5*dx();} // sphere, should be the same dx()/dy()/dz() value (but can't assert due to FP precision errors)
std::cerr << "Error: get_radius() called on non-cylinder/cube object of type " << int(type) << endl;
assert(0); // cubes don't have a radius
return 0.0; // never gets here
}
cylinder_3dw room_object_t::get_cylinder() const {
float const radius(get_radius());
point const center(get_cube_center());
return cylinder_3dw(point(center.x, center.y, z1()), point(center.x, center.y, z2()), radius, radius);
}
// Note: returns bit vector for each cube that collides; supports up to 32 cubes
unsigned check_cubes_collision(cube_t const *const cubes, unsigned num_cubes, point &pos, point const &p_last, float radius, vector3d *cnorm) {
unsigned coll_ret(0);
for (unsigned n = 0; n < num_cubes; ++n) {
if (!cubes[n].is_all_zeros() && sphere_cube_int_update_pos(pos, radius, cubes[n], p_last, 0, cnorm)) {coll_ret |= (1<<n);} // skip_z=0
}
return coll_ret;
}
unsigned get_closet_num_coll_cubes(room_object_t const &c) {
// include closed closet door for large closets; skip collision check of open doors for large closets since this case is more complex
return ((!c.is_open() && !c.is_small_closet()) ? 5U : 4U);
}
unsigned check_closet_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[5];
get_closet_cubes(c, cubes, 1); // get cubes for walls and door; required to handle collision with closet interior; for_collision=1
return check_cubes_collision(cubes, get_closet_num_coll_cubes(c), pos, p_last, radius, cnorm);
}
unsigned check_bed_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[6]; // frame, head, foot, mattress, pillow, legs_bcube
get_bed_cubes(c, cubes);
unsigned num_to_check(5); // skip legs_bcube
if (c.taken_level > 0) {--num_to_check;} // skip pillows
if (c.taken_level > 2) {--num_to_check;} // skip mattress
unsigned coll_ret(check_cubes_collision(cubes, num_to_check, pos, p_last, radius, cnorm));
get_tc_leg_cubes(cubes[5], c, BED_HEAD_WIDTH, 0, cubes); // cubes[5] is not overwritten
coll_ret |= (check_cubes_collision(cubes, 4, pos, p_last, radius, cnorm) << 5); // check legs
return coll_ret;
}
bool can_use_table_coll(room_object_t const &c) {
return (c.type == TYPE_DESK || c.type == TYPE_DRESSER || c.type == TYPE_NIGHTSTAND || c.type == TYPE_TABLE);
}
// actually applies to tables, desks, dressers, and nightstands
unsigned get_table_like_object_cubes(room_object_t const &c, cube_t cubes[7]) { // tables, desks, dressers, and nightstands
if (c.type == TYPE_TABLE && c.item_flags > 0) {
get_cubes_for_plastic_table(c, 0.12, cubes); // top_dz=0.12
return 3; // {top, vert, base}
}
unsigned num(5);
get_table_cubes(c, cubes); // top and 4 legs
if (c.type == TYPE_DRESSER || c.type == TYPE_NIGHTSTAND) {cubes[num++] = get_dresser_middle(c);}
else if (c.type == TYPE_DESK) {
if (c.desk_has_drawers() ) {cubes[num++] = get_desk_drawers_part(c);}
if (c.shape == SHAPE_TALL) {cubes[num++] = get_desk_top_back (c);} // tall desk
}
return num;
}
unsigned check_table_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[7];
unsigned const num(get_table_like_object_cubes(c, cubes));
return check_cubes_collision(cubes, num, pos, p_last, radius, cnorm);
}
unsigned check_conf_table_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[2]; // {top, base}
get_conf_table_cubes(c, cubes);
return check_cubes_collision(cubes, 2, pos, p_last, radius, cnorm);
}
unsigned check_rdesk_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[3]; // {front, left, right}
get_reception_desk_cubes(c, cubes);
return check_cubes_collision(cubes, 3, pos, p_last, radius, cnorm);
}
unsigned check_chair_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[3], leg_cubes[4]; // seat, back, legs_bcube
get_chair_cubes(c, cubes);
get_tc_leg_cubes(cubes[2], c, c.get_chair_leg_width(), 1, leg_cubes);
return (check_cubes_collision(cubes, 2, pos, p_last, radius, cnorm) | check_cubes_collision(leg_cubes, 4, pos, p_last, radius, cnorm));
}
float get_tub_water_level(room_object_t const &c);
void get_tub_cubes(room_object_t const &c, cube_t cubes[5], float radius_if_ball=0.0) { // bottom, front, back, 2 sides
float const height(c.get_height()), width(c.get_width()), signed_depth((c.dir ? 1.0 : -1.0)*c.get_depth());
cube_t bottom(c), front(c), back(c);
float bot_z2(c.z1() + 0.07*height);
// if the collider is a ball, assume density=0.5 and it floats centered at the water level; not using ball_type.density here
if (radius_if_ball > 0.0) {max_eq(bot_z2, (c.z1() + min(get_tub_water_level(c), 1.0f)*c.dz() - radius_if_ball));}
bottom.z2() = front.z1() = back.z1() = bot_z2;
front.d[c.dim][ c.dir] -= 0.81*signed_depth;
back .d[c.dim][!c.dir] += 0.86*signed_depth;
cubes[0] = bottom;
cubes[1] = front;
cubes[2] = back;
for (unsigned d = 0; d < 2; ++d) { // sides
cube_t side(c);
side.z1() = bottom.z2();
side.d[!c.dim][d] -= (d ? 1.0 : -1.0)*0.85*width;
side.d[c.dim][!c.dir] = front.d[c.dim][ c.dir];
side.d[c.dim][ c.dir] = back .d[c.dim][!c.dir];
cubes[d+3] = side;
}
}
unsigned check_tub_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm, bool is_ball) {
cube_t cubes[5]; // bottom, front, back, 2 sides
get_tub_cubes(c, cubes, (is_ball ? radius : 0.0));
return check_cubes_collision(cubes, 5, pos, p_last, radius, cnorm);
}
unsigned check_bookcase_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
if (c.is_on_floor()) return sphere_cube_int_update_pos(pos, radius, c, p_last, 0, cnorm); // fallen over
cube_t top, middle, back, lr[2];
get_bookcase_cubes(c, top, middle, back, lr);
cube_t const cubes[3] = {middle, lr[0], lr[1]}; // ignore top and back since they should be contained in middle
return check_cubes_collision(cubes, 3, pos, p_last, radius, cnorm);
}
unsigned check_bench_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[4]; // seat, lo side, hi side, [back]
unsigned const num(get_bench_cubes(c, cubes));
return check_cubes_collision(cubes, num, pos, p_last, radius, cnorm);
}
unsigned check_diving_board_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[2]; // board, base
get_diving_board_cubes(c, cubes);
return check_cubes_collision(cubes, 2, pos, p_last, radius, cnorm);
}
bool check_ramp_collision(room_object_t const &c, point &pos, float radius, vector3d *cnorm) {
if (!sphere_cube_intersect(pos, radius, c)) return 0;
bool const is_pg_ramp(c.type == TYPE_RAMP);
float const thickness(is_pg_ramp ? RAMP_THICKNESS_SCALE*c.dz() : 0.0), half_thickness(0.5*thickness);
tquad_t ramp(get_ramp_tquad(c));
for (unsigned n = 0; n < ramp.npts; ++n) {ramp.pts[n].z -= half_thickness;} // shift to the centerline, which is what get_sphere_poly_int_val() requires
vector3d const normal(ramp.get_norm());
float dist(0.0); // distance from sphere to ramp surface
vector3d coll_normal;
if (!get_sphere_poly_int_val(pos, radius, ramp.pts, ramp.npts, normal, thickness, dist, coll_normal)) return 0;
if (cnorm) {*cnorm = coll_normal;}
pos += coll_normal*dist;
pos += vector3d(coll_normal.x, coll_normal.y, 0.0)*0.1*radius; // move downhill (slowly when rolling)
return 1;
}
unsigned get_stall_cubes(room_object_t const &c, cube_t sides[3]) { // {side, side, door}
sides[0] = sides[1] = sides[2] = c;
float const width(c.get_width());
sides[0].d[!c.dim][1] -= 0.95*width;
sides[1].d[!c.dim][0] += 0.95*width;
if (!c.is_open()) {sides[2].d[c.dim][c.dir] += (c.dir ? -1.0 : 1.0)*0.975*c.get_length();} // include closed door
return (c.is_open() ? 2U : 3U);
}
// Note: these next two are intended to be called when maybe_inside_room_object() returns true
bool check_stall_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t sides[3];
unsigned const num_cubes(get_stall_cubes(c, sides));
return check_cubes_collision(sides, num_cubes, pos, p_last, radius, cnorm);
}
unsigned get_shower_cubes(room_object_t const &c, cube_t sides[2]) {
sides[0] = sides[1] = c;
bool const door_dim(c.dx() < c.dy()), door_dir(door_dim ? c.dir : c.dim), side_dir(door_dim ? c.dim : c.dir); // {c.dim, c.dir} => {dir_x, dir_y}
sides[0].d[!door_dim][side_dir] -= (side_dir ? 1.0 : -1.0)*0.95*c.get_sz_dim(!door_dim); // shrink to just the outer glass wall of the shower
if (!c.is_open()) {sides[1].d[door_dim][door_dir] += (door_dir ? -1.0 : 1.0)*0.95*c.get_sz_dim(door_dim);} // check collision with closed door
return (c.is_open() ? 1U : 2U);
}
bool check_shower_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t sides[2];
unsigned const num_cubes(get_shower_cubes(c, sides));
return check_cubes_collision(sides, num_cubes, pos, p_last, radius, cnorm);
}
bool check_balcony_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[4];
get_balcony_cubes(c, cubes);
return check_cubes_collision(cubes, 4, pos, p_last, radius, cnorm);
}
void get_pool_table_cubes(room_object_t const &c, cube_t cubes[5]) { // body + 4 legs
cube_t legs_bcube(c);
legs_bcube.z2() -= 0.48*c.dz();
legs_bcube.expand_by_xy(-0.13*c.get_width()); // legs are recessed a bit, but not quite centered; should be close enough
get_tc_leg_cubes(legs_bcube, c, 0.11, 0, cubes+1); // legs are cubes[1]:cubes[4]
cubes[0] = c;
cubes[0].z1() = cubes[1].z2(); // body starts at the top of the legs
}
unsigned check_pool_table_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[5]; // body + 4 legs
get_pool_table_cubes(c, cubes);
// if pos is over the interior, use the top surface zval; otherwise, use z2 and assume an edge collision; could also call subtract_cube_from_cube()
cube_t const top(get_pool_table_top_surface(c));
if (top.contains_pt_xy(pos)) {cubes[0].z2() = top.z1();}
return check_cubes_collision(cubes, 5, pos, p_last, radius, cnorm);
}
cube_t get_shelves_no_bot_gap(room_object_t const &c) {
cube_t bcube(c);
bcube.z1() += c.dz()/(c.get_num_shelves() + 1); // raise z1 to the bottom of the first shelf
return bcube;
}
unsigned get_all_shelf_rack_cubes(room_object_t const &c, cube_t cubes[9]) { // back, <= 5 shelves, top, <= 2 sides
cube_t back, top, sides[2], shelves[5];
unsigned const num_shelves(get_shelf_rack_cubes(c, back, top, sides, shelves));
unsigned num(0);
cubes[num++] = back;
for (unsigned n = 0; n < num_shelves; ++n) {cubes[num++] = shelves[n];}
if (!top.is_all_zeros()) {cubes[num++] = top;}
if (!sides[0].is_all_zeros()) {
for (unsigned d = 0; d < 2; ++d) {cubes[num++] = sides[d];}
}
return num;
}
unsigned check_shelf_rack_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[9];
unsigned const num_cubes(get_all_shelf_rack_cubes(c, cubes));
if (c.is_nonempty()) { // if there are objects on the shelf rack, expand the back to 80% of the full shelf width
set_wall_width(cubes[0], c.get_center_dim(c.dim), 0.8*0.5*c.get_depth(), c.dim);
}
return check_cubes_collision(cubes, num_cubes, pos, p_last, radius, cnorm);
}
unsigned get_couch_cubes(room_object_t const &c, cube_t cubes[4]) { // bottom, back, arm, arm; excludes pillows and blanket
bool const dir2(c.dim ^ c.dir);
float const height(c.dz()), depth(c.get_depth()), width(c.get_width());
cube_t c_clip(c);
c_clip.d[!c.dim][dir2] -= (dir2 ? 1.0 : -1.0)*0.025*width; // subtract off the blanket on the right side
cube_t bot(c_clip), arms(c_clip);
bot.expand_in_dim(!c.dim, -0.1*width); // shrink off arms
cube_t back(bot);
bot .z2() -= 0.48*height;
bot .z1() += 0.05*height;
arms.z2() -= 0.32*height;
back.z1() = bot.z2();
back.d[c.dim][c.dir] -= (c.dir ? 1.0 : -1.0)*0.65*depth;
cubes[0] = bot;
cubes[1] = back;
for (unsigned d = 0; d < 2; ++d) {
cube_t arm(arms);
arm.d[!c.dim][!d] = back.d[!c.dim][d];
cubes[d+2] = arm;
}
return 4;
}
unsigned check_couch_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[4];
return check_cubes_collision(cubes, get_couch_cubes(c, cubes), pos, p_last, radius, cnorm);
}
unsigned get_cashreg_cubes(room_object_t const &c, cube_t cubes[2]) { // main, screen
cube_t const bc(get_true_room_obj_bcube(c));
bool const dir2(c.dim ^ c.dir);
float const width(bc.get_sz_dim(!c.dim)), length(bc.get_sz_dim(c.dim));
cube_t body(bc), screen(bc);
body.z2() = screen.z1() = c.z1() + 0.7*c.dz();
screen.d[ c.dim][ c.dir] -= (c.dir ? 1.0 : -1.0)*0.395*length; // black screen side
screen.d[ c.dim][!c.dir] += (c.dir ? 1.0 : -1.0)*0.36*length; // shorter rectangle side
screen.d[!c.dim][ dir2 ] -= (dir2 ? 1.0 : -1.0)*0.47*width; // subtract off space in front of screen
screen.d[!c.dim][!dir2 ] += (dir2 ? 1.0 : -1.0)*0.26*width; // subtract off space behind of screen
cubes[0] = body;
cubes[1] = screen; // conservative
return 2;
}
unsigned check_cashreg_collision(room_object_t const &c, point &pos, point const &p_last, float radius, vector3d *cnorm) {
cube_t cubes[2];
return check_cubes_collision(cubes, get_cashreg_cubes(c, cubes), pos, p_last, radius, cnorm);
}
bool maybe_inside_room_object(room_object_t const &obj, point const &pos, float radius) {
return ((obj.is_open() && sphere_cube_intersect(pos, radius, obj)) || obj.contains_pt(pos));
}
cube_t get_true_room_obj_bcube(room_object_t const &c) { // for collisions, etc.
if (c.type == TYPE_ATTIC_DOOR) {
cube_t bcube(get_attic_access_door_cube(c));
if (c.is_open()) {bcube.union_with_cube(get_ladder_bcube_from_open_attic_door(c, bcube));} // include ladder as well
return bcube;
}
if (c.type == TYPE_RAILING && (c.flags & RO_FLAG_IN_POOL)) { // pool stairs railing
cube_t c_ext(c);
c_ext.z2() += 0.5*c_ext.dz(); // extend the top upward to block the player from walking onto the railing
return c_ext;
}
if (c.type == TYPE_RAILING) { // only works for straight (non-sloped) top railings
cube_t c_ext(c);
c_ext.z2() = c.z1() + get_railing_height(c);
return c_ext;
}
if (c.type == TYPE_CASHREG) {
cube_t c_ext(c);
c_ext.expand_in_dim(!c.dim, -0.25*c.get_width()); // object bcube is twice the size it should be; subtract this off
return c_ext;
}
if (c.type == TYPE_PLANT) {
cube_t c_pot(c);
c_pot.expand_by_xy(-(1.0 - PLANT_POT_RADIUS)*c.get_radius()); // use XY radius of the pot; better for AI coll
return c_pot;
}
if (c.type == TYPE_TREE) {} // bcubes are not a good fit for trees; cube covers the pot and trunk, but not leaves; should not be colliding with leaves anyway
if (c.type == TYPE_SHOWERTUB) {return get_shower_tub_wall (c);} // only the end wall is a collider; the tub handles the bottom (what about curtains?)
if (c.type == TYPE_SHELVES ) {return get_shelves_no_bot_gap(c);}
return c; // default cube case
}
bool room_object_t::is_player_collidable() const {
// chairs are player collidable only when in attics or backrooms; trashcans are only player collidable in malls
return (!no_coll() && (bldg_obj_types[type].player_coll || (type == TYPE_CHAIR && (in_attic() || (flags & RO_FLAG_BACKROOM))) ||
(type == TYPE_TCAN && (in_mall() || in_hallway()))));
}
// Note: used for the player; pos and p_last are already in rotated coordinate space
// default player is actually too large to fit through doors and too tall to fit between the floor and celing,
// so player size/height must be reduced in the config file
bool building_t::check_sphere_coll_interior(point &pos, point const &p_last, float radius, bool is_in_attic, bool xy_only, vector3d *cnorm) const {
pos.z = get_bcube_z1_inc_ext_basement(); // start at building z1 rather than the terrain height in case we're at the foot of a steep hill
assert(interior);
float const floor_spacing(get_window_vspace()), floor_thickness(get_floor_thickness()), attic_door_z_gap(0.2f*floor_thickness);
float const xy_radius(radius*global_building_params.player_coll_radius_scale); // XY radius can be smaller to allow player to fit between furniture
bool had_coll(0), on_stairs(0), on_attic_ladder(0);
float obj_z(max(pos.z, p_last.z)); // use p_last to get orig zval
cube_with_ix_t const &attic_access(interior->attic_access);
unsigned reset_to_last_dims(0); // {x, y} bit flags, for attic roof collision
double camera_height(get_player_height());
static double prev_camera_height(0.0);
cube_t tunnel_walk_area;
if (!xy_only && 2.2f*radius < (floor_spacing - floor_thickness)) { // diameter is smaller than space between floor and ceiling
// check Z collision with floors; no need to check ceilings; this will set pos.z correctly so that we can set skip_z=0 in later tests
float const floor_test_zval(obj_z + floor_thickness); // move up by floor thickness to better handle steep stairs
float closest_floor_zval(pos.z - radius); // start at the player's feet
for (auto i = interior->floors.begin(); i != interior->floors.end(); ++i) { // Note: includes attic and basement floors
if (!i->contains_pt_xy(pos)) continue; // sphere not in this floor
float const z(i->z2());
if (z <= floor_test_zval && z > closest_floor_zval) {closest_floor_zval = z; had_coll = 1;} // move up
}
if (interior->room_geom) { // check glass floors
for (cube_t const &f : interior->room_geom->glass_floors) {
if (!f.contains_pt_xy(pos)) continue; // sphere not on this floor
if (point_in_elevator(pos) || point_in_stairwell(pos)) continue; // elevators and stairwells cut out glass floors, so ignore them in this case
float const z(f.z2());
if (z <= floor_test_zval && z > closest_floor_zval) {closest_floor_zval = z; had_coll = 1;} // move up
}
}
// check tunnels
if (!interior->tunnels.empty() && point_in_extended_basement_not_basement(pos)) {
point const p_test(pos.x, pos.y, obj_z);
bool found(0);
// first pass checks bcube, second pass checks bcube_ext (which may overlap other tunnels)
for (unsigned pass = 0; pass < 2 && !found; ++pass) {
for (tunnel_seg_t const &t : interior->tunnels) {
if (!((pass == 0) ? t.bcube : t.bcube_ext).contains_pt(p_test)) continue;
float const z(t.bcube.z1());
if (z > floor_test_zval || z < closest_floor_zval) continue; // wrong zval
tunnel_walk_area = t.get_walk_area(pos, xy_radius);
tunnel_walk_area.clamp_pt_xy(pos);
closest_floor_zval = z; // move up
had_coll = player_in_tunnel = found = 1;
}
} // for pass
}
if (had_coll) {pos.z = closest_floor_zval + radius; obj_z = max(pos.z, p_last.z);}
if (has_attic() && attic_access.contains_pt_xy(pos)) {
if (interior->attic_access_open && obj_z > (attic_access.z2() - floor_spacing)) { // on attic ladder - handle like a ramp
apply_speed_factor(pos, p_last, 0.3); // slow down to 30% when climbing the ladder
bool const dim(attic_access.ix >> 1), dir(attic_access.ix & 1);
float const length(attic_access.get_sz_dim(dim)), t(CLIP_TO_01((pos[dim] - attic_access.d[dim][0])/length)), T(dir ? t : (1.0-t));
pos.z = (attic_access.z2() + attic_door_z_gap) - floor_spacing*(1.0 - T) + radius;
obj_z = max(pos.z, p_last.z);
had_coll = on_attic_ladder = 1;
}
else if (!interior->attic_access_open && obj_z > attic_access.z2()) { // standing on closed attic access door
pos.z = attic_access.z2() + radius; // move up
obj_z = max(pos.z, p_last.z);
had_coll = 1;
}
}
if (is_in_attic) {
vector3d roof_normal;
float const beam_depth(get_attic_beam_depth());
// check player's head against the roof/overhead beams to avoid clipping through it
if (!point_in_attic(point(pos.x, pos.y, (pos.z + 1.1f*camera_height + beam_depth)), &roof_normal)) {
for (unsigned d = 0; d < 2; ++d) { // reset pos X/Y if oriented toward the roof
if (roof_normal[d] != 0.0 && ((roof_normal[d] < 0.0) ^ (pos[d] < p_last[d]))) {reset_to_last_dims |= (1<<d);}
}
if (cnorm) {*cnorm = roof_normal;}
if (camera_height > prev_camera_height) { // less crouched than before
camera_height = prev_camera_height;
force_player_height(prev_camera_height); // force crouch
}
}
// find the part the player is in and clamp our bsphere to it; currently attics are limited to a single part
for (auto i = parts.begin(); i != get_real_parts_end(); ++i) {
if (!i->contains_pt_xy(pos)) continue; // wrong part
cube_t valid_area(*i);
valid_area.expand_by_xy(-xy_radius);
valid_area.clamp_pt_xy(pos);
break;
} // for i
player_in_attic = (has_attic_window ? 1 : 2);
had_coll = 1;
obj_z = max(pos.z, p_last.z);
}
}
// *** Note ***: at this point pos.z is radius above the floors and *not* at the camera height, which is why we add camera_height to pos.z below
// Note: this check must be after pos.z is set from interior->floors
// pass in radius as wall_test_extra_z as a hack to allow player to step over a wall that's below the stairs connecting stacked parts
had_coll |= interior->check_sphere_coll_walls_elevators_doors(*this, pos, p_last, xy_radius, radius, 1, cnorm); // is_player=1
if (interior->room_geom) { // collision with room geometry
vect_room_object_t const &objs(interior->room_geom->objs);
float speed_factor(1.0);
for (auto c = interior->room_geom->get_stairs_start(); c != objs.end(); ++c) { // check for and handle stairs first
if (c->no_coll() || c->type != TYPE_STAIR) continue;
if (!c->contains_pt_xy(pos)) continue; // sphere not on this stair
if (obj_z < c->z1()) continue; // below the stair
if (pos.z - radius > c->z2()) continue; // above the stair
bool const is_u(c->shape == SHAPE_STAIRS_U), is_l(c->shape == SHAPE_STAIRS_L), is_ul(is_u || is_l);
// U-shaped stairs have strange collisions, and it seems to work better to have the player stand on the z1 value rather than the z2;
// but stairs that are tall (such as pool stairs) only work when standing on the z2 value
float const stairs_zval(is_u ? c->z1() : c->z2());
pos.z = stairs_zval + radius; // stand on the stair - this can happen for multiple stairs
if (has_basement()) { // don't let the zval end where the camera is between the terrain mesh and the basement entrance plane or we'll see the terrain
float const camera_z(pos.z + camera_height), mesh_z(get_basement().z2()), entrance_z(mesh_z + BASEMENT_ENTRANCE_SCALE*floor_thickness);
if (camera_z > mesh_z && camera_z < entrance_z) {pos.z += 1.2*(entrance_z - camera_z);} // move up above the entrance plane
}
obj_z = max(pos.z, p_last.z);
if (c->shape != SHAPE_STAIRS_FAN) { // force the sphere onto the stairs unless it's a fan (mall entrance)
if (!is_ul || c->dir == 1) {max_eq(pos[!c->dim], (c->d[!c->dim][0] + xy_radius));}
if (!is_ul || c->dir == 0) {min_eq(pos[!c->dim], (c->d[!c->dim][1] - xy_radius));}
}
had_coll = on_stairs = 1;