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