]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/t_plats.qc
Don't spawn an empty entity for fallback origin, that's bad. Use a vector instead...
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / t_plats.qc
1 .float dmgtime2;
2 void generic_plat_blocked()
3 {
4     if(self.dmg && other.takedamage != DAMAGE_NO) {
5         if(self.dmgtime2 < time) {
6             Damage (other, self, self, self.dmg, DEATH_HURTTRIGGER, other.origin, '0 0 0');
7             self.dmgtime2 = time + self.dmgtime;
8         }
9
10         // Gib dead/dying stuff
11         if(other.deadflag != DEAD_NO)
12             Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
13     }
14 }
15
16
17 float   STATE_TOP               = 0;
18 float   STATE_BOTTOM    = 1;
19 float   STATE_UP                = 2;
20 float   STATE_DOWN              = 3;
21
22 .entity trigger_field;
23
24 void() plat_center_touch;
25 void() plat_outside_touch;
26 void() plat_trigger_use;
27 void() plat_go_up;
28 void() plat_go_down;
29 void() plat_crush;
30 float PLAT_LOW_TRIGGER = 1;
31
32 void plat_spawn_inside_trigger()
33 {
34         entity trigger;
35         vector tmin, tmax;
36
37         trigger = spawn();
38         trigger.touch = plat_center_touch;
39         trigger.movetype = MOVETYPE_NONE;
40         trigger.solid = SOLID_TRIGGER;
41         trigger.enemy = self;
42
43         tmin = self.absmin + '25 25 0';
44         tmax = self.absmax - '25 25 -8';
45         tmin_z = tmax_z - (self.pos1_z - self.pos2_z + 8);
46         if (self.spawnflags & PLAT_LOW_TRIGGER)
47                 tmax_z = tmin_z + 8;
48
49         if (self.size_x <= 50)
50         {
51                 tmin_x = (self.mins_x + self.maxs_x) / 2;
52                 tmax_x = tmin_x + 1;
53         }
54         if (self.size_y <= 50)
55         {
56                 tmin_y = (self.mins_y + self.maxs_y) / 2;
57                 tmax_y = tmin_y + 1;
58         }
59
60         if(tmin_x < tmax_x)
61                 if(tmin_y < tmax_y)
62                         if(tmin_z < tmax_z)
63                         {
64                                 setsize (trigger, tmin, tmax);
65                                 return;
66                         }
67
68         // otherwise, something is fishy...
69         remove(trigger);
70         objerror("plat_spawn_inside_trigger: platform has odd size or lip, can't spawn");
71 }
72
73 void plat_hit_top()
74 {
75         sound (self, CH_TRIGGER_SINGLE, self.noise1, VOL_BASE, ATTN_NORM);
76         self.state = 1;
77         self.think = plat_go_down;
78         self.nextthink = self.ltime + 3;
79 }
80
81 void plat_hit_bottom()
82 {
83         sound (self, CH_TRIGGER_SINGLE, self.noise1, VOL_BASE, ATTN_NORM);
84         self.state = 2;
85 }
86
87 void plat_go_down()
88 {
89         sound (self, CH_TRIGGER_SINGLE, self.noise, VOL_BASE, ATTN_NORM);
90         self.state = 3;
91         SUB_CalcMove (self.pos2, self.speed, plat_hit_bottom);
92 }
93
94 void plat_go_up()
95 {
96         sound (self, CH_TRIGGER_SINGLE, self.noise, VOL_BASE, ATTN_NORM);
97         self.state = 4;
98         SUB_CalcMove (self.pos1, self.speed, plat_hit_top);
99 }
100
101 void plat_center_touch()
102 {
103         if not(other.iscreature)
104                 return;
105
106         if (other.health <= 0)
107                 return;
108
109         self = self.enemy;
110         if (self.state == 2)
111                 plat_go_up ();
112         else if (self.state == 1)
113                 self.nextthink = self.ltime + 1;        // delay going down
114 }
115
116 void plat_outside_touch()
117 {
118         if not(other.iscreature)
119                 return;
120
121         if (other.health <= 0)
122                 return;
123
124         self = self.enemy;
125         if (self.state == 1)
126                 plat_go_down ();
127 }
128
129 void plat_trigger_use()
130 {
131         if (self.think)
132                 return;         // already activated
133         plat_go_down();
134 }
135
136
137 void plat_crush()
138 {
139     if((self.spawnflags & 4) && (other.takedamage != DAMAGE_NO)) { // KIll Kill Kill!!
140         Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
141     } else {
142         if((self.dmg) && (other.takedamage != DAMAGE_NO)) {   // Shall we bite?
143             Damage (other, self, self, self.dmg, DEATH_HURTTRIGGER, other.origin, '0 0 0');
144             // Gib dead/dying stuff
145             if(other.deadflag != DEAD_NO)
146                 Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
147         }
148
149         if (self.state == 4)
150             plat_go_down ();
151         else if (self.state == 3)
152             plat_go_up ();
153         // when in other states, then the plat_crush event came delayed after
154         // plat state already had changed
155         // this isn't a bug per se!
156     }
157 }
158
159 void plat_use()
160 {
161         self.use = SUB_Null;
162         if (self.state != 4)
163                 objerror ("plat_use: not in up state");
164         plat_go_down();
165 }
166
167 .string sound1, sound2;
168
169 void plat_reset()
170 {
171         IFTARGETED
172         {
173                 setorigin (self, self.pos1);
174                 self.state = 4;
175                 self.use = plat_use;
176         }
177         else
178         {
179                 setorigin (self, self.pos2);
180                 self.state = 2;
181                 self.use = plat_trigger_use;
182         }
183 }
184
185 void spawnfunc_path_corner() { }
186 void spawnfunc_func_plat()
187 {
188         if (self.sounds == 0)
189                 self.sounds = 2;
190
191     if(self.spawnflags & 4)
192         self.dmg = 10000;
193
194     if(self.dmg && (!self.message))
195                 self.message = "was squished";
196     if(self.dmg && (!self.message2))
197                 self.message2 = "was squished by";
198
199         if (self.sounds == 1)
200         {
201                 precache_sound ("plats/plat1.wav");
202                 precache_sound ("plats/plat2.wav");
203                 self.noise = "plats/plat1.wav";
204                 self.noise1 = "plats/plat2.wav";
205         }
206
207         if (self.sounds == 2)
208         {
209                 precache_sound ("plats/medplat1.wav");
210                 precache_sound ("plats/medplat2.wav");
211                 self.noise = "plats/medplat1.wav";
212                 self.noise1 = "plats/medplat2.wav";
213         }
214
215         if (self.sound1)
216         {
217                 precache_sound (self.sound1);
218                 self.noise = self.sound1;
219         }
220         if (self.sound2)
221         {
222                 precache_sound (self.sound2);
223                 self.noise1 = self.sound2;
224         }
225
226         self.mangle = self.angles;
227         self.angles = '0 0 0';
228
229         self.classname = "plat";
230         if not(InitMovingBrushTrigger())
231                 return;
232         self.effects |= EF_LOWPRECISION;
233         setsize (self, self.mins , self.maxs);
234
235         self.blocked = plat_crush;
236
237         if (!self.speed)
238                 self.speed = 150;
239         if (!self.lip)
240                 self.lip = 16;
241         if (!self.height)
242                 self.height = self.size_z - self.lip;
243
244         self.pos1 = self.origin;
245         self.pos2 = self.origin;
246         self.pos2_z = self.origin_z - self.height;
247
248         self.reset = plat_reset;
249         plat_reset();
250
251         plat_spawn_inside_trigger ();   // the "start moving" trigger
252 }
253
254
255 void() train_next;
256 void train_wait()
257 {
258         if(self.noise != "")
259                 stopsoundto(MSG_BROADCAST, self, CH_TRIGGER_SINGLE); // send this as unreliable only, as the train will resume operation shortly anyway
260
261         if(self.wait < 0)
262         {
263                 train_next();
264         }
265         else
266         {
267                 self.think = train_next;
268                 self.nextthink = self.ltime + self.wait;
269         }
270
271         entity oldself;
272         oldself = self;
273         self = self.enemy;
274         SUB_UseTargets();
275         self = oldself;
276         self.enemy = world;
277 }
278
279 void train_next()
280 {
281         entity targ, cp;
282         vector cp_org;
283
284         targ = find(world, targetname, self.target);
285         self.target = targ.target;
286         if (self.spawnflags & 1)
287         {
288                 entity prev;
289                 prev = find(world, target, targ.targetname); // get the previous corner first
290                 cp = find(world, targetname, prev.target2); // now get its second target (the control point)
291                 if(cp.targetname == "") // none found
292                 {
293                         // when using bezier curves, you must have a control point for each corner in the path
294                         if(autocvar_developer)
295                                 dprint(strcat("Warning: func_train using beizer curves reached the path_corner '", prev.targetname, "' which does not have a control point. Please add a target2 for each path_corner used by this train!\n"));
296                         cp_org = targ.origin - self.mins; // assume a straight line to the destination as fallback
297                 }
298                 else
299                         cp_org = cp.origin;
300         }
301         if (!self.target)
302                 objerror("train_next: no next target");
303         self.wait = targ.wait;
304         if (!self.wait)
305                 self.wait = 0.1;
306
307         if (targ.speed)
308         {
309                 if (self.spawnflags & 1)
310                         SUB_CalcMove_Bezier(cp_org, targ.origin - self.mins, targ.speed, train_wait);
311                 else
312                         SUB_CalcMove(targ.origin - self.mins, targ.speed, train_wait);
313         }
314         else
315         {
316                 if (self.spawnflags & 1)
317                         SUB_CalcMove_Bezier(cp_org, targ.origin - self.mins, self.speed, train_wait);
318                 else
319                         SUB_CalcMove(targ.origin - self.mins, self.speed, train_wait);
320         }
321
322         if(self.noise != "")
323                 sound(self, CH_TRIGGER_SINGLE, self.noise, VOL_BASE, ATTN_IDLE);
324 }
325
326 void func_train_find()
327 {
328         entity targ;
329         targ = find(world, targetname, self.target);
330         self.target = targ.target;
331         if (!self.target)
332                 objerror("func_train_find: no next target");
333         setorigin(self, targ.origin - self.mins);
334         self.nextthink = self.ltime + 1;
335         self.think = train_next;
336 }
337
338 /*QUAKED spawnfunc_func_train (0 .5 .8) ?
339 Ridable platform, targets spawnfunc_path_corner path to follow.
340 speed : speed the train moves (can be overridden by each spawnfunc_path_corner)
341 target : targetname of first spawnfunc_path_corner (starts here)
342 */
343 void spawnfunc_func_train()
344 {
345         if (self.noise != "")
346                 precache_sound(self.noise);
347
348         if (!self.target)
349                 objerror("func_train without a target");
350         if (!self.speed)
351                 self.speed = 100;
352
353         if not(InitMovingBrushTrigger())
354                 return;
355         self.effects |= EF_LOWPRECISION;
356
357         // wait for targets to spawn
358         InitializeEntity(self, func_train_find, INITPRIO_SETLOCATION);
359
360         self.blocked = generic_plat_blocked;
361         if(self.dmg & (!self.message))
362                 self.message = " was squished";
363     if(self.dmg && (!self.message2))
364                 self.message2 = "was squished by";
365         if(self.dmg && (!self.dmgtime))
366                 self.dmgtime = 0.25;
367         self.dmgtime2 = time;
368
369         // TODO make a reset function for this one
370 }
371
372 void func_rotating_setactive(float astate)
373 {
374         
375         if (astate == ACTIVE_TOGGLE)
376         {               
377                 if(self.active == ACTIVE_ACTIVE)
378                         self.active = ACTIVE_NOT;
379                 else
380                         self.active = ACTIVE_ACTIVE;
381         }
382         else
383                 self.active = astate;
384                 
385         if(self.active  == ACTIVE_NOT)          
386                 self.avelocity = '0 0 0';
387         else
388                 self.avelocity = self.pos1;
389 }
390
391 /*QUAKED spawnfunc_func_rotating (0 .5 .8) ? - - X_AXIS Y_AXIS
392 Brush model that spins in place on one axis (default Z).
393 speed   : speed to rotate (in degrees per second)
394 noise   : path/name of looping .wav file to play.
395 dmg     : Do this mutch dmg every .dmgtime intervall when blocked
396 dmgtime : See above.
397 */
398
399 void spawnfunc_func_rotating()
400 {
401         if (self.noise != "")
402         {
403                 precache_sound(self.noise);
404                 ambientsound(self.origin, self.noise, VOL_BASE, ATTN_IDLE);
405         }
406         
407         self.active = ACTIVE_ACTIVE;
408         self.setactive = func_rotating_setactive;
409         
410         if (!self.speed)
411                 self.speed = 100;
412         // FIXME: test if this turns the right way, then remove this comment (negate as needed)
413         if (self.spawnflags & 4) // X (untested)
414                 self.avelocity = '0 0 1' * self.speed;
415         // FIXME: test if this turns the right way, then remove this comment (negate as needed)
416         else if (self.spawnflags & 8) // Y (untested)
417                 self.avelocity = '1 0 0' * self.speed;
418         // FIXME: test if this turns the right way, then remove this comment (negate as needed)
419         else // Z
420                 self.avelocity = '0 1 0' * self.speed;
421         
422         self.pos1 = self.avelocity;
423     
424     if(self.dmg & (!self.message))
425         self.message = " was squished";
426     if(self.dmg && (!self.message2))
427                 self.message2 = "was squished by";
428
429
430     if(self.dmg && (!self.dmgtime))
431         self.dmgtime = 0.25;
432
433     self.dmgtime2 = time;
434
435         if not(InitMovingBrushTrigger())
436                 return;
437         // no EF_LOWPRECISION here, as rounding angles is bad
438
439     self.blocked = generic_plat_blocked;
440
441         // wait for targets to spawn
442         self.nextthink = self.ltime + 999999999;
443         self.think = SUB_Null;
444
445         // TODO make a reset function for this one
446 }
447
448 .float height;
449 void func_bobbing_controller_think()
450 {
451         vector v;
452         self.nextthink = time + 0.1;
453         
454         if not (self.owner.active == ACTIVE_ACTIVE)
455         {
456                 self.owner.velocity = '0 0 0';          
457                 return;
458         }
459                 
460         // calculate sinewave using makevectors
461         makevectors((self.nextthink * self.owner.cnt + self.owner.phase * 360) * '0 1 0');
462         v = self.owner.destvec + self.owner.movedir * v_forward_y;
463         if(self.owner.classname == "func_bobbing") // don't brake stuff if the func_bobbing was killtarget'ed
464                 // * 10 so it will arrive in 0.1 sec
465                 self.owner.velocity = (v - self.owner.origin) * 10;
466 }
467
468 /*QUAKED spawnfunc_func_bobbing (0 .5 .8) ? X_AXIS Y_AXIS
469 Brush model that moves back and forth on one axis (default Z).
470 speed : how long one cycle takes in seconds (default 4)
471 height : how far the cycle moves (default 32)
472 phase : cycle timing adjustment (0-1 as a fraction of the cycle, default 0)
473 noise : path/name of looping .wav file to play.
474 dmg : Do this mutch dmg every .dmgtime intervall when blocked
475 dmgtime : See above.
476 */
477 void spawnfunc_func_bobbing()
478 {
479         entity controller;
480         if (self.noise != "")
481         {
482                 precache_sound(self.noise);
483                 soundto(MSG_INIT, self, CH_TRIGGER_SINGLE, self.noise, VOL_BASE, ATTN_IDLE);
484         }
485         if (!self.speed)
486                 self.speed = 4;
487         if (!self.height)
488                 self.height = 32;
489         // center of bobbing motion
490         self.destvec = self.origin;
491         // time scale to get degrees
492         self.cnt = 360 / self.speed;
493
494         self.active = ACTIVE_ACTIVE;
495
496         // damage when blocked
497         self.blocked = generic_plat_blocked;
498         if(self.dmg & (!self.message))
499                 self.message = " was squished";
500     if(self.dmg && (!self.message2))
501                 self.message2 = "was squished by";
502         if(self.dmg && (!self.dmgtime))
503                 self.dmgtime = 0.25;
504         self.dmgtime2 = time;
505
506         // how far to bob
507         if (self.spawnflags & 1) // X
508                 self.movedir = '1 0 0' * self.height;
509         else if (self.spawnflags & 2) // Y
510                 self.movedir = '0 1 0' * self.height;
511         else // Z
512                 self.movedir = '0 0 1' * self.height;
513
514         if not(InitMovingBrushTrigger())
515                 return;
516
517         // wait for targets to spawn
518         controller = spawn();
519         controller.classname = "func_bobbing_controller";
520         controller.owner = self;
521         controller.nextthink = time + 1;
522         controller.think = func_bobbing_controller_think;
523         self.nextthink = self.ltime + 999999999;
524         self.think = SUB_Null;
525
526         // Savage: Reduce bandwith, critical on e.g. nexdm02
527         self.effects |= EF_LOWPRECISION;
528
529         // TODO make a reset function for this one
530 }
531
532 .float freq;
533 void func_pendulum_controller_think()
534 {
535         float v;
536         self.nextthink = time + 0.1;
537
538         if not (self.owner.active == ACTIVE_ACTIVE)
539         {
540                 self.owner.avelocity_x = 0;
541                 return;
542         }
543
544         // calculate sinewave using makevectors
545         makevectors((self.nextthink * self.owner.freq + self.owner.phase) * '0 360 0');
546         v = self.owner.speed * v_forward_y + self.cnt;
547         if(self.owner.classname == "func_pendulum") // don't brake stuff if the func_bobbing was killtarget'ed
548         {
549                 // * 10 so it will arrive in 0.1 sec
550                 self.owner.avelocity_z = (remainder(v - self.owner.angles_z, 360)) * 10;
551         }
552 }
553
554 void spawnfunc_func_pendulum()
555 {
556         entity controller;
557         if (self.noise != "")
558         {
559                 precache_sound(self.noise);
560                 soundto(MSG_INIT, self, CH_TRIGGER_SINGLE, self.noise, VOL_BASE, ATTN_IDLE);
561         }
562
563         self.active = ACTIVE_ACTIVE;
564
565         // keys: angle, speed, phase, noise, freq
566
567         if(!self.speed)
568                 self.speed = 30;
569         // not initializing self.dmg to 2, to allow damageless pendulum
570
571         if(self.dmg & (!self.message))
572                 self.message = " was squished";
573         if(self.dmg && (!self.message2))
574                 self.message2 = "was squished by";
575         if(self.dmg && (!self.dmgtime))
576                 self.dmgtime = 0.25;
577         self.dmgtime2 = time;
578
579         self.blocked = generic_plat_blocked;
580
581         self.avelocity_z = 0.0000001;
582         if not(InitMovingBrushTrigger())
583                 return;
584
585         if(!self.freq)
586         {
587                 // find pendulum length (same formula as Q3A)
588                 self.freq = 1 / (M_PI * 2) * sqrt(autocvar_sv_gravity / (3 * max(8, fabs(self.mins_z))));
589         }
590
591         // copy initial angle
592         self.cnt = self.angles_z;
593
594         // wait for targets to spawn
595         controller = spawn();
596         controller.classname = "func_pendulum_controller";
597         controller.owner = self;
598         controller.nextthink = time + 1;
599         controller.think = func_pendulum_controller_think;
600         self.nextthink = self.ltime + 999999999;
601         self.think = SUB_Null;
602
603         //self.effects |= EF_LOWPRECISION;
604
605         // TODO make a reset function for this one
606 }
607
608 // button and multiple button
609
610 void() button_wait;
611 void() button_return;
612
613 void button_wait()
614 {
615         self.state = STATE_TOP;
616         self.nextthink = self.ltime + self.wait;
617         self.think = button_return;
618         activator = self.enemy;
619         SUB_UseTargets();
620         self.frame = 1;                 // use alternate textures
621 }
622
623 void button_done()
624 {
625         self.state = STATE_BOTTOM;
626 }
627
628 void button_return()
629 {
630         self.state = STATE_DOWN;
631         SUB_CalcMove (self.pos1, self.speed, button_done);
632         self.frame = 0;                 // use normal textures
633         if (self.health)
634                 self.takedamage = DAMAGE_YES;   // can be shot again
635 }
636
637
638 void button_blocked()
639 {
640         // do nothing, just don't come all the way back out
641 }
642
643
644 void button_fire()
645 {
646         self.health = self.max_health;
647         self.takedamage = DAMAGE_NO;    // will be reset upon return
648
649         if (self.state == STATE_UP || self.state == STATE_TOP)
650                 return;
651
652         if (self.noise != "")
653                 sound (self, CH_TRIGGER, self.noise, VOL_BASE, ATTN_NORM);
654
655         self.state = STATE_UP;
656         SUB_CalcMove (self.pos2, self.speed, button_wait);
657 }
658
659 void button_reset()
660 {
661         self.health = self.max_health;
662         setorigin(self, self.pos1);
663         self.frame = 0;                 // use normal textures
664         self.state = STATE_BOTTOM;
665         if (self.health)
666                 self.takedamage = DAMAGE_YES;   // can be shot again
667 }
668
669 void button_use()
670 {
671 //      if (activator.classname != "player")
672 //      {
673 //              dprint(activator.classname);
674 //              dprint(" triggered a button\n");
675 //      }
676
677         if not (self.active == ACTIVE_ACTIVE)
678                 return;
679
680         self.enemy = activator;
681         button_fire ();
682 }
683
684 void button_touch()
685 {
686 //      if (activator.classname != "player")
687 //      {
688 //              dprint(activator.classname);
689 //              dprint(" touched a button\n");
690 //      }
691         if (!other)
692                 return;
693         if not(other.iscreature)
694                 return;
695         if(other.velocity * self.movedir < 0)
696                 return;
697         self.enemy = other;
698         if (other.owner)
699                 self.enemy = other.owner;
700         button_fire ();
701 }
702
703 void button_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
704 {
705         if(self.spawnflags & DOOR_NOSPLASH)
706                 if(!(DEATH_ISSPECIAL(deathtype)) && (deathtype & HITTYPE_SPLASH))
707                         return;
708         self.health = self.health - damage;
709         if (self.health <= 0)
710         {
711         //      if (activator.classname != "player")
712         //      {
713         //              dprint(activator.classname);
714         //              dprint(" killed a button\n");
715         //      }
716                 self.enemy = damage_attacker;
717                 button_fire ();
718         }
719 }
720
721
722 /*QUAKED spawnfunc_func_button (0 .5 .8) ?
723 When a button is touched, it moves some distance in the direction of it's angle, triggers all of it's targets, waits some time, then returns to it's original position where it can be triggered again.
724
725 "angle"         determines the opening direction
726 "target"        all entities with a matching targetname will be used
727 "speed"         override the default 40 speed
728 "wait"          override the default 1 second wait (-1 = never return)
729 "lip"           override the default 4 pixel lip remaining at end of move
730 "health"        if set, the button must be killed instead of touched. If set to -1, the button will fire on ANY attack, even damageless ones like the MinstaGib laser
731 "sounds"
732 0) steam metal
733 1) wooden clunk
734 2) metallic click
735 3) in-out
736 */
737 void spawnfunc_func_button()
738 {
739         SetMovedir ();
740
741         if not(InitMovingBrushTrigger())
742                 return;
743         self.effects |= EF_LOWPRECISION;
744
745         self.blocked = button_blocked;
746         self.use = button_use;
747
748 //      if (self.health == 0) // all buttons are now shootable
749 //              self.health = 10;
750         if (self.health)
751         {
752                 self.max_health = self.health;
753                 self.event_damage = button_damage;
754                 self.takedamage = DAMAGE_YES;
755         }
756         else
757                 self.touch = button_touch;
758
759         if (!self.speed)
760                 self.speed = 40;
761         if (!self.wait)
762                 self.wait = 1;
763         if (!self.lip)
764                 self.lip = 4;
765
766     if(self.noise != "")
767         precache_sound(self.noise);
768
769         self.active = ACTIVE_ACTIVE;
770
771         self.pos1 = self.origin;
772         self.pos2 = self.pos1 + self.movedir*(fabs(self.movedir*self.size) - self.lip);
773     self.flags |= FL_NOTARGET;
774
775         button_reset();
776 }
777
778
779 float DOOR_START_OPEN = 1;
780 float DOOR_DONT_LINK = 4;
781 float DOOR_TOGGLE = 32;
782
783 /*
784
785 Doors are similar to buttons, but can spawn a fat trigger field around them
786 to open without a touch, and they link together to form simultanious
787 double/quad doors.
788
789 Door.owner is the master door.  If there is only one door, it points to itself.
790 If multiple doors, all will point to a single one.
791
792 Door.enemy chains from the master door through all doors linked in the chain.
793
794 */
795
796 /*
797 =============================================================================
798
799 THINK FUNCTIONS
800
801 =============================================================================
802 */
803
804 void() door_go_down;
805 void() door_go_up;
806 void() door_rotating_go_down;
807 void() door_rotating_go_up;
808
809 void door_blocked()
810 {
811
812     if((self.spawnflags & 8) && (other.takedamage != DAMAGE_NO)) { // KIll Kill Kill!!
813         Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
814     } else {
815
816         if((self.dmg) && (other.takedamage == DAMAGE_YES))    // Shall we bite?
817             Damage (other, self, self, self.dmg, DEATH_HURTTRIGGER, other.origin, '0 0 0');
818
819          //Dont chamge direction for dead or dying stuff
820         if(other.deadflag != DEAD_NO && (other.takedamage == DAMAGE_NO)) {
821             if (self.wait >= 0)
822             {
823                 if (self.state == STATE_DOWN)
824                         if (self.classname == "door")
825                         {
826                                 door_go_up ();
827                         } else
828                         {
829                                 door_rotating_go_up ();
830                         }
831                 else
832                         if (self.classname == "door")
833                         {
834                                 door_go_down ();
835                         } else
836                         {
837                                 door_rotating_go_down ();
838                         }
839             }
840         } else {
841             //gib dying stuff just to make sure
842             if((self.dmg) && (other.takedamage != DAMAGE_NO))    // Shall we bite?
843                 Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
844         }
845     }
846
847         //T_Damage (other, self, self, self.dmg, self.dmg, self.deathtype, DT_IMPACT, (self.absmin + self.absmax) * 0.5, '0 0 0', Obituary_Generic);
848 // if a door has a negative wait, it would never come back if blocked,
849 // so let it just squash the object to death real fast
850 /*      if (self.wait >= 0)
851         {
852                 if (self.state == STATE_DOWN)
853                         door_go_up ();
854                 else
855                         door_go_down ();
856         }
857 */
858 }
859
860
861 void door_hit_top()
862 {
863         if (self.noise1 != "")
864                 sound (self, CH_TRIGGER_SINGLE, self.noise1, VOL_BASE, ATTN_NORM);
865         self.state = STATE_TOP;
866         if (self.spawnflags & DOOR_TOGGLE)
867                 return;         // don't come down automatically
868         if (self.classname == "door")
869         {
870                 self.think = door_go_down;
871         } else
872         {
873                 self.think = door_rotating_go_down;
874         }
875         self.nextthink = self.ltime + self.wait;
876 }
877
878 void door_hit_bottom()
879 {
880         if (self.noise1 != "")
881                 sound (self, CH_TRIGGER_SINGLE, self.noise1, VOL_BASE, ATTN_NORM);
882         self.state = STATE_BOTTOM;
883 }
884
885 void door_go_down()
886 {
887         if (self.noise2 != "")
888                 sound (self, CH_TRIGGER_SINGLE, self.noise2, VOL_BASE, ATTN_NORM);
889         if (self.max_health)
890         {
891                 self.takedamage = DAMAGE_YES;
892                 self.health = self.max_health;
893         }
894
895         self.state = STATE_DOWN;
896         SUB_CalcMove (self.pos1, self.speed, door_hit_bottom);
897 }
898
899 void door_go_up()
900 {
901         if (self.state == STATE_UP)
902                 return;         // already going up
903
904         if (self.state == STATE_TOP)
905         {       // reset top wait time
906                 self.nextthink = self.ltime + self.wait;
907                 return;
908         }
909
910         if (self.noise2 != "")
911                 sound (self, CH_TRIGGER_SINGLE, self.noise2, VOL_BASE, ATTN_NORM);
912         self.state = STATE_UP;
913         SUB_CalcMove (self.pos2, self.speed, door_hit_top);
914
915         string oldmessage;
916         oldmessage = self.message;
917         self.message = "";
918         SUB_UseTargets();
919         self.message = oldmessage;
920 }
921
922
923
924 /*
925 =============================================================================
926
927 ACTIVATION FUNCTIONS
928
929 =============================================================================
930 */
931
932 float door_check_keys(void) {
933         local entity door;
934         
935         
936         if (self.owner)
937                 door = self.owner;
938         else
939                 door = self;
940         
941         // no key needed
942         if not(door.itemkeys)
943                 return TRUE;
944
945         // this door require a key
946         // only a player can have a key
947         if (other.classname != "player")
948                 return FALSE;
949         
950         if (item_keys_usekey(door, other)) {
951                 // some keys were used
952                 if (other.key_door_messagetime <= time) {
953                         play2(other, "misc/talk.wav");
954                         centerprint(other, strcat("You also need ", item_keys_keylist(door.itemkeys), "!"));
955                         other.key_door_messagetime = time + 2;
956                 }
957         } else {
958                 // no keys were used
959                 if (other.key_door_messagetime <= time) {
960                         play2(other, "misc/talk.wav");
961                         centerprint(other, strcat("You need ", item_keys_keylist(door.itemkeys), "!"));
962                         other.key_door_messagetime = time + 2;
963                 }
964         }
965
966         if (door.itemkeys) {
967                 // door is now unlocked
968                 play2(other, "misc/talk.wav");
969                 centerprint(other, "Door unlocked!");
970                 return TRUE;
971         } else
972                 return FALSE;
973 }
974
975
976 void door_fire()
977 {
978         entity  oself;
979         entity  starte;
980
981         if (self.owner != self)
982                 objerror ("door_fire: self.owner != self");
983
984         oself = self;
985
986         if (self.spawnflags & DOOR_TOGGLE)
987         {
988                 if (self.state == STATE_UP || self.state == STATE_TOP)
989                 {
990                         starte = self;
991                         do
992                         {
993                                 if (self.classname == "door")
994                                 {
995                                         door_go_down ();
996                                 }
997                                 else
998                                 {
999                                         door_rotating_go_down ();
1000                                 }
1001                                 self = self.enemy;
1002                         } while ( (self != starte) && (self != world) );
1003                         self = oself;
1004                         return;
1005                 }
1006         }
1007
1008 // trigger all paired doors
1009         starte = self;
1010         do
1011         {
1012                 if (self.classname == "door")
1013                 {
1014                         door_go_up ();
1015                 } else
1016                 {
1017                         // if the BIDIR spawnflag (==2) is set and the trigger has set trigger_reverse, reverse the opening direction
1018                         if ((self.spawnflags & 2) && other.trigger_reverse!=0 && self.lip!=666 && self.state == STATE_BOTTOM)
1019                         {
1020                                 self.lip = 666; // self.lip is used to remember reverse opening direction for door_rotating
1021                                 self.pos2 = '0 0 0' - self.pos2;
1022                         }
1023                         // if BIDIR_IN_DOWN (==8) is set, prevent the door from reoping during closing if it is triggered from the wrong side
1024                         if (!((self.spawnflags & 2) &&  (self.spawnflags & 8) && self.state == STATE_DOWN
1025                             && (((self.lip==666) && (other.trigger_reverse==0)) || ((self.lip!=666) && (other.trigger_reverse!=0)))))
1026                         {
1027                                 door_rotating_go_up ();
1028                         }
1029                 }
1030                 self = self.enemy;
1031         } while ( (self != starte) && (self != world) );
1032         self = oself;
1033 }
1034
1035
1036 void door_use()
1037 {
1038         entity oself;
1039
1040         //dprint("door_use (model: ");dprint(self.model);dprint(")\n");
1041         
1042         if (self.owner)
1043         {
1044                 oself = self;
1045                 self = self.owner;
1046                 door_fire ();
1047                 self = oself;
1048         }
1049 }
1050
1051
1052 void door_trigger_touch()
1053 {
1054         if (other.health < 1)
1055                 if not(other.iscreature && other.deadflag == DEAD_NO)
1056                         return;
1057
1058         if (time < self.attack_finished_single)
1059                 return;
1060         
1061         // check if door is locked
1062         if (!door_check_keys())
1063                 return;
1064         
1065         self.attack_finished_single = time + 1;
1066
1067         activator = other;
1068
1069         self = self.owner;
1070         door_use ();
1071 }
1072
1073
1074 void door_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
1075 {
1076         entity oself;
1077         if(self.spawnflags & DOOR_NOSPLASH)
1078                 if(!(DEATH_ISSPECIAL(deathtype)) && (deathtype & HITTYPE_SPLASH))
1079                         return;
1080         self.health = self.health - damage;
1081         
1082         if (self.itemkeys) {
1083                 // don't allow opening doors through damage if keys are required
1084                 return;
1085         }
1086         
1087         if (self.health <= 0)
1088         {
1089                 oself = self;
1090                 self = self.owner;
1091                 self.health = self.max_health;
1092                 self.takedamage = DAMAGE_NO;    // wil be reset upon return
1093                 door_use ();
1094                 self = oself;
1095         }
1096 }
1097
1098
1099 /*
1100 ================
1101 door_touch
1102
1103 Prints messages
1104 ================
1105 */
1106 void door_touch()
1107 {
1108         if(other.classname != "player")
1109                 return;
1110         if (self.owner.attack_finished_single > time)
1111                 return;
1112
1113         self.owner.attack_finished_single = time + 2;
1114
1115         if (!(self.owner.dmg) && (self.owner.message != ""))
1116         {
1117                 if (other.flags & FL_CLIENT)
1118                         centerprint (other, self.owner.message);
1119                 play2(other, "misc/talk.wav");
1120         }
1121 }
1122
1123
1124 void door_generic_plat_blocked()
1125 {
1126
1127     if((self.spawnflags & 8) && (other.takedamage != DAMAGE_NO)) { // KIll Kill Kill!!
1128         Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
1129     } else {
1130
1131         if((self.dmg) && (other.takedamage == DAMAGE_YES))    // Shall we bite?
1132             Damage (other, self, self, self.dmg, DEATH_HURTTRIGGER, other.origin, '0 0 0');
1133
1134          //Dont chamge direction for dead or dying stuff
1135         if(other.deadflag != DEAD_NO && (other.takedamage == DAMAGE_NO)) {
1136             if (self.wait >= 0)
1137             {
1138                 if (self.state == STATE_DOWN)
1139                     door_rotating_go_up ();
1140                 else
1141                     door_rotating_go_down ();
1142             }
1143         } else {
1144             //gib dying stuff just to make sure
1145             if((self.dmg) && (other.takedamage != DAMAGE_NO))    // Shall we bite?
1146                 Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
1147         }
1148     }
1149
1150         //T_Damage (other, self, self, self.dmg, self.dmg, self.deathtype, DT_IMPACT, (self.absmin + self.absmax) * 0.5, '0 0 0', Obituary_Generic);
1151 // if a door has a negative wait, it would never come back if blocked,
1152 // so let it just squash the object to death real fast
1153 /*      if (self.wait >= 0)
1154         {
1155                 if (self.state == STATE_DOWN)
1156                         door_rotating_go_up ();
1157                 else
1158                         door_rotating_go_down ();
1159         }
1160 */
1161 }
1162
1163
1164 void door_rotating_hit_top()
1165 {
1166         if (self.noise1 != "")
1167                 sound (self, CH_TRIGGER_SINGLE, self.noise1, VOL_BASE, ATTN_NORM);
1168         self.state = STATE_TOP;
1169         if (self.spawnflags & DOOR_TOGGLE)
1170                 return;         // don't come down automatically
1171         self.think = door_rotating_go_down;
1172         self.nextthink = self.ltime + self.wait;
1173 }
1174
1175 void door_rotating_hit_bottom()
1176 {
1177         if (self.noise1 != "")
1178                 sound (self, CH_TRIGGER_SINGLE, self.noise1, VOL_BASE, ATTN_NORM);
1179         if (self.lip==666) // self.lip is used to remember reverse opening direction for door_rotating
1180         {
1181                 self.pos2 = '0 0 0' - self.pos2;
1182                 self.lip = 0;
1183         }
1184         self.state = STATE_BOTTOM;
1185 }
1186
1187 void door_rotating_go_down()
1188 {
1189         if (self.noise2 != "")
1190                 sound (self, CH_TRIGGER_SINGLE, self.noise2, VOL_BASE, ATTN_NORM);
1191         if (self.max_health)
1192         {
1193                 self.takedamage = DAMAGE_YES;
1194                 self.health = self.max_health;
1195         }
1196
1197         self.state = STATE_DOWN;
1198         SUB_CalcAngleMove (self.pos1, self.speed, door_rotating_hit_bottom);
1199 }
1200
1201 void door_rotating_go_up()
1202 {
1203         if (self.state == STATE_UP)
1204                 return;         // already going up
1205
1206         if (self.state == STATE_TOP)
1207         {       // reset top wait time
1208                 self.nextthink = self.ltime + self.wait;
1209                 return;
1210         }
1211         if (self.noise2 != "")
1212                 sound (self, CH_TRIGGER_SINGLE, self.noise2, VOL_BASE, ATTN_NORM);
1213         self.state = STATE_UP;
1214         SUB_CalcAngleMove (self.pos2, self.speed, door_rotating_hit_top);
1215
1216         string oldmessage;
1217         oldmessage = self.message;
1218         self.message = "";
1219         SUB_UseTargets();
1220         self.message = oldmessage;
1221 }
1222
1223
1224
1225
1226 /*
1227 =============================================================================
1228
1229 SPAWNING FUNCTIONS
1230
1231 =============================================================================
1232 */
1233
1234
1235 entity spawn_field(vector fmins, vector fmaxs)
1236 {
1237         entity  trigger;
1238         vector  t1, t2;
1239
1240         trigger = spawn();
1241         trigger.classname = "doortriggerfield";
1242         trigger.movetype = MOVETYPE_NONE;
1243         trigger.solid = SOLID_TRIGGER;
1244         trigger.owner = self;
1245         trigger.touch = door_trigger_touch;
1246
1247         t1 = fmins;
1248         t2 = fmaxs;
1249         setsize (trigger, t1 - '60 60 8', t2 + '60 60 8');
1250         return (trigger);
1251 }
1252
1253
1254 float EntitiesTouching(entity e1, entity e2)
1255 {
1256         if (e1.absmin_x > e2.absmax_x)
1257                 return FALSE;
1258         if (e1.absmin_y > e2.absmax_y)
1259                 return FALSE;
1260         if (e1.absmin_z > e2.absmax_z)
1261                 return FALSE;
1262         if (e1.absmax_x < e2.absmin_x)
1263                 return FALSE;
1264         if (e1.absmax_y < e2.absmin_y)
1265                 return FALSE;
1266         if (e1.absmax_z < e2.absmin_z)
1267                 return FALSE;
1268         return TRUE;
1269 }
1270
1271
1272 /*
1273 =============
1274 LinkDoors
1275
1276
1277 =============
1278 */
1279 void LinkDoors()
1280 {
1281         entity  t, starte;
1282         vector  cmins, cmaxs;
1283
1284         if (self.enemy)
1285                 return;         // already linked by another door
1286         if (self.spawnflags & 4)
1287         {
1288                 self.owner = self.enemy = self;
1289
1290                 if (self.health)
1291                         return;
1292                 IFTARGETED
1293                         return;
1294                 if (self.items)
1295                         return;
1296                 self.trigger_field = spawn_field(self.absmin, self.absmax);
1297
1298                 return;         // don't want to link this door
1299         }
1300
1301         cmins = self.absmin;
1302         cmaxs = self.absmax;
1303
1304         starte = self;
1305         t = self;
1306
1307         do
1308         {
1309                 self.owner = starte;                    // master door
1310
1311                 if (self.health)
1312                         starte.health = self.health;
1313                 IFTARGETED
1314                         starte.targetname = self.targetname;
1315                 if (self.message != "")
1316                         starte.message = self.message;
1317
1318                 t = find(t, classname, self.classname);
1319                 if (!t)
1320                 {
1321                         self.enemy = starte;            // make the chain a loop
1322
1323                 // shootable, or triggered doors just needed the owner/enemy links,
1324                 // they don't spawn a field
1325
1326                         self = self.owner;
1327
1328                         if (self.health)
1329                                 return;
1330                         IFTARGETED
1331                                 return;
1332                         if (self.items)
1333                                 return;
1334
1335                         self.owner.trigger_field = spawn_field(cmins, cmaxs);
1336
1337                         return;
1338                 }
1339
1340                 if (EntitiesTouching(self,t))
1341                 {
1342                         if (t.enemy)
1343                                 objerror ("cross connected doors");
1344
1345                         self.enemy = t;
1346                         self = t;
1347
1348                         if (t.absmin_x < cmins_x)
1349                                 cmins_x = t.absmin_x;
1350                         if (t.absmin_y < cmins_y)
1351                                 cmins_y = t.absmin_y;
1352                         if (t.absmin_z < cmins_z)
1353                                 cmins_z = t.absmin_z;
1354                         if (t.absmax_x > cmaxs_x)
1355                                 cmaxs_x = t.absmax_x;
1356                         if (t.absmax_y > cmaxs_y)
1357                                 cmaxs_y = t.absmax_y;
1358                         if (t.absmax_z > cmaxs_z)
1359                                 cmaxs_z = t.absmax_z;
1360                 }
1361         } while (1 );
1362
1363 }
1364
1365
1366 /*QUAKED spawnfunc_func_door (0 .5 .8) ? START_OPEN x DOOR_DONT_LINK GOLD_KEY SILVER_KEY TOGGLE
1367 if two doors touch, they are assumed to be connected and operate as a unit.
1368
1369 TOGGLE causes the door to wait in both the start and end states for a trigger event.
1370
1371 START_OPEN causes the door to move to its destination when spawned, and operate in reverse.  It is used to temporarily or permanently close off an area when triggered (not useful for touch or takedamage doors).
1372
1373 GOLD_KEY causes the door to open only if the activator holds a gold key.
1374
1375 SILVER_KEY causes the door to open only if the activator holds a silver key.
1376
1377 "message"       is printed when the door is touched if it is a trigger door and it hasn't been fired yet
1378 "angle"         determines the opening direction
1379 "targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door.
1380 "health"        if set, door must be shot open
1381 "speed"         movement speed (100 default)
1382 "wait"          wait before returning (3 default, -1 = never return)
1383 "lip"           lip remaining at end of move (8 default)
1384 "dmg"           damage to inflict when blocked (2 default)
1385 "sounds"
1386 0)      no sound
1387 1)      stone
1388 2)      base
1389 3)      stone chain
1390 4)      screechy metal
1391 FIXME: only one sound set available at the time being
1392
1393 */
1394
1395 void door_init_startopen()
1396 {
1397         setorigin (self, self.pos2);
1398         self.pos2 = self.pos1;
1399         self.pos1 = self.origin;
1400 }
1401
1402 void door_reset()
1403 {
1404         setorigin(self, self.pos1);
1405         self.velocity = '0 0 0';
1406         self.state = STATE_BOTTOM;
1407         self.think = SUB_Null;
1408 }
1409
1410 // spawnflags require key (for now only func_door)
1411 #define SPAWNFLAGS_GOLD_KEY 8
1412 #define SPAWNFLAGS_SILVER_KEY 16
1413 void spawnfunc_func_door()
1414 {
1415         // Quake 1 keys compatibility
1416         if (self.spawnflags & SPAWNFLAGS_GOLD_KEY)
1417                 self.itemkeys |= ITEM_KEY_BIT(0);
1418         if (self.spawnflags & SPAWNFLAGS_SILVER_KEY)
1419                 self.itemkeys |= ITEM_KEY_BIT(1);
1420                 
1421         //if (!self.deathtype) // map makers can override this
1422         //      self.deathtype = " got in the way";
1423         SetMovedir ();
1424
1425         self.max_health = self.health;
1426         if not(InitMovingBrushTrigger())
1427                 return;
1428         self.effects |= EF_LOWPRECISION;
1429         self.classname = "door";
1430
1431         self.blocked = door_blocked;
1432         self.use = door_use;
1433
1434         // FIXME: undocumented flag 8, originally (Q1) GOLD_KEY
1435         // if(self.spawnflags & 8)
1436         //      self.dmg = 10000;
1437
1438     if(self.dmg && (!self.message))
1439                 self.message = "was squished";
1440     if(self.dmg && (!self.message2))
1441                 self.message2 = "was squished by";
1442
1443         if (self.sounds > 0)
1444         {
1445                 precache_sound ("plats/medplat1.wav");
1446                 precache_sound ("plats/medplat2.wav");
1447                 self.noise2 = "plats/medplat1.wav";
1448                 self.noise1 = "plats/medplat2.wav";
1449         }
1450
1451         if (!self.speed)
1452                 self.speed = 100;
1453         if (!self.wait)
1454                 self.wait = 3;
1455         if (!self.lip)
1456                 self.lip = 8;
1457
1458         self.pos1 = self.origin;
1459         self.pos2 = self.pos1 + self.movedir*(fabs(self.movedir*self.size) - self.lip);
1460
1461 // DOOR_START_OPEN is to allow an entity to be lighted in the closed position
1462 // but spawn in the open position
1463         if (self.spawnflags & DOOR_START_OPEN)
1464                 InitializeEntity(self, door_init_startopen, INITPRIO_SETLOCATION);
1465
1466         self.state = STATE_BOTTOM;
1467
1468         if (self.health)
1469         {
1470                 self.takedamage = DAMAGE_YES;
1471                 self.event_damage = door_damage;
1472         }
1473
1474         if (self.items)
1475                 self.wait = -1;
1476
1477         self.touch = door_touch;
1478
1479 // LinkDoors can't be done until all of the doors have been spawned, so
1480 // the sizes can be detected properly.
1481         InitializeEntity(self, LinkDoors, INITPRIO_LINKDOORS);
1482
1483         self.reset = door_reset;
1484 }
1485
1486 /*QUAKED spawnfunc_func_door_rotating (0 .5 .8) ? START_OPEN BIDIR DOOR_DONT_LINK BIDIR_IN_DOWN x TOGGLE X_AXIS Y_AXIS
1487 if two doors touch, they are assumed to be connected and operate as a unit.
1488
1489 TOGGLE causes the door to wait in both the start and end states for a trigger event.
1490
1491 BIDIR makes the door work bidirectional, so that the opening direction is always away from the requestor.
1492 The usage of bidirectional doors requires two manually instantiated triggers (trigger_multiple), the one to open it in the other direction
1493 must have set trigger_reverse to 1.
1494 BIDIR_IN_DOWN will the door prevent from reopening while closing if it is triggered from the other side.
1495
1496 START_OPEN causes the door to move to its destination when spawned, and operate in reverse.  It is used to temporarily or permanently close off an area when triggered (not usefull for touch or takedamage doors).
1497
1498 "message"       is printed when the door is touched if it is a trigger door and it hasn't been fired yet
1499 "angle"         determines the destination angle for opening. negative values reverse the direction.
1500 "targetname"    if set, no touch field will be spawned and a remote button or trigger field activates the door.
1501 "health"        if set, door must be shot open
1502 "speed"         movement speed (100 default)
1503 "wait"          wait before returning (3 default, -1 = never return)
1504 "dmg"           damage to inflict when blocked (2 default)
1505 "sounds"
1506 0)      no sound
1507 1)      stone
1508 2)      base
1509 3)      stone chain
1510 4)      screechy metal
1511 FIXME: only one sound set available at the time being
1512 */
1513
1514 void door_rotating_reset()
1515 {
1516         self.angles = self.pos1;
1517         self.avelocity = '0 0 0';
1518         self.state = STATE_BOTTOM;
1519         self.think = SUB_Null;
1520 }
1521
1522 void door_rotating_init_startopen()
1523 {
1524         self.angles = self.movedir;
1525         self.pos2 = '0 0 0';
1526         self.pos1 = self.movedir;
1527 }
1528
1529
1530 void spawnfunc_func_door_rotating()
1531 {
1532
1533         //if (!self.deathtype) // map makers can override this
1534         //      self.deathtype = " got in the way";
1535
1536         // I abuse "movedir" for denoting the axis for now
1537         if (self.spawnflags & 64) // X (untested)
1538                 self.movedir = '0 0 1';
1539         else if (self.spawnflags & 128) // Y (untested)
1540                 self.movedir = '1 0 0';
1541         else // Z
1542                 self.movedir = '0 1 0';
1543
1544         if (self.angles_y==0) self.angles_y = 90;
1545
1546         self.movedir = self.movedir * self.angles_y;
1547         self.angles = '0 0 0';
1548
1549         self.max_health = self.health;
1550         self.avelocity = self.movedir;
1551         if not(InitMovingBrushTrigger())
1552                 return;
1553         self.velocity = '0 0 0';
1554         //self.effects |= EF_LOWPRECISION;
1555         self.classname = "door_rotating";
1556
1557         self.blocked = door_blocked;
1558         self.use = door_use;
1559
1560     if(self.spawnflags & 8)
1561         self.dmg = 10000;
1562
1563     if(self.dmg && (!self.message))
1564                 self.message = "was squished";
1565     if(self.dmg && (!self.message2))
1566                 self.message2 = "was squished by";
1567
1568     if (self.sounds > 0)
1569         {
1570                 precache_sound ("plats/medplat1.wav");
1571                 precache_sound ("plats/medplat2.wav");
1572                 self.noise2 = "plats/medplat1.wav";
1573                 self.noise1 = "plats/medplat2.wav";
1574         }
1575
1576         if (!self.speed)
1577                 self.speed = 50;
1578         if (!self.wait)
1579                 self.wait = 1;
1580         self.lip = 0; // self.lip is used to remember reverse opening direction for door_rotating
1581
1582         self.pos1 = '0 0 0';
1583         self.pos2 = self.movedir;
1584
1585 // DOOR_START_OPEN is to allow an entity to be lighted in the closed position
1586 // but spawn in the open position
1587         if (self.spawnflags & DOOR_START_OPEN)
1588                 InitializeEntity(self, door_rotating_init_startopen, INITPRIO_SETLOCATION);
1589
1590         self.state = STATE_BOTTOM;
1591
1592         if (self.health)
1593         {
1594                 self.takedamage = DAMAGE_YES;
1595                 self.event_damage = door_damage;
1596         }
1597
1598         if (self.items)
1599                 self.wait = -1;
1600
1601         self.touch = door_touch;
1602
1603 // LinkDoors can't be done until all of the doors have been spawned, so
1604 // the sizes can be detected properly.
1605         InitializeEntity(self, LinkDoors, INITPRIO_LINKDOORS);
1606
1607         self.reset = door_rotating_reset;
1608 }
1609
1610 /*
1611 =============================================================================
1612
1613 SECRET DOORS
1614
1615 =============================================================================
1616 */
1617
1618 void() fd_secret_move1;
1619 void() fd_secret_move2;
1620 void() fd_secret_move3;
1621 void() fd_secret_move4;
1622 void() fd_secret_move5;
1623 void() fd_secret_move6;
1624 void() fd_secret_done;
1625
1626 float SECRET_OPEN_ONCE = 1;             // stays open
1627 float SECRET_1ST_LEFT = 2;              // 1st move is left of arrow
1628 float SECRET_1ST_DOWN = 4;              // 1st move is down from arrow
1629 float SECRET_NO_SHOOT = 8;              // only opened by trigger
1630 float SECRET_YES_SHOOT = 16;    // shootable even if targeted
1631
1632
1633 void fd_secret_use()
1634 {
1635         float temp;
1636         string message_save;
1637
1638         self.health = 10000;
1639         self.bot_attack = TRUE;
1640
1641         // exit if still moving around...
1642         if (self.origin != self.oldorigin)
1643                 return;
1644
1645         message_save = self.message;
1646         self.message = ""; // no more message
1647         SUB_UseTargets();                               // fire all targets / killtargets
1648         self.message = message_save;
1649
1650         self.velocity = '0 0 0';
1651
1652         // Make a sound, wait a little...
1653
1654         if (self.noise1 != "")
1655                 sound(self, CH_TRIGGER_SINGLE, self.noise1, VOL_BASE, ATTN_NORM);
1656         self.nextthink = self.ltime + 0.1;
1657
1658         temp = 1 - (self.spawnflags & SECRET_1ST_LEFT); // 1 or -1
1659         makevectors(self.mangle);
1660
1661         if (!self.t_width)
1662         {
1663                 if (self.spawnflags & SECRET_1ST_DOWN)
1664                         self.t_width = fabs(v_up * self.size);
1665                 else
1666                         self.t_width = fabs(v_right * self.size);
1667         }
1668
1669         if (!self.t_length)
1670                 self.t_length = fabs(v_forward * self.size);
1671
1672         if (self.spawnflags & SECRET_1ST_DOWN)
1673                 self.dest1 = self.origin - v_up * self.t_width;
1674         else
1675                 self.dest1 = self.origin + v_right * (self.t_width * temp);
1676
1677         self.dest2 = self.dest1 + v_forward * self.t_length;
1678         SUB_CalcMove(self.dest1, self.speed, fd_secret_move1);
1679         if (self.noise2 != "")
1680                 sound(self, CH_TRIGGER_SINGLE, self.noise2, VOL_BASE, ATTN_NORM);
1681 }
1682
1683 // Wait after first movement...
1684 void fd_secret_move1()
1685 {
1686         self.nextthink = self.ltime + 1.0;
1687         self.think = fd_secret_move2;
1688         if (self.noise3 != "")
1689                 sound(self, CH_TRIGGER_SINGLE, self.noise3, VOL_BASE, ATTN_NORM);
1690 }
1691
1692 // Start moving sideways w/sound...
1693 void fd_secret_move2()
1694 {
1695         if (self.noise2 != "")
1696                 sound(self, CH_TRIGGER_SINGLE, self.noise2, VOL_BASE, ATTN_NORM);
1697         SUB_CalcMove(self.dest2, self.speed, fd_secret_move3);
1698 }
1699
1700 // Wait here until time to go back...
1701 void fd_secret_move3()
1702 {
1703         if (self.noise3 != "")
1704                 sound(self, CH_TRIGGER_SINGLE, self.noise3, VOL_BASE, ATTN_NORM);
1705         if (!(self.spawnflags & SECRET_OPEN_ONCE))
1706         {
1707                 self.nextthink = self.ltime + self.wait;
1708                 self.think = fd_secret_move4;
1709         }
1710 }
1711
1712 // Move backward...
1713 void fd_secret_move4()
1714 {
1715         if (self.noise2 != "")
1716                 sound(self, CH_TRIGGER_SINGLE, self.noise2, VOL_BASE, ATTN_NORM);
1717         SUB_CalcMove(self.dest1, self.speed, fd_secret_move5);
1718 }
1719
1720 // Wait 1 second...
1721 void fd_secret_move5()
1722 {
1723         self.nextthink = self.ltime + 1.0;
1724         self.think = fd_secret_move6;
1725         if (self.noise3 != "")
1726                 sound(self, CH_TRIGGER_SINGLE, self.noise3, VOL_BASE, ATTN_NORM);
1727 }
1728
1729 void fd_secret_move6()
1730 {
1731         if (self.noise2 != "")
1732                 sound(self, CH_TRIGGER_SINGLE, self.noise2, VOL_BASE, ATTN_NORM);
1733         SUB_CalcMove(self.oldorigin, self.speed, fd_secret_done);
1734 }
1735
1736 void fd_secret_done()
1737 {
1738         if (self.spawnflags&SECRET_YES_SHOOT)
1739         {
1740                 self.health = 10000;
1741                 self.takedamage = DAMAGE_YES;
1742                 //self.th_pain = fd_secret_use;
1743         }
1744         if (self.noise3 != "")
1745                 sound(self, CH_TRIGGER_SINGLE, self.noise3, VOL_BASE, ATTN_NORM);
1746 }
1747
1748 void secret_blocked()
1749 {
1750         if (time < self.attack_finished_single)
1751                 return;
1752         self.attack_finished_single = time + 0.5;
1753         //T_Damage (other, self, self, self.dmg, self.dmg, self.deathtype, DT_IMPACT, (self.absmin + self.absmax) * 0.5, '0 0 0', Obituary_Generic);
1754 }
1755
1756 /*
1757 ==============
1758 secret_touch
1759
1760 Prints messages
1761 ================
1762 */
1763 void secret_touch()
1764 {
1765         if not(other.iscreature)
1766                 return;
1767         if (self.attack_finished_single > time)
1768                 return;
1769
1770         self.attack_finished_single = time + 2;
1771
1772         if (self.message)
1773         {
1774                 if (other.flags & FL_CLIENT)
1775                         centerprint (other, self.message);
1776                 play2(other, "misc/talk.wav");
1777         }
1778 }
1779
1780 void secret_reset()
1781 {
1782         if (self.spawnflags&SECRET_YES_SHOOT)
1783         {
1784                 self.health = 10000;
1785                 self.takedamage = DAMAGE_YES;
1786         }
1787         setorigin(self, self.oldorigin);
1788         self.think = SUB_Null;
1789 }
1790
1791 /*QUAKED spawnfunc_func_door_secret (0 .5 .8) ? open_once 1st_left 1st_down no_shoot always_shoot
1792 Basic secret door. Slides back, then to the side. Angle determines direction.
1793 wait  = # of seconds before coming back
1794 1st_left = 1st move is left of arrow
1795 1st_down = 1st move is down from arrow
1796 always_shoot = even if targeted, keep shootable
1797 t_width = override WIDTH to move back (or height if going down)
1798 t_length = override LENGTH to move sideways
1799 "dmg"           damage to inflict when blocked (2 default)
1800
1801 If a secret door has a targetname, it will only be opened by it's botton or trigger, not by damage.
1802 "sounds"
1803 1) medieval
1804 2) metal
1805 3) base
1806 */
1807
1808 void spawnfunc_func_door_secret()
1809 {
1810         /*if (!self.deathtype) // map makers can override this
1811                 self.deathtype = " got in the way";*/
1812
1813         if (!self.dmg)
1814                 self.dmg = 2;
1815
1816         // Magic formula...
1817         self.mangle = self.angles;
1818         self.angles = '0 0 0';
1819         self.classname = "door";
1820         if not(InitMovingBrushTrigger())
1821                 return;
1822         self.effects |= EF_LOWPRECISION;
1823
1824         self.touch = secret_touch;
1825         self.blocked = secret_blocked;
1826         self.speed = 50;
1827         self.use = fd_secret_use;
1828         IFTARGETED
1829         {
1830         }
1831         else
1832                 self.spawnflags |= SECRET_YES_SHOOT;
1833
1834         if(self.spawnflags&SECRET_YES_SHOOT)
1835         {
1836                 self.health = 10000;
1837                 self.takedamage = DAMAGE_YES;
1838                 self.event_damage = fd_secret_use;
1839         }
1840         self.oldorigin = self.origin;
1841         if (!self.wait)
1842                 self.wait = 5;          // 5 seconds before closing
1843
1844         self.reset = secret_reset;
1845         secret_reset();
1846 }
1847
1848 /*QUAKED spawnfunc_func_fourier (0 .5 .8) ?
1849 Brush model that moves in a pattern of added up sine waves, can be used e.g. for circular motions.
1850 netname: list of <frequencymultiplier> <phase> <x> <y> <z> quadruples, separated by spaces; note that phase 0 represents a sine wave, and phase 0.25 a cosine wave (by default, it uses 1 0 0 0 1, to match func_bobbing's defaults
1851 speed: how long one cycle of frequency multiplier 1 in seconds (default 4)
1852 height: amplitude modifier (default 32)
1853 phase: cycle timing adjustment (0-1 as a fraction of the cycle, default 0)
1854 noise: path/name of looping .wav file to play.
1855 dmg: Do this mutch dmg every .dmgtime intervall when blocked
1856 dmgtime: See above.
1857 */
1858
1859 void func_fourier_controller_think()
1860 {
1861         vector v;
1862         float n, i, t;
1863
1864         self.nextthink = time + 0.1;
1865         if not (self.owner.active == ACTIVE_ACTIVE)
1866         {
1867                 self.owner.velocity = '0 0 0';          
1868                 return;
1869         }
1870
1871
1872         n = floor((tokenize_console(self.owner.netname)) / 5);
1873         t = self.nextthink * self.owner.cnt + self.owner.phase * 360;
1874
1875         v = self.owner.destvec;
1876
1877         for(i = 0; i < n; ++i)
1878         {
1879                 makevectors((t * stof(argv(i*5)) + stof(argv(i*5+1)) * 360) * '0 1 0');
1880                 v = v + ('1 0 0' * stof(argv(i*5+2)) + '0 1 0' * stof(argv(i*5+3)) + '0 0 1' * stof(argv(i*5+4))) * self.owner.height * v_forward_y;
1881         }
1882
1883         if(self.owner.classname == "func_fourier") // don't brake stuff if the func_fourier was killtarget'ed
1884                 // * 10 so it will arrive in 0.1 sec
1885                 self.owner.velocity = (v - self.owner.origin) * 10;
1886 }
1887
1888 void spawnfunc_func_fourier()
1889 {
1890         entity controller;
1891         if (self.noise != "")
1892         {
1893                 precache_sound(self.noise);
1894                 soundto(MSG_INIT, self, CH_TRIGGER_SINGLE, self.noise, VOL_BASE, ATTN_IDLE);
1895         }
1896
1897         if (!self.speed)
1898                 self.speed = 4;
1899         if (!self.height)
1900                 self.height = 32;
1901         self.destvec = self.origin;
1902         self.cnt = 360 / self.speed;
1903
1904         self.blocked = generic_plat_blocked;
1905         if(self.dmg & (!self.message))
1906                 self.message = " was squished";
1907     if(self.dmg && (!self.message2))
1908                 self.message2 = "was squished by";
1909         if(self.dmg && (!self.dmgtime))
1910                 self.dmgtime = 0.25;
1911         self.dmgtime2 = time;
1912
1913         if(self.netname == "")
1914                 self.netname = "1 0 0 0 1";
1915
1916         if not(InitMovingBrushTrigger())
1917                 return;
1918
1919         self.active = ACTIVE_ACTIVE;
1920
1921         // wait for targets to spawn
1922         controller = spawn();
1923         controller.classname = "func_fourier_controller";
1924         controller.owner = self;
1925         controller.nextthink = time + 1;
1926         controller.think = func_fourier_controller_think;
1927         self.nextthink = self.ltime + 999999999;
1928         self.think = SUB_Null;
1929
1930         // Savage: Reduce bandwith, critical on e.g. nexdm02
1931         self.effects |= EF_LOWPRECISION;
1932
1933         // TODO make a reset function for this one
1934 }
1935
1936 // reusing some fields havocbots declared
1937 .entity wp00, wp01, wp02, wp03;
1938
1939 .float targetfactor, target2factor, target3factor, target4factor;
1940 .vector targetnormal, target2normal, target3normal, target4normal;
1941
1942 vector func_vectormamamam_origin(entity o, float t)
1943 {
1944         vector v, p;
1945         float f;
1946         entity e;
1947
1948         f = o.spawnflags;
1949         v = '0 0 0';
1950
1951         e = o.wp00;
1952         if(e)
1953         {
1954                 p = e.origin + t * e.velocity;
1955                 if(f & 1)
1956                         v = v + (p * o.targetnormal) * o.targetnormal * o.targetfactor;
1957                 else
1958                         v = v + (p - (p * o.targetnormal) * o.targetnormal) * o.targetfactor;
1959         }
1960
1961         e = o.wp01;
1962         if(e)
1963         {
1964                 p = e.origin + t * e.velocity;
1965                 if(f & 2)
1966                         v = v + (p * o.target2normal) * o.target2normal * o.target2factor;
1967                 else
1968                         v = v + (p - (p * o.target2normal) * o.target2normal) * o.target2factor;
1969         }
1970
1971         e = o.wp02;
1972         if(e)
1973         {
1974                 p = e.origin + t * e.velocity;
1975                 if(f & 4)
1976                         v = v + (p * o.target3normal) * o.target3normal * o.target3factor;
1977                 else
1978                         v = v + (p - (p * o.target3normal) * o.target3normal) * o.target3factor;
1979         }
1980
1981         e = o.wp03;
1982         if(e)
1983         {
1984                 p = e.origin + t * e.velocity;
1985                 if(f & 8)
1986                         v = v + (p * o.target4normal) * o.target4normal * o.target4factor;
1987                 else
1988                         v = v + (p - (p * o.target4normal) * o.target4normal) * o.target4factor;
1989         }
1990
1991         return v;
1992 }
1993
1994 void func_vectormamamam_controller_think()
1995 {
1996         self.nextthink = time + 0.1;
1997
1998         if not (self.owner.active == ACTIVE_ACTIVE)
1999         {
2000                 self.owner.velocity = '0 0 0';          
2001                 return;
2002         }
2003
2004         if(self.owner.classname == "func_vectormamamam") // don't brake stuff if the func_vectormamamam was killtarget'ed
2005                 self.owner.velocity = (self.owner.destvec + func_vectormamamam_origin(self.owner, 0.1) - self.owner.origin) * 10;
2006 }
2007
2008 void func_vectormamamam_findtarget()
2009 {
2010         if(self.target != "")
2011                 self.wp00 = find(world, targetname, self.target);
2012
2013         if(self.target2 != "")
2014                 self.wp01 = find(world, targetname, self.target2);
2015
2016         if(self.target3 != "")
2017                 self.wp02 = find(world, targetname, self.target3);
2018
2019         if(self.target4 != "")
2020                 self.wp03 = find(world, targetname, self.target4);
2021
2022         if(!self.wp00 && !self.wp01 && !self.wp02 && !self.wp03)
2023                 objerror("No reference entity found, so there is nothing to move. Aborting.");
2024
2025         self.destvec = self.origin - func_vectormamamam_origin(self.owner, 0);
2026
2027         entity controller;
2028         controller = spawn();
2029         controller.classname = "func_vectormamamam_controller";
2030         controller.owner = self;
2031         controller.nextthink = time + 1;
2032         controller.think = func_vectormamamam_controller_think;
2033 }
2034
2035 void spawnfunc_func_vectormamamam()
2036 {
2037         if (self.noise != "")
2038         {
2039                 precache_sound(self.noise);
2040                 soundto(MSG_INIT, self, CH_TRIGGER_SINGLE, self.noise, VOL_BASE, ATTN_IDLE);
2041         }
2042
2043         if(!self.targetfactor)
2044                 self.targetfactor = 1;
2045
2046         if(!self.target2factor)
2047                 self.target2factor = 1;
2048
2049         if(!self.target3factor)
2050                 self.target3factor = 1;
2051
2052         if(!self.target4factor)
2053                 self.target4factor = 1;
2054
2055         if(vlen(self.targetnormal))
2056                 self.targetnormal = normalize(self.targetnormal);
2057
2058         if(vlen(self.target2normal))
2059                 self.target2normal = normalize(self.target2normal);
2060
2061         if(vlen(self.target3normal))
2062                 self.target3normal = normalize(self.target3normal);
2063
2064         if(vlen(self.target4normal))
2065                 self.target4normal = normalize(self.target4normal);
2066
2067         self.blocked = generic_plat_blocked;
2068         if(self.dmg & (!self.message))
2069                 self.message = " was squished";
2070     if(self.dmg && (!self.message2))
2071                 self.message2 = "was squished by";
2072         if(self.dmg && (!self.dmgtime))
2073                 self.dmgtime = 0.25;
2074         self.dmgtime2 = time;
2075
2076         if(self.netname == "")
2077                 self.netname = "1 0 0 0 1";
2078
2079         if not(InitMovingBrushTrigger())
2080                 return;
2081
2082         // wait for targets to spawn
2083         self.nextthink = self.ltime + 999999999;
2084         self.think = SUB_Null;
2085
2086         // Savage: Reduce bandwith, critical on e.g. nexdm02
2087         self.effects |= EF_LOWPRECISION;
2088
2089         self.active = ACTIVE_ACTIVE;
2090
2091         InitializeEntity(self, func_vectormamamam_findtarget, INITPRIO_FINDTARGET);
2092 }
2093
2094 void conveyor_think()
2095 {
2096         entity e;
2097
2098         // set myself as current conveyor where possible
2099         for(e = world; (e = findentity(e, conveyor, self)); )
2100                 e.conveyor = world;
2101
2102         if(self.state)
2103         {
2104                 for(e = findradius((self.absmin + self.absmax) * 0.5, vlen(self.absmax - self.absmin) * 0.5 + 1); e; e = e.chain)
2105                         if(!e.conveyor.state)
2106                                 if(isPushable(e))
2107                                 {
2108                                         vector emin = e.absmin;
2109                                         vector emax = e.absmax;
2110                                         if(self.solid == SOLID_BSP)
2111                                         {
2112                                                 emin -= '1 1 1';
2113                                                 emax += '1 1 1';
2114                                         }
2115                                         if(boxesoverlap(emin, emax, self.absmin, self.absmax)) // quick
2116                                                 if(WarpZoneLib_BoxTouchesBrush(emin, emax, self, e)) // accurate
2117                                                         e.conveyor = self;
2118                                 }
2119
2120                 for(e = world; (e = findentity(e, conveyor, self)); )
2121                 {
2122                         if(e.flags & FL_CLIENT) // doing it via velocity has quite some advantages
2123                                 continue; // done in SV_PlayerPhysics
2124
2125                         setorigin(e, e.origin + self.movedir * sys_frametime);
2126                         move_out_of_solid(e);
2127                         UpdateCSQCProjectile(e);
2128                         /*
2129                         // stupid conveyor code
2130                         tracebox(e.origin, e.mins, e.maxs, e.origin + self.movedir * sys_frametime, MOVE_NORMAL, e);
2131                         if(trace_fraction > 0)
2132                                 setorigin(e, trace_endpos);
2133                         */
2134                 }
2135         }
2136
2137         self.nextthink = time;
2138 }
2139
2140 void conveyor_use()
2141 {
2142         self.state = !self.state;
2143 }
2144
2145 void conveyor_reset()
2146 {
2147         self.state = (self.spawnflags & 1);
2148 }
2149
2150 void conveyor_init()
2151 {
2152         if (!self.speed)
2153                 self.speed = 200;
2154         self.movedir = self.movedir * self.speed;
2155         self.think = conveyor_think;
2156         self.nextthink = time;
2157         IFTARGETED
2158         {
2159                 self.use = conveyor_use;
2160                 self.reset = conveyor_reset;
2161                 conveyor_reset();
2162         }
2163         else
2164                 self.state = 1;
2165 }
2166
2167 void spawnfunc_trigger_conveyor()
2168 {
2169         SetMovedir();
2170         EXACTTRIGGER_INIT;
2171         conveyor_init();
2172 }
2173
2174 void spawnfunc_func_conveyor()
2175 {
2176         SetMovedir();
2177         InitMovingBrushTrigger();
2178         self.movetype = MOVETYPE_NONE;
2179         conveyor_init();
2180 }