-
Notifications
You must be signed in to change notification settings - Fork 96
/
building_interact.cpp
2403 lines (2210 loc) · 128 KB
/
building_interact.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 vs. Player/AI Interaction Logic
// by Frank Gennari 1/14/21
#include "3DWorld.h"
#include "function_registry.h"
#include "buildings.h"
#include "openal_wrap.h"
// physics constants, currently applied to balls
float const KICK_VELOCITY = 0.0025;
float const MIN_VELOCITY = 0.0001;
float const MIN_VELOCITY_SQ= MIN_VELOCITY*MIN_VELOCITY;
float const SMOKE_VELOCITY = 0.0006;
float const OBJ_DECELERATE = 0.008;
float const OBJ_GRAVITY = 0.0003; // unsigned magnitude
float const TERM_VELOCITY = 1.0;
float const OBJ_ELASTICITY = 0.8;
extern bool tt_fire_button_down, flashlight_on, use_last_pickup_object, city_action_key, can_do_building_action, toggle_room_light, player_wait_respawn, building_alarm_active;
extern int player_in_closet, camera_surf_collide, can_pickup_bldg_obj, building_action_key, animate2, frame_counter, player_in_elevator, player_in_attic;
extern float fticks, CAMERA_RADIUS, office_chair_rot_rate;
extern double tfticks;
extern building_dest_t cur_player_building_loc;
extern building_t const *player_building;
bool player_can_open_door(door_t const &door);
unsigned player_has_room_key();
bool player_has_pool_cue();
bool was_room_stolen_from(unsigned room_id);
void register_broken_object(room_object_t const &obj);
void record_building_damage(float damage);
void pool_ball_in_pocket(unsigned ball_number);
void refill_thirst();
colorRGBA get_glow_color(float stime, bool fade);
void play_hum_sound(point const &pos, float gain, float pitch);
bool ceiling_fan_is_on(room_object_t &obj, vect_room_object_t const &objs);
// Note: pos is in camera space
void gen_sound_thread_safe(unsigned id, point const &pos, float gain, float pitch, float gain_scale, bool skip_if_already_playing) {
assert(gain > 0.0 && pitch > 0.0 && gain_scale > 0.0);
float const dist(p2p_dist(get_camera_pos(), pos)), dscale(10.0*CAMERA_RADIUS*gain_scale); // distance at which volume is halved
gain *= dscale/(dist + dscale);
if (gain < 0.025) return; // too soft to hear
#pragma omp critical(gen_sound)
gen_sound(id, pos, gain, pitch, 0, zero_vector, skip_if_already_playing);
}
// lights
float get_radius_for_room_light(room_object_t const &obj);
bool is_motion_detected(point const &activator, cube_t const &light, cube_t const &room, float fc_gap) {
return (room.contains_pt(activator) && activator.z < light.z1() && activator.z > (light.z2() - fc_gap));
}
void building_t::run_light_motion_detect_logic(point const &camera_bs) {
if (!animate2) return;
if (is_house || !interior) return; // office buildings only
if (player_in_elevator) return; // skip so that we don't have a lot of clicking when lights switch on due to AIs while passing floors
float const floor_spacing(get_window_vspace()), fc_gap(get_floor_ceil_gap());
auto objs_end(interior->room_geom->get_placed_objs_end()); // skip buttons/stairs/elevators
for (auto i = interior->room_geom->objs.begin(); i != objs_end; ++i) {
if (i->type != TYPE_LIGHT || !i->is_active() || !i->is_powered()) continue; // not a light, unpowered, or not motion activated
assert(i->room_id < interior->rooms.size());
room_t const &room(interior->rooms[i->room_id]);
bool const is_player(is_motion_detected(camera_bs, *i, room, fc_gap));
bool activated(is_player);
float &off_time(i->light_amt); // store auto off time in the light_amt field
for (auto p = interior->people.begin(); p != interior->people.end() && !activated; ++p) {
if (p->is_waiting_or_stopped()) continue; // skip if stopped/waiting
if (fabs(p->pos.z - camera_bs.z) > 0.75*floor_spacing) continue; // player is on a different floor, skip (too many clicking sounds)
activated |= is_motion_detected(p->pos, *i, room, fc_gap);
}
if (activated) {
off_time = tfticks + 10.0*TICKS_PER_SECOND; // automatically turn off 10s since last activation
if (i->is_lit()) continue; // stays lit - no change
}
else {
if (!animate2) continue; // no auto off
if (!i->is_lit()) continue; // already off, and stays off
if (tfticks < off_time) continue; // already on, and not yet time to switch off
}
i->toggle_lit_state();
set_obj_lit_state_to(i->room_id, i->z2(), i->is_lit());
register_light_state_change(*i, i->get_cube_center());
if (1 || is_player) {register_indir_lighting_state_change(i - interior->room_geom->objs.begin());} // only update for player activations?
} // for i
}
// Note: called by the player; closest_to is in building space, not camera space
bool building_t::toggle_room_light(point const &closest_to, bool sound_from_closest_to, int room_id, bool inc_lamps, bool closet_light, bool known_in_attic) {
if (!has_room_geom()) return 0; // error?
// attic lights on posts are exactly between roof tquads and point_in_attic() may not return the correct value, so known_in_attic should be passed in
bool const in_attic(known_in_attic || point_in_attic(closest_to));
if (room_id < 0 && !in_attic) { // caller has not provided a valid room_id, so determine it now
room_id = get_room_containing_pt(get_inv_rot_pos(closest_to));
if (room_id < 0) return 0; // closest_to is not contained in a room of this building
}
bool const ignore_floor(in_attic || get_room(room_id).is_single_floor);
vect_room_object_t &objs(interior->room_geom->objs);
auto objs_end(interior->room_geom->get_placed_objs_end()); // skip buttons/stairs/elevators
bool const in_closet(closet_light || bool(player_in_closet)); // while in the closet, player can only toggle closet lights and not room lights
float closest_dist_sq(0.0);
int closest_light(-1);
for (auto i = objs.begin(); i != objs_end; ++i) {
if (!i->is_light_type() || (!inc_lamps && i->type == TYPE_LAMP)) continue; // not a light
if ( in_attic && !i->in_attic()) continue;
if (!in_attic && i->room_id != room_id) continue; // wrong room
if (i->in_closet() != in_closet) continue;
if (i->in_elevator()) continue; // can't toggle elevator light
if (i->z2() < closest_to.z) continue; // below the query pos; needed for malls
if (!ignore_floor && get_floor_for_zval(i->z1()) != get_floor_for_zval(closest_to.z)) continue; // wrong floor (skip garages and sheds)
point center(i->get_cube_center());
if (is_rotated()) {do_xy_rotate(bcube.get_cube_center(), center);}
float const dist_sq(p2p_dist_sq(closest_to, center));
if (closest_dist_sq == 0.0 || dist_sq < closest_dist_sq) {closest_dist_sq = dist_sq; closest_light = int(i - objs.begin());}
} // for i
if (closest_light < 0) return 0;
room_object_t const &light(objs[closest_light]);
toggle_light_object(light, (sound_from_closest_to ? closest_to : light.get_cube_center()));
return 1;
}
bool building_t::toggle_walkway_lights(point const &pos) {
for (building_walkway_t const &w : walkways) {
if (!w.is_owner || !w.bcube.contains_pt(pos)) continue;
vect_room_object_t &objs(interior->room_geom->objs);
auto objs_end(interior->room_geom->get_placed_objs_end()); // skip buttons/stairs/elevators
bool found(0);
for (auto i = objs.begin(); i != objs_end; ++i) { // toggle all walkway lights for this floor
if (i->type != TYPE_LIGHT) continue; // not a light
if (!w.bcube.contains_cube(*i)) continue; // not in the walkway
if (get_floor_for_zval(i->z1()) != get_floor_for_zval(pos.z)) continue; // wrong floor
i->toggle_lit_state(); // Note: doesn't update indir lighting
//register_indir_lighting_state_change(i - objs.begin()); // no indir from walkway lights
if (!found) {register_light_state_change(*i, pos);} // update on first light found
found = 1;
}
return found; // can only be in one walkway
} // for walkways
return 0;
}
void building_t::toggle_light_object(room_object_t const &light, point const &sound_pos) { // called by the player
assert(has_room_geom());
auto objs_end(interior->room_geom->get_placed_objs_end()); // skip buttons/stairs/elevators
bool updated(0), is_lamp(1);
for (auto i = interior->room_geom->objs.begin(); i != objs_end; ++i) { // toggle all lights on this floor of this room
if (!i->is_light_type() || i->room_id != light.room_id || !i->is_powered()) continue;
if (light.in_closet() != i->in_closet()) continue; // closet + room light are toggled independently
if (i->z2() != light.z2()) continue; // Note: uses light z2 rather than z1 so that thin lights near doors are handled correctly
i->toggle_lit_state(); // Note: doesn't update indir lighting
i->flags &= ~RO_FLAG_IS_ACTIVE; // disable motion detection feature if the player manually switches lights off
register_indir_lighting_state_change(i - interior->room_geom->objs.begin());
updated = 1;
if (i->type == TYPE_LAMP) continue; // lamps don't affect room object ambient lighting, and don't require regenerating the vertex data, so skip the step below
if (is_lamp) {set_obj_lit_state_to(light.room_id, light.z2(), i->is_lit());} // update object lighting flags as well, for first non-lamp light
is_lamp = 0;
} // for i
if (!updated) return; // can we get here?
register_light_state_change(light, sound_pos, is_lamp);
//interior->room_geom->modified_by_player = 1; // should light state always be preserved?
}
void building_t::register_light_state_change(room_object_t const &light, point const &sound_pos, bool is_lamp) {
if (!is_lamp) {interior->room_geom->invalidate_lights_geom();} // recreate light geom with correct emissive properties if not a lamp; deferred until next draw pass
gen_sound_thread_safe(SOUND_CLICK, local_to_camera_space(sound_pos));
register_building_sound(sound_pos, 0.1);
float const fear_amt((light.is_light_on() ? 1.0 : 0.5)*(is_lamp ? 0.5 : 1.0)); // max fear from lights turning on; lamps are half as much fear
for (rat_t &rat : interior->room_geom->rats) { // light change scares rats
if (get_room_containing_pt(rat.pos) == light.room_id) {scare_rat_at_pos(rat, sound_pos, fear_amt, 0);} // scare if in the same room
}
}
bool building_t::set_room_light_state_to(room_t const &room, float zval, bool make_on) { // called by AI people
if (!has_room_geom()) return 0; // error?
if (room.is_hallway) return 0; // don't toggle lights for hallways, which can have more than one light
auto objs_end(interior->room_geom->get_placed_objs_end()); // skip buttons/stairs/elevators
float const window_vspacing(get_window_vspace());
bool updated(0);
for (auto i = interior->room_geom->objs.begin(); i != objs_end; ++i) {
if (i->type != TYPE_LIGHT) continue; // not a light (excludes lamps)
if (i->flags & (RO_FLAG_IN_CLOSET | RO_FLAG_IN_ELEV)) continue; // skip lights in closets or elevators, these can only be toggled by the player
if (i->z1() < zval || i->z1() > (zval + window_vspacing) || !room.contains_cube_xy(*i)) continue; // light is on the wrong floor or in the wrong room
if (i->is_lit() != make_on) {i->toggle_lit_state(); updated = 1;} // Note: doesn't update indir lighting or room light value
} // for i
if (updated) {interior->room_geom->invalidate_lights_geom();} // recreate light geom with correct emissive properties; will flag for update next frame
return updated;
}
void building_t::set_obj_lit_state_to(unsigned room_id, float light_z2, bool lit_state) {
assert(has_room_geom());
room_t const &room(get_room(room_id));
float const light_intensity(room.light_intensity);
auto objs_end(interior->room_geom->get_placed_objs_end()); // skip buttons/stairs/elevators
float const obj_zmin(room.is_single_floor ? room.z1() : (light_z2 - get_floor_ceil_gap()));
for (auto i = interior->room_geom->objs.begin(); i != objs_end; ++i) {
if (i->room_id != room_id || i->z1() < obj_zmin || i->z1() > light_z2) continue; // wrong room or floor
if (i->is_obj_model_type()) continue; // light_amt currently does not apply to 3D models; should it?
if (i->is_exterior ()) continue; // not lit by interior light
bool invalidate(0);
if (i->type == TYPE_STAIR || i->type == TYPE_STAIR_WALL || i->type == TYPE_ELEVATOR || i->type == TYPE_LIGHT || i->type == TYPE_BLOCKER ||
i->type == TYPE_COLLIDER || i->type == TYPE_SIGN || i->type == TYPE_WALL_TRIM || i->type == TYPE_RAILING || i->type == TYPE_BLINDS ||
i->type == TYPE_SWITCH || i->type == TYPE_OUTLET || i->type == TYPE_PG_WALL || i->type == TYPE_PG_PILLAR || i->type == TYPE_PG_BEAM ||
i->type == TYPE_PARK_SPACE || i->type == TYPE_RAMP || i->type == TYPE_VENT || i->type == TYPE_METAL_BAR || i->type == TYPE_OFF_PILLAR)
{
continue; // not a type that uses light_amt
}
else if (i->type == TYPE_WINDOW) {
if (lit_state) {i->flags |= RO_FLAG_LIT;} else {i->flags &= ~RO_FLAG_LIT;}
invalidate = 1;
}
else {
float const prev_light_amt(i->light_amt);
if (lit_state) {i->light_amt += light_intensity;} else {i->light_amt = max((i->light_amt - light_intensity), 0.0f);} // shouldn't be negative, but clamp to 0 just in case
invalidate = (fabs(i->light_amt - prev_light_amt) > 0.1); // generally always true, but good to have this check/optimization in the future
}
if (invalidate) {interior->room_geom->invalidate_draw_data_for_obj(*i);} // Note: can't clear here if called from building AI (not in the draw thread)
} // for i
}
bool building_room_geom_t::closet_light_is_on(cube_t const &closet) const {
auto objs_end(get_placed_objs_end()); // skip buttons/stairs/elevators
for (auto i = objs.begin(); i != objs_end; ++i) {
if (i->type == TYPE_LIGHT && i->in_closet() && closet.contains_cube(*i)) {return i->is_light_on();}
}
return 0;
}
breaker_zone_t building_interior_t::get_circuit_breaker_info(unsigned zone_id, unsigned num_zones, float floor_spacing) const {
assert(zone_id < num_zones);
// Note: if there are multiple panels, they will affect the same set of zones; it seems too difficult to assign rooms/zones across panels;
// this means that zones will follow the state of the last breaker that was toggled to a different state
if (!elevators.empty()) { // elevators are always zone 0 (lower left or right breaker)
if (zone_id == 0) return breaker_zone_t(RTYPE_ELEVATOR, 0, 0, -1);
--zone_id; --num_zones; // exclude elevator
}
unsigned const num_rooms(rooms.size());
if (num_zones == 0 || num_rooms == 0) return breaker_zone_t(); // no zones left, or no rooms
// wrap around if there are more zones than rooms; this will assign the same room to multiple zones/breakers;
// labels will use room names on upper floors for wrapped zones; maybe this should also split toggle behavior by floor groups?
// but that would be extra complex because we would need per-floor room unpowered flags, and it's unclear if the player would even understand how this works
unsigned const floor_block_ix(zone_id/num_rooms);
zone_id = zone_id % num_rooms;
// determine which rooms this breaker controls;
// we really should have breakers control lights on separate floors rather than vertical rooms stacks, but this is much easier;
// note that the first breaker/room (after the elevator) will be the primary hallway in office buildings and will also control all cameras,
// and the last breaker will be for the extended basement/backrooms if there is one, otherwise the basement/parking garage
float const rooms_per_zone(max(1.0f, float(num_rooms)/num_zones));
unsigned const room_start(round_fp(zone_id*rooms_per_zone)), room_end(min((unsigned)round_fp((zone_id+1)*rooms_per_zone), num_rooms));
if (room_start >= room_end) return breaker_zone_t(); // no rooms
// pick a room with the highest priority for the label
unsigned const room_priorities[NUM_RTYPES] = {0, 2, 1, 1, 2, 2, 3, 3, 3, 2, 1, 3, 2, 3, 3, 3, 2, 2, 2, 2, 3, 3, 0, 3, 3, 0, 4, 3, 4, 4, 4, 0};
unsigned ret_rtype(0), highest_priority(0), pri_room(0);
for (unsigned r = room_start; r < room_end; ++r) {
room_t const &room(rooms[r]);
// use room on the first floor, since it's more likely to be special, unless the zones wrap around (more zones than rooms)
unsigned const floor_ix(min(floor_block_ix, ((unsigned)round_fp(room.dz()/floor_spacing)-1)));
unsigned const rtype(room.get_room_type(floor_ix));
assert(rtype < NUM_RTYPES);
unsigned const priority(room_priorities[rtype] + 1); // add one to be nonzero
if (priority > highest_priority) {ret_rtype = rtype; highest_priority = priority; pri_room = r;}
}
return breaker_zone_t(ret_rtype, room_start, room_end, pri_room);
}
void building_t::toggle_circuit_breaker(bool is_on, unsigned zone_id, unsigned num_zones) {
assert(has_room_geom());
breaker_zone_t const zone(interior->get_circuit_breaker_info(zone_id, num_zones, get_window_vspace()));
if (zone.invalid()) return; // no rooms for this zone
if (zone.rtype == RTYPE_ELEVATOR) { // disable elevator; as long as we don't place breakers in elevators, the player can't get trapped in an elevator
interior->elevators_disabled = !is_on;
interior->room_geom->modified_by_player = 1;
interior->room_geom->invalidate_lights_geom ();
interior->room_geom->update_dynamic_draw_data(); // needed for lit buttons
auto objs_end(interior->room_geom->get_placed_objs_end()); // skip buttons/stairs/elevators
for (auto i = interior->room_geom->objs.begin(); i != objs_end; ++i) {
if (i->type != TYPE_LIGHT || !i->in_elevator() || i->is_lit() == is_on) continue;
i->flags ^= RO_FLAG_LIT; // toggle elevator light lit state
}
return;
}
//for (unsigned r = zone.room_start; r < zone.room_end; ++r) {interior->rooms[r].unpowered = !is_on;} // update room unpowered flags
bool updated(0);
auto objs_start(interior->room_geom->objs.begin());
auto objs_end (interior->room_geom->get_placed_objs_end()); // skip buttons/stairs/elevators
for (auto i = objs_start; i != objs_end; ++i) {
if ((i->is_powered() == is_on) || i->room_id < zone.room_start || i->room_id >= zone.room_end) continue; // no state change, or wrong zone
if (i->is_light_type()) { // light
if (i->in_elevator()) continue; // handled above
bool const was_on(i->is_light_on());
i->flags ^= RO_FLAG_NO_POWER; // need to disable this light and not allow the player/AI/motion detector to turn it back on
bool const state_change(i->is_light_on() != was_on); // update if light on state changed
updated |= state_change;
if (state_change) {register_indir_lighting_state_change(i - objs_start);} // Note: could be slow
}
else if (i->type == TYPE_MONITOR || i->type == TYPE_TV) { // interactive + drawn powered devices
if (i->obj_id != 1) {interior->room_geom->invalidate_draw_data_for_obj(*i);}
i->obj_id = 1; // turn it off
i->flags ^= RO_FLAG_NO_POWER;
}
else if (i->type == TYPE_MWAVE || i->type == TYPE_CEIL_FAN || i->type == TYPE_CAMERA || i->type == TYPE_CLOCK || i->type == TYPE_LAVALAMP || i->type == TYPE_FISHTANK) {
i->flags ^= RO_FLAG_NO_POWER; // interactive powered devices; stove is gas and not electric powered
}
// Note: stoves use gas rather than electricity and don't need power; lit exit signs are always on
} // for i
interior->room_geom->modified_by_player = 1; // I guess we need to set this, to be safe, as this breaker will likely have some effect
if (!updated) return; // that's it, don't need to update geom
// since the state of at least one light has changed, it's likely that other geom has been invalidated, so just update it all
interior->room_geom->invalidate_lights_geom();
interior->room_geom->invalidate_static_geom();
interior->room_geom->invalidate_small_geom ();
interior->room_geom->update_text_draw_data ();
}
// doors and other interactive objects
// used for drawing open doors
int building_t::find_ext_door_close_to_point(tquad_with_ix_t &door, point const &pos, float dist) const {
if (doors.empty()) return -1;
point const query_pt(get_inv_rot_pos(pos));
int const room_id(get_room_containing_pt(query_pt));
cube_t room_exp;
if (room_id >= 0) { // pos is inside a room
room_exp = get_room(room_id);
room_exp.expand_by(get_wall_thickness()); // make sure it contains the door
}
// Note: returns the first exterior door found, assuming there can be at most one within dist of pos
for (auto d = doors.begin(); d != doors.end(); ++d) {
cube_t c(d->get_bcube());
if (room_id >= 0 && !room_exp.contains_cube(c)) continue; // door not in the same room as pos - there is likely a wall between them
c.expand_by_xy(dist);
if (c.contains_pt(query_pt)) {door = *d; return (d - doors.begin());}
} // for d
return -1; // not found
}
bool building_t::point_near_ext_door(point const &pos, float dist) const { // simplified version of above function
if (doors.empty()) return 0;
point const query_pt(get_inv_rot_pos(pos));
for (auto d = doors.begin(); d != doors.end(); ++d) {
if (d->get_bcube().contains_pt_exp(query_pt, dist)) return 1;
}
return 0;
}
// used for pedestrians; pos should be outside the building
bool building_t::get_building_door_pos_closest_to(point const &target_pos, point &door_pos, bool inc_garage_door) const {
float dmin_sq(0.0);
for (auto d = doors.begin(); d != doors.end(); ++d) {
if (d->type == tquad_with_ix_t::TYPE_GDOOR && !inc_garage_door) continue; // skip garage doors
if (d->type == tquad_with_ix_t::TYPE_RDOOR) continue; // skip rooftop doors
point const center(d->get_bcube().get_cube_center());
float const dsq(p2p_dist_xy_sq(target_pos, center)); // ignore zval
if (dmin_sq == 0.0 || dsq < dmin_sq) {door_pos = center; dmin_sq = dsq;}
}
if (dmin_sq == 0.0) return 0; // doors not added for some reason
return 1;
}
void building_t::register_open_ext_door_state(int door_ix) {
bool const is_open(door_ix >= 0), was_open(open_door_ix >= 0);
bool const ring_doorbell(is_open && is_house && door_ix == 0 && city_action_key); // action key when front door of a house is open
if (is_open == was_open && !ring_doorbell) return; // no state change
unsigned const dix(is_open ? (unsigned)door_ix : (unsigned)open_door_ix);
assert(dix < doors.size());
auto const &door(doors[dix]);
point const door_center(door.get_bcube().get_cube_center());
if (ring_doorbell) {
if (!camera_pdu.point_visible_test(door_center + get_camera_coord_space_xlate())) return; // not looking at the door
gen_sound_thread_safe(SOUND_DOORBELL, local_to_camera_space(door_center)); // convert to camera space
return;
}
static float last_sound_tfticks(0);
if ((tfticks - last_sound_tfticks) > 0.25*TICKS_PER_SECOND) { // play at most once every 0.25 second
last_sound_tfticks = tfticks;
play_door_open_close_sound(door_center, is_open);
vector3d const normal(door.get_norm());
bool const dim(fabs(normal.x) < fabs(normal.y)), dir(normal[dim] < 0.0);
point pos_interior(door_center);
pos_interior[dim] += (dir ? 1.0 : -1.0)*CAMERA_RADIUS; // move point to the building interior so that it's a valid AI position
register_building_sound(pos_interior, 0.4); // slightly quieter than interior doors because the user has no control over this
}
open_door_ix = door_ix;
}
bool check_obj_dir_dist(point const &closest_to, vector3d const &in_dir, cube_t const &c, point const ¢er, float dmax) { // Note: only zvals of door are used, no rotate required
if (!c.closest_dist_less_than(closest_to, dmax)) return 0; // too far
if (in_dir == zero_vector) return 1; // no direction filter specified
point vis_pt(center.x, center.y, closest_to.z); // use query point zval
max_eq(vis_pt.z, c.z1()); // clamp visibility test point to z-range of door to allow the player to open the door even looking at the top or bottom of it
min_eq(vis_pt.z, c.z2());
return (dot_product(in_dir, (vis_pt - closest_to).get_norm()) > 0.5); // door is not in the correct direction, skip
}
bool can_open_bathroom_stall_or_shower(room_object_t const &stall, point const &pos, vector3d const &from_dir) {
if (stall.is_broken()) return 0; // broken, can't open
// Note: since there currently aren't any objects the player can push/pull in bathrooms, we don't need to check if the door is blocked from opening; may need to revisit later
bool dim(0), dir(0); // dim/dir that door is on
if (stall.type == TYPE_STALL ) {dim = stall.dim; dir = !stall.dir;} // bathroom stall
else if (stall.type == TYPE_SHOWER) { // shower stall; TYPE_SHOWERTUB can't be opened
dim = (stall.dx() < stall.dy());
dir = !(dim ? stall.dir : stall.dim); // xdir=stall.dim, ydir=stall.dir
}
else {assert(0);}
point door_center;
door_center[ dim] = stall.d[dim][dir];
door_center[!dim] = stall.get_center_dim(!dim);
door_center.z = pos.z;
return (dot_product_ptv(from_dir, door_center, pos) > 0.0); // facing the stall door
}
bool building_t::chair_can_be_rotated(room_object_t const &chair) const {
if (chair.rotates()) return 1;
// check if blocked by another object such as a desk
auto objs_end(interior->room_geom->get_placed_objs_end()); // skip buttons/stairs/elevators
for (auto i = interior->room_geom->objs.begin(); i != objs_end; ++i) {
if (i->type != TYPE_DESK && i->type != TYPE_TABLE && i->type != TYPE_CONF_TABLE) continue; // these should be the only objects a chair can be pushed under
if (i->intersects(chair)) return 0;
}
return 1;
}
void building_t::run_player_interact_logic(point const &camera_bs) {
update_security_cameras(camera_bs);
if (::toggle_room_light) {toggle_room_light(camera_bs);}
if (building_action_key) {apply_player_action_key(camera_bs, cview_dir, (building_action_key-1), 0);} // check_only=0
else {can_do_building_action = apply_player_action_key(camera_bs, cview_dir, 0, 1);} // mode=0, check_only=1
player_pickup_object(camera_bs, cview_dir);
if (animate2) {update_player_interact_objects(camera_bs);} // update dynamic objects if the player is in the building
}
// called for the player; mode: 0=normal, 1=pull
bool building_t::apply_player_action_key(point const &closest_to_in, vector3d const &in_dir_in, int mode, bool check_only, bool no_check_conn_building) {
if (!interior) return 0; // error?
float const dmax(4.0*CAMERA_RADIUS), floor_spacing(get_window_vspace());
float closest_dist_sq(0.0), t(0.0); // t is unused
unsigned door_ix(0), obj_ix(0);
bool found_item(0), is_obj(0);
vector3d in_dir(in_dir_in);
point closest_to(closest_to_in);
maybe_inv_rotate_pos_dir(closest_to, in_dir);
point const query_ray_end(closest_to + dmax*in_dir);
if (mode == 0) { // if the player is in the closet, only the closet door can be opened
for (auto i = interior->doors.begin(); i != interior->doors.end(); ++i) {
if (player_in_closet && !i->is_closet_door()) continue; // only allow the player to open closet doors when in the closet
float const door_z2(i->z2() + (i->on_stairs ? 0.25*floor_spacing : 0.0)); // increase height when on stairs; needed for steep basement stairs
if (i->z1() > closest_to.z || door_z2 < closest_to.z) continue; // wrong floor, skip
point const center(i->get_cube_center());
float const dist_sq(p2p_dist_sq(closest_to, center));
if (found_item && dist_sq >= closest_dist_sq) continue; // not the closest
if (!check_obj_dir_dist(closest_to, in_dir, *i, center, (player_in_closet ? 0.5 : 1.0)*dmax)) continue; // door not in the correct direction or too far away
cube_t const door_bcube(i->get_true_bcube()); // expand to nonzero area
if (!door_bcube.line_intersects(closest_to, query_ray_end)) { // if camera ray doesn't intersect the door frame, check for ray intersection with opened door
if (!i->open || (closest_to[i->dim] < i->d[i->dim][i->open_dir]) == i->open_dir) continue; // closed, or player not on the side the door opens to
tquad_with_ix_t const door(set_interior_door_from_cube(*i));
if (!line_poly_intersect(closest_to, query_ray_end, door.pts, door.npts, door.get_norm(), t)) continue; // test camera ray intersection with door plane
}
closest_dist_sq = dist_sq;
door_ix = (i - interior->doors.begin());
found_item = 1;
} // for i
}
if (has_room_geom()) { // check for closet doors in houses, bathroom stalls in office buildings, and other objects that can be interacted with
vect_room_object_t &objs(interior->room_geom->objs), &expanded_objs(interior->room_geom->expanded_objs);
auto objs_end(interior->room_geom->get_stairs_start());
cube_t active_area;
// make a first pass over all the large objects to determine if the player is inside one; in that case, the player can't reach out and interact with an object outside it
for (auto i = objs.begin(); i != objs_end; ++i) {
if (i->type != TYPE_STALL && i->type != TYPE_SHOWER && i->type != TYPE_ELEVATOR && !(i->type == TYPE_CLOSET && i->is_open())) continue; // TYPE_CUBICLE?
if (!i->contains_pt(closest_to)) continue;
active_area = *i;
break; // there can be only one - done
}
for (unsigned vect_id = 0; vect_id < 2; ++vect_id) {
if (mode > 0) continue; // pull object only mode, skip this step
auto const &obj_vect((vect_id == 1) ? expanded_objs : objs);
unsigned const obj_id_offset((vect_id == 1) ? objs.size() : 0);
auto obj_vect_end((vect_id == 1) ? expanded_objs.end() : objs.end()); // include all objects because blinds are added at the end
for (auto i = obj_vect.begin(); i != obj_vect_end; ++i) {
room_object const type(i->type);
if (cur_player_building_loc.room_ix >= 0 && i->room_id != cur_player_building_loc.room_ix && type != TYPE_BUTTON) continue; // not in the same room as the player
if (!active_area.is_all_zeros() && !i->intersects(active_area)) continue; // out of reach for the player
// check for objects not in the attic when the player is in the attic and vice versa
if (bool(player_in_attic) != i->in_attic() && type != TYPE_ATTIC_DOOR) continue;
bool keep(0);
if (type == TYPE_BOX && !i->is_open()) {keep = 1;} // box can only be opened once; check first so that selection works for boxes in closets
else if (type == TYPE_CLOSET) {
if (i->is_small_closet()) continue; // uses regular door now
if (in_dir.z > 0.5) continue; // not looking up at the light
keep = 1; // closet door can be opened
}
else if (!player_in_closet) {
if ((type == TYPE_TOILET || type == TYPE_URINAL) && !i->is_broken()) {keep = 1;} // toilet/urinal can be flushed unless broken
else if (type == TYPE_STALL && i->shape == SHAPE_CUBE && can_open_bathroom_stall_or_shower(*i, closest_to, in_dir)) {keep = 1;} // bathroom stall can be opened
else if (type == TYPE_MIRROR && i->is_house()) {keep = 1;} // medicine cabinet
else if (i->is_sink_type() || type == TYPE_TUB) {keep = 1;} // sink/tub
else if (i->is_light_type() || type == TYPE_LAVALAMP) {keep = 1;} // room light or lamp
else if (type == TYPE_FISHTANK && i->has_lid()) {keep = 1;} // fishtank with a lid and light
else if (type == TYPE_PICTURE || type == TYPE_TPROLL || type == TYPE_MWAVE || type == TYPE_STOVE || type == TYPE_TV || /*type == TYPE_FRIDGE ||*/
type == TYPE_MONITOR || type == TYPE_BLINDS || type == TYPE_SHOWER || type == TYPE_SHOWERTUB || type == TYPE_SWITCH || type == TYPE_BOOK ||
type == TYPE_BRK_PANEL || type == TYPE_BREAKER || type == TYPE_ATTIC_DOOR || type == TYPE_OFF_CHAIR || type == TYPE_WFOUNTAIN) {keep = 1;}
else if (type == TYPE_LG_BALL && i->has_dstate()) {keep = 1;}
else if (type == TYPE_BUTTON && i->in_elevator() == bool(player_in_elevator)) {keep = 1;} // check for buttons inside/outside elevator
else if (type == TYPE_PIZZA_BOX && !i->was_expanded()) {keep = 1;} // can't open if on a shelf
else if (i->is_parked_car() && !i->is_broken()) {keep = 1;} // parked car with unbroken windows
else if (!check_only && type == TYPE_SHELFRACK && !i->obj_expanded()) {keep = 1;} // expand shelfrack when action key is actually applied
else if (type == TYPE_POOL_BALL && player_has_pool_cue()) {keep = 1;} // can only push pool ball if holding a pool cue
else if (type == TYPE_FALSE_DOOR && !((i->flags & RO_FLAG_WALKWAY) && i->is_interior())) {keep = 1;} // skip walkway only decal doors
}
else if (type == TYPE_LIGHT) {keep = 1;} // closet light
if (!keep) continue;
cube_t obj_bc(*i), dishwasher;
if (type == TYPE_KSINK && get_dishwasher_for_ksink(*i, dishwasher) && dishwasher.line_intersects(closest_to, query_ray_end)) {obj_bc = dishwasher;}
else if (type == TYPE_KSINK || type == TYPE_BRSINK) {obj_bc = get_sink_cube(*i);} // the sink itself is actually smaller
// shrink lamps in XY to a cube interior to their building cylinder to make drawers under lamps easier to select
else if (type == TYPE_LAMP ) {obj_bc.expand_by(vector3d(-i->dx(), -i->dy(), 0.0)*(0.5*(1.0 - 1.0/SQRT2)));}
else if (type == TYPE_ATTIC_DOOR) {obj_bc = get_attic_access_door_cube(*i, 1);} // inc_ladder=1, to make it easier to select when in the attic
else if (type == TYPE_SHOWERTUB ) {obj_bc.z1() += 0.4*i->dz();} // use upper part so that tub can be interacted with as well
point center;
if (type == TYPE_CLOSET) {
center = i->get_cube_center();
center[i->dim] = i->d[i->dim][i->dir]; // use center of door, not center of closet
}
else {center = obj_bc.closest_pt(closest_to);}
if (fabs(center.z - closest_to.z) > 0.7*floor_spacing) continue; // wrong floor
// use dmax for closets and open breaker boxes to prioritize objects inside
bool const low_priority(type == TYPE_CLOSET || (type == TYPE_BRK_PANEL && i->is_open()));
float const dist_sq(low_priority ? dmax*dmax : p2p_dist_sq(closest_to, center));
if (found_item && dist_sq >= closest_dist_sq) continue; // not the closest
if (!obj_bc.closest_dist_less_than(closest_to, dmax)) continue; // too far
if (in_dir != zero_vector && !obj_bc.line_intersects(closest_to, query_ray_end)) continue; // player is not pointing at this object
// checking for office chair rotation is expensive, so it's done last, just before updating closest
if (type == TYPE_OFF_CHAIR && !chair_can_be_rotated(*i)) continue;
closest_dist_sq = dist_sq;
obj_ix = (i - obj_vect.begin()) + obj_id_offset;
is_obj = found_item = 1;
} // for i
} // for vect_id
if (!player_in_closet && mode == 0) {
float const drawer_dist(found_item ? sqrt(closest_dist_sq) : 2.5*CAMERA_RADIUS);
if (interior->room_geom->open_nearest_drawer(*this, closest_to, in_dir, drawer_dist, 0, check_only)) { // drawer is closer - open or close it
return (check_only ? 1 : 0); // check_only returns 1 here because this counts as interactive
}
}
if (!found_item && !check_only && !player_in_closet) {move_nearest_object(closest_to, in_dir, 3.0*CAMERA_RADIUS, mode);} // try to move an object instead
}
if (!found_item) { // no door or object found
if (no_check_conn_building) return 0; // avoid infinite recursion
building_t *const conn_building(get_conn_bldg_for_pt(closest_to)); // check for door in connected building; should we check if room is ext conn first?
if (conn_building == nullptr) return 0; // no other building
return conn_building->apply_player_action_key(closest_to_in, in_dir_in, mode, check_only, 1); // chain call to the other building; no_check_conn_building=1
}
if (check_only) return 1;
if (is_obj) { // interactive object
if (!interact_with_object(obj_ix, closest_to, query_ray_end, in_dir)) return 0; // generate sound from the player height
}
else { // interior door
door_t &door(interior->doors[door_ix]);
if (!player_can_open_door(door)) return 0; // locked/blocked
if (door.is_padlocked() && !door.open) {remove_padlock_from_door(door_ix, closest_to);}
if (door.locked && !player_has_room_key()) {door.locked = 0;} // don't lock door when closing, to prevent the player from locking themselves in a room
toggle_door_state(door_ix, 1, 1, closest_to); // toggle state if interior door; player_in_this_building=1, by_player=1, at player pos
//interior->room_geom->modified_by_player = 1; // should door state always be preserved?
}
return 1;
}
void make_object_dynamic(room_object_t &obj, building_interior_t &interior) {
if (obj.is_dynamic()) return; // already dynamic
obj.flags |= RO_FLAG_DYNAMIC;
interior.update_dynamic_draw_data();
interior.room_geom->invalidate_small_geom();
}
void obj_dynamic_to_static(room_object_t &obj, building_interior_t &interior) {
obj.flags &= ~RO_FLAG_DYNAMIC; // clear dynamic flag
interior.update_dynamic_draw_data(); // remove from dynamic objects and schedule an update
interior.room_geom->invalidate_small_geom(); // add to small static objects
}
bool building_t::interact_with_object(unsigned obj_ix, point const &int_pos, point const &query_ray_end, vector3d const &int_dir) {
auto &obj(interior->room_geom->get_room_object_by_index(obj_ix));
point const sound_origin(obj.xc(), obj.yc(), int_pos.z), local_center(local_to_camera_space(sound_origin)); // generate sound from the player height
float sound_scale(0.5); // for building sound level
bool update_draw_data(0);
cube_t dishwasher;
if (obj.type == TYPE_TOILET || obj.type == TYPE_URINAL) { // toilet/urinal can be flushed, but otherwise is not modified
bool const is_urinal(obj.type == TYPE_URINAL); // urinal is quieter and higher pitch
gen_sound_thread_safe(SOUND_FLUSH, local_center, (is_urinal ? 0.5 : 1.0), (is_urinal ? 1.25 : 1.0));
sound_scale = 0.5;
//refill_thirst(); // player can drink from toilet?
}
else if (obj.type == TYPE_KSINK && get_dishwasher_for_ksink(obj, dishwasher) && dishwasher.line_intersects(int_pos, query_ray_end)) { // dishwasher
gen_sound_thread_safe_at_player(SOUND_METAL_DOOR, 0.2, 0.75);
obj.flags ^= RO_FLAG_OPEN; // toggle open/closed
sound_scale = 0.5;
update_draw_data = 1;
// since TYPE_KSINK already uses the RO_FLAG_EXPANDED flag for cabinet doors, we have to use the RO_FLAG_USED for dishwasher expansion
if (obj.is_open() && !obj.is_used()) { // newly opened
interior->room_geom->expand_dishwasher(obj, dishwasher);
obj.flags |= RO_FLAG_USED; // can't expand again
}
else if (!obj.is_open() && obj.is_used()) { // closed
interior->room_geom->unexpand_dishwasher(obj, dishwasher);
obj.flags &= ~RO_FLAG_USED; // can now expand again
}
}
else if (obj.is_sink_type() || obj.type == TYPE_TUB) { // sink or tub
if (!obj.is_active() && obj.type == TYPE_TUB) {
gen_sound_thread_safe(SOUND_SINK, local_center); // play sound when turning the tub on
if (obj.state_flags < 4) { // water level is 0-4
++obj.state_flags;
interior->room_geom->invalidate_static_geom();
}
//refill_thirst(); // player can drink from tub?
}
if (obj.is_sink_type()) {
obj.flags ^= RO_FLAG_IS_ACTIVE; // toggle active bit, only for sinks for now
if (obj.is_active() && obj.state_flags == 0) { // no water yet
obj.state_flags ^= 1; // mark as filled with water
interior->room_geom->invalidate_static_geom();
}
refill_thirst(); // player can drink from sink
}
sound_scale = 0.4;
}
else if (obj.is_light_type()) {
toggle_light_object(obj, obj.get_cube_center());
sound_scale = 0.0; // sound has already been registered above
}
else if (obj.type == TYPE_TPROLL) {
if (!obj.is_hanging() && !obj.was_expanded()) {
gen_sound_thread_safe(SOUND_FOOTSTEP, local_center, 0.5, 1.5); // could be better
obj.flags |= RO_FLAG_HANGING; // pull down the roll
update_draw_data = 1;
}
sound_scale = 0.0; // no sound
}
else if (obj.type == TYPE_PICTURE) { // tilt the picture
obj.flags |= RO_FLAG_RAND_ROT;
++obj.item_flags; // choose a different random rotation
gen_sound_thread_safe(SOUND_SLIDING, local_center, 0.25, 2.0); // higher pitch
sound_scale = 0.0; // no sound
update_draw_data = 1;
}
else if (obj.type == TYPE_OFF_CHAIR) { // handle rotate of office chair
office_chair_rot_rate += 0.1;
obj.flags |= RO_FLAG_ROTATING; // Note: this is a model, no need to regen vertex data
gen_sound_thread_safe(SOUND_SQUEAK, local_center, 0.25, 0.5); // lower pitch
sound_scale = 0.2;
}
else if (obj.type == TYPE_MWAVE) {
cube_t const panel(get_mwave_panel_bcube(obj));
float cur_tmin(0.0), cur_tmax(1.0);
if (!get_line_clip(int_pos, query_ray_end, panel.d, cur_tmin, cur_tmax)) { // not pointing at the panel - open and close the door
obj.flags ^= RO_FLAG_OPEN; // toggle open/closed
update_draw_data = 1;
gen_sound_thread_safe((obj.is_open() ? (unsigned)SOUND_DOOR_OPEN : (unsigned)SOUND_DOOR_CLOSE), local_center, 0.5, 1.6);
}
else if (obj.is_powered()) { // pointing at the panel - make it beep
gen_sound_thread_safe(SOUND_BEEP, local_center, 0.25);
sound_scale = 0.6;
}
}
else if (obj.type == TYPE_STOVE) { // toggle burners; doesn't need power
float const height(obj.dz());
bool const dim(obj.dim), dir(obj.dir);
unsigned burner_id(0);
float tmin(1.0);
for (unsigned w = 0; w < 2; ++w) { // width dim
for (unsigned d = 0; d < 2; ++d) { // depth dim
bool wv(bool(w) ^ dim ^ dir ^ 1), dv(bool(d) ^ dir);
cube_t c(obj);
set_cube_zvals(c, (c.z1() + 0.7*height), (c.z1() + 0.8*height)); // select the cook top area
((dim ? wv : dv) ? c.x1() : c.x2()) = c.xc();
((dim ? dv : wv) ? c.y1() : c.y2()) = c.yc();
float cur_tmin(0.0), cur_tmax(1.0);
if (!get_line_clip(int_pos, query_ray_end, c.d, cur_tmin, cur_tmax) || tmin < cur_tmin) continue;
tmin = cur_tmin;
burner_id = (2U*w + d); // this point intersects earlier - select this burner
} // for d
} // for w
if (tmin < 1.0) { // found an intersection
unsigned const flag_mask(1U<<burner_id);
bool const is_on(obj.item_flags & flag_mask);
if (is_on) { // currently on, turn off
obj.item_flags &= ~flag_mask;
gen_sound_thread_safe(SOUND_CLICK, local_center, 0.75);
sound_scale = 0.1;
}
else { // currently off, turn on
obj.item_flags |= flag_mask;
gen_sound_thread_safe(SOUND_HISS, local_center, 0.25, 0.6);
sound_scale = 0.3;
}
}
}
else if (obj.type == TYPE_FRIDGE) {
obj.flags ^= RO_FLAG_OPEN; // toggle open/closed
update_draw_data = 1;
gen_sound_thread_safe((obj.is_open() ? (unsigned)SOUND_DOOR_OPEN : (unsigned)SOUND_DOOR_CLOSE), local_center, 0.4, 0.67);
}
else if (obj.type == TYPE_TV || obj.type == TYPE_MONITOR) {
if (obj.is_powered()) {
if (!obj.is_broken()) { // no visual effect if broken, but still clicks
if (obj.type == TYPE_MONITOR && (obj.obj_id & 1)) {--obj.obj_id;} // toggle on and off, but don't change the desktop
else {++obj.obj_id;} // toggle on/off, and also change the picture
update_draw_data = 1;
}
gen_sound_thread_safe(SOUND_CLICK, local_center, 0.4);
}
}
else if (obj.type == TYPE_BUTTON) { // Note: currently, buttons are only used for elevators
if (!obj.is_active() && !interior->elevators_disabled) { // if not already active
register_button_event(obj);
obj.flags |= RO_FLAG_IS_ACTIVE;
interior->room_geom->invalidate_draw_data_for_obj(obj); // need to regen object data due to lit state change; don't have to set modified_by_player
}
}
else if (obj.type == TYPE_SWITCH) {
// should select the correct light(s) for the room containing the switch
toggle_room_light(obj.get_cube_center(), 1, obj.room_id, 0, obj.in_closet(), obj.in_attic()); // exclude lamps; select closet lights if a closet light switch
gen_sound_thread_safe_at_player(SOUND_CLICK, 0.5);
obj.flags ^= RO_FLAG_OPEN; // toggle on/off
sound_scale = 0.1;
update_draw_data = 1;
}
else if (obj.type == TYPE_BREAKER) {
gen_sound_thread_safe_at_player(SOUND_CLICK, 1.0);
obj.flags ^= RO_FLAG_OPEN; // toggle on/off
toggle_circuit_breaker(obj.is_open(), obj.obj_id, obj.item_flags);
sound_scale = 0.25;
update_draw_data = 1;
}
else if (obj.type == TYPE_BLINDS) { // see building_t::add_window_blinds()
if (!adjust_blinds_state(obj_ix)) return 0;
gen_sound_thread_safe_at_player(SOUND_SLIDING, 0.5);
sound_scale = 0.3;
update_draw_data = 1;
}
else if (obj.type == TYPE_BOOK) {
if (!obj.is_open()) { // check if there's space to open the book
room_object_t open_area(obj); // the area that must be clear if we want to open this book
open_area.z1() += 0.75*obj.dz(); // shift the bottom up to keep it from intersecting whatever this book is resting on
open_area.translate_dim(obj.dim, 1.01*(obj.dir ? -1.0 : 1.0)*obj.get_sz_dim(obj.dim)); // translate more than width to keep it from overlapping obj
if (!is_obj_pos_valid(open_area, 0, 1, 0)) return 0; // intersects some part of the building; keep_in_room=0, allow_block_door=1, check_stairs=0
if (overlaps_any_placed_obj(open_area)) return 0;
}
if (!check_for_water_splash(sound_origin, 0.4, 1)) {gen_sound_thread_safe_at_player(SOUND_OBJ_FALL, 0.25);} // splash or drop; full_room_height=1
obj.flags ^= RO_FLAG_OPEN; // toggle open/closed
sound_scale = 0.1; // very little sound
update_draw_data = 1;
}
else if (obj.type == TYPE_SHOWER) { // shower
// if (interior->room_geom->cube_intersects_moved_obj(c_test)) continue; // not yet needed
if (can_open_bathroom_stall_or_shower(obj, int_pos, int_dir)) { // open/close shower door
obj.flags ^= RO_FLAG_OPEN; // toggle open/close
sound_scale = 0.35;
update_draw_data = 1;
play_open_close_sound(obj, sound_origin);
}
else { // turn on shower water
gen_sound_thread_safe_at_player(SOUND_SINK);
sound_scale = 0.5;
if (!obj.is_open()) {register_achievement("Squeaky Clean");}
if (!obj.state_flags) {
obj.state_flags = 1; // mark as filled with water
interior->room_geom->invalidate_static_geom();
}
}
}
else if (obj.type == TYPE_SHOWERTUB) { // open/close curtains
bool const side(query_ray_end[!obj.dim] > obj.get_center_dim(!obj.dim));
if (obj.taken_level & (1 << unsigned(side))) return 0; // already taken
obj.flags ^= (side ? RO_FLAG_IS_ACTIVE : RO_FLAG_OPEN); // toggle open/close using two different flags for the left vs. right curtains
gen_sound_thread_safe_at_player(SOUND_SLIDING, 0.5, 1.5);
sound_scale = 0.4;
update_draw_data = 1;
}
else if (obj.type == TYPE_BOX) {
if (!check_for_water_splash(sound_origin, 0.6)) {gen_sound_thread_safe_at_player(SOUND_OBJ_FALL, 0.5);}
obj.flags |= RO_FLAG_OPEN; // mark as open
sound_scale = 0.2;
update_draw_data = 1;
}
else if (obj.type == TYPE_PIZZA_BOX) {
gen_sound_thread_safe_at_player(SOUND_SLIDING, 0.1);
obj.flags ^= RO_FLAG_OPEN; // toggle open/close
sound_scale = 0.0; // no sound
update_draw_data = 1;
}
else if (obj.type == TYPE_CLOSET || obj.type == TYPE_STALL) {
if (!obj.is_open()) { // not yet open
// remove any spraypaint or marker that's on the door; would be better if we could move it with the door, or add it back when the door is closed
cube_t door(get_open_closet_door(obj));
door.expand_in_dim(obj.dim, get_wall_thickness());
remove_paint_in_cube(door); // use the door before it's opened
}
obj.flags ^= RO_FLAG_OPEN; // toggle open/close
if (obj.type == TYPE_CLOSET) {
interior->room_geom->expand_object(obj, *this); // expand any boxes so that the player can pick them up
sound_scale = 0.25; // closets are quieter, to allow players to more easily hide
}
play_open_close_sound(obj, sound_origin);
register_indir_lighting_geom_change();
update_draw_data = 1;
}
else if (obj.type == TYPE_MIRROR && obj.is_house()) { // medicine cabinet
obj.flags ^= RO_FLAG_OPEN; // toggle open/close
interior->room_geom->expand_object(obj, *this);
play_open_close_sound(obj, sound_origin);
sound_scale = 0.4;
update_draw_data = 1;
}
else if (obj.type == TYPE_BRK_PANEL) { // breaker panel
obj.flags ^= RO_FLAG_OPEN; // toggle open/close
interior->room_geom->expand_object(obj, *this);
play_open_close_sound(obj, sound_origin);
sound_scale = 0.6;
update_draw_data = 1;
}
else if (obj.type == TYPE_ATTIC_DOOR) {
gen_sound_thread_safe_at_player(SOUND_SLIDING, 1.0); // better sound?
obj.flags ^= RO_FLAG_OPEN; // open/close
sound_scale = 0.5;
update_draw_data = 1;
interior->attic_access_open ^= 1;
// toggle the attic light as well
auto objs_end(interior->room_geom->get_placed_objs_end()); // skip buttons/stairs/elevators
for (auto i = interior->room_geom->objs.begin(); i != objs_end; ++i) {
if (i->type != TYPE_LIGHT || !i->in_attic()) continue; // not attic light
if (i->is_lit() != obj.is_open()) {toggle_light_object(*i, sound_origin);}
}
}
else if (obj.type == TYPE_LAVALAMP) {
obj.flags ^= RO_FLAG_LIT; // toggle lit
update_draw_data = 1;
gen_sound_thread_safe(SOUND_CLICK, local_center, 0.35);
}
else if (obj.type == TYPE_FISHTANK) {
obj.flags ^= RO_FLAG_LIT; // toggle the light on the lid; no draw data update
gen_sound_thread_safe(SOUND_CLICK, local_center, 0.4);
}
else if (obj.type == TYPE_WFOUNTAIN) {
refill_thirst();
gen_sound_thread_safe(SOUND_GULP, local_center, 0.5);
sound_scale = 0.1; // very little sound
}
else if (obj.type == TYPE_FALSE_DOOR) { // locked, can't open
print_text_onscreen("Door is locked", RED, 1.0, 2.0*TICKS_PER_SECOND, 0);
gen_sound_thread_safe_at_player(SOUND_CLICK, 1.0, 0.6);
return 0;
}
else if (obj.type == TYPE_SHELFRACK) { // expand shelfrack
interior->room_geom->expand_object(obj, *this);
}
else if (is_ball_type(obj.type)) { // push the ball
assert(obj.has_dstate());
room_obj_dstate_t &dstate(interior->room_geom->get_dstate(obj));
dstate.velocity.x += 0.5*KICK_VELOCITY*int_dir.x;
dstate.velocity.y += 0.5*KICK_VELOCITY*int_dir.y;
make_object_dynamic(obj, *interior);
}
else if (obj.is_parked_car()) {
gen_sound_thread_safe_at_player(SOUND_GLASS);
register_broken_object(obj);
add_broken_glass_to_floor(int_pos, 0.8*CAMERA_RADIUS);
assert(!obj.is_broken());
obj.flags |= RO_FLAG_BROKEN;
sound_scale = 1.0; // loud sound, but no update of draw data
interior->room_geom->modified_by_player = 1;
}
else {assert(0);} // unhandled type
if (update_draw_data) {interior->room_geom->update_draw_state_for_room_object(obj, *this, 0);}
if (sound_scale > 0.0) {register_building_sound(sound_origin, sound_scale);}
if (obj.type == TYPE_BOX) {add_box_contents(obj);} // must be done last to avoid reference invalidation
return 1;
}
bool building_t::adjust_blinds_state(unsigned obj_ix) {
auto &obj(interior->room_geom->get_room_object_by_index(obj_ix));
if (obj.is_hanging()) { // hanging horizontal blinds
float const floor_spacing(get_window_vspace()), window_v_border(0.94*get_window_v_border()); // border_mult=0.94 to account for the frame
float const window_height(floor_spacing*(1.0 - 2.0*window_v_border)), blinds_height(obj.dz());
assert(window_height > 0.0);
bool const mostly_open(blinds_height < 0.5*window_height);
if (mostly_open) {obj.z1() = obj.z2() - floor_spacing*(1.0 - window_v_border) + 0.05*floor_spacing;} // close the blinds fully
else {obj.z1() = obj.z2() - (1.8*get_wall_thickness() + 0.05*floor_spacing);} // open the blinds
// set new thickness (matches building_t::add_window_blinds())
obj.d[obj.dim][!obj.dir] = obj.d[obj.dim][obj.dir] + (mostly_open ? 0.0927 : 0.227)*get_wall_thickness()*(obj.dir ? -1.0 : 1.0);
}
else { // vertical blinds - in pairs
assert(obj.flags & (RO_FLAG_ADJ_LO | RO_FLAG_ADJ_HI)); // should have had one of these flags set
bool const move_dir(obj.flags & RO_FLAG_ADJ_HI);
unsigned other_blinds_ix(0);
if (move_dir) { // this is the left side blind, the other side is to the right in the next slot
other_blinds_ix = obj_ix + 1;
}
else { // this is the right side blind, the other side is to the left in the previous slot
assert(obj_ix > 0);
other_blinds_ix = obj_ix - 1;
}
auto &other_blinds(interior->room_geom->get_room_object_by_index(other_blinds_ix));
if (other_blinds.type != TYPE_BLINDS) {assert(0); return 0;} // was taken, etc.
float const fixed_end(obj.d[!obj.dim][!move_dir]), width(obj.get_width());
float const window_center(0.5f*(fixed_end + other_blinds.d[!obj.dim][move_dir])); // center of the span of the pair of left/right blinds
bool const mostly_open(width < 0.5*fabs(fixed_end - window_center));
float &move_edge(obj.d[!obj.dim][move_dir]);
if (mostly_open) {move_edge = window_center;} // close the blinds fully
else {move_edge = 0.25*window_center + 0.75*fixed_end;} // open the blinds
}
assert(obj.is_strictly_normalized());
register_blinds_state_change();
return 1;
}
void building_t::toggle_door_state(unsigned door_ix, bool player_in_this_building, bool by_player, point const &actor_pos) { // called by the player or AI
assert(interior);
door_t &door(interior->get_door(door_ix));
door.toggle_open_state(/*by_player*/player_in_this_building); // allow partial open/animated door if player is in this building
// we changed the door state, but navigation should adapt to this, except for doors on stairs (which are special)
if ( door.on_stairs) {invalidate_nav_graph();} // any in-progress paths may have people walking to and stopping at closed/locked doors
if (!door.get_for_closet()) {interior->door_state_updated = 1;} // required for AI navigation logic to adjust to this change; what about backrooms doors?
if (has_room_geom()) {interior->room_geom->invalidate_mats_mask |= (1 << MAT_TYPE_DOORS);} // need to recreate doors VBO
check_for_water_splash(cube_bot_center(door), 2.0); // big splash
if (player_in_this_building || by_player) { // is it really safe to call this from the AI thread?
point door_center(door.xc(), door.yc(), actor_pos.z);
// if door was opened or is fully closed play a sound; otherwise, play the close sound later when fully closed
if (door.open || door.open_amt == 0.0) {play_door_open_close_sound(door_center, door.open);}
if (by_player) {
// bias the sound slightly toward the side of the door the player is on to force a zombie to go through the doorway to get there,
// rather than targeting the exact center of the doorway and possibly clipping through the wall (for example, if in a hallway)
door_center[door.dim] += get_wall_thickness()*((door_center[door.dim] < actor_pos[door.dim]) ? 1.0 : -1.0);
register_building_sound(door_center, 0.5);
}
// update indir lighting state if needed; for now this is only for player actions to avoid thread safety issues and too many updates
if (by_player && enable_building_indir_lighting()) {
// Note: only have to register geom change if the light is on the same floor as the player, but this should always be true if the player just closed this door
register_indir_lighting_geom_change();
static vector<unsigned> light_ids;
get_lights_near_door(door_ix, light_ids);
for (unsigned light_ix : light_ids) {register_indir_lighting_state_change(light_ix, 1);} // is_door_change=1
}
}
handle_items_intersecting_closed_door(door); // check if we need to move any objects out of the way
if (door.open) { // was closed and now open
notify_door_fully_closed_state(door);
cube_t door_exp(door);
door_exp.expand_in_dim(door.dim, 0.6*get_wall_thickness()); // make sure decals are included
remove_paint_in_cube(door_exp); // remove any paint that was over the closed door
}
}
void building_t::notify_door_fully_closed_state(door_t const &door) {
if (door.obj_ix < 0) return; // no associated object
if (!has_room_geom()) return; // error?
assert((unsigned)door.obj_ix < interior->room_geom->objs.size());
room_object_t &obj(interior->room_geom->objs[door.obj_ix]);
if (obj.type == TYPE_CLOSET) { // this was a closet door