]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/g_triggers.qc
Use an .olgravity rather than 0. Set the timers to 0.1 again for now.
[xonotic/xonotic-data.pk3dir.git] / qcsrc / server / g_triggers.qc
1 void SUB_DontUseTargets()
2 {
3 }
4
5
6 void() SUB_UseTargets;
7
8 void DelayThink()
9 {
10         activator = self.enemy;
11         SUB_UseTargets ();
12         remove(self);
13 };
14
15 /*
16 ==============================
17 SUB_UseTargets
18
19 the global "activator" should be set to the entity that initiated the firing.
20
21 If self.delay is set, a DelayedUse entity will be created that will actually
22 do the SUB_UseTargets after that many seconds have passed.
23
24 Centerprints any self.message to the activator.
25
26 Removes all entities with a targetname that match self.killtarget,
27 and removes them, so some events can remove other triggers.
28
29 Search for (string)targetname in all entities that
30 match (string)self.target and call their .use function
31
32 ==============================
33 */
34 void SUB_UseTargets()
35 {
36         local entity t, stemp, otemp, act;
37         string s;
38         float i;
39
40 //
41 // check for a delay
42 //
43         if (self.delay)
44         {
45         // create a temp object to fire at a later time
46                 t = spawn();
47                 t.classname = "DelayedUse";
48                 t.nextthink = time + self.delay;
49                 t.think = DelayThink;
50                 t.enemy = activator;
51                 t.message = self.message;
52                 t.killtarget = self.killtarget;
53                 t.target = self.target;
54                 return;
55         }
56
57
58 //
59 // print the message
60 //
61         if (activator.classname == "player" && self.message != "")
62         {
63                 if(clienttype(activator) == CLIENTTYPE_REAL)
64                 {
65                         centerprint (activator, self.message);
66                         if (!self.noise)
67                                 play2(activator, "misc/talk.wav");
68                 }
69         }
70
71 //
72 // kill the killtagets
73 //
74         s = self.killtarget;
75         if (s != "")
76         {
77                 for(t = world; (t = find(t, targetname, s)); )
78                         remove(t);
79         }
80
81 //
82 // fire targets
83 //
84         act = activator;
85         stemp = self;
86         otemp = other;
87
88         for(i = 0; i < 4; ++i)
89         {
90                 switch(i)
91                 {
92                         default:
93                         case 0: s = stemp.target; break;
94                         case 1: s = stemp.target2; break;
95                         case 2: s = stemp.target3; break;
96                         case 3: s = stemp.target4; break;
97                 }
98                 if (s != "")
99                 {
100                         for(t = world; (t = find(t, targetname, s)); )
101                         if(t.use)
102                         {
103                                 //print(stemp.classname, " ", stemp.targetname, " -> ", t.classname, " ", t.targetname, "\n");
104                                 self = t;
105                                 other = stemp;
106                                 activator = act;
107                                 self.use();
108                         }
109                 }
110         }
111
112         activator = act;
113         self = stemp;
114         other = otemp;
115 };
116
117
118 //=============================================================================
119
120 float   SPAWNFLAG_NOMESSAGE = 1;
121 float   SPAWNFLAG_NOTOUCH = 1;
122
123 // the wait time has passed, so set back up for another activation
124 void multi_wait()
125 {
126         if (self.max_health)
127         {
128                 self.health = self.max_health;
129                 self.takedamage = DAMAGE_YES;
130                 self.solid = SOLID_BBOX;
131         }
132 };
133
134
135 // the trigger was just touched/killed/used
136 // self.enemy should be set to the activator so it can be held through a delay
137 // so wait for the delay time before firing
138 void multi_trigger()
139 {
140         if (self.nextthink > time)
141         {
142                 return;         // allready been triggered
143         }
144
145         if (self.classname == "trigger_secret")
146         {
147                 if (self.enemy.classname != "player")
148                         return;
149                 found_secrets = found_secrets + 1;
150                 WriteByte (MSG_ALL, SVC_FOUNDSECRET);
151         }
152
153         if (self.noise)
154                 sound (self.enemy, CHAN_AUTO, self.noise, VOL_BASE, ATTN_NORM);
155
156 // don't trigger again until reset
157         self.takedamage = DAMAGE_NO;
158
159         activator = self.enemy;
160         other = self.goalentity;
161         SUB_UseTargets();
162
163         if (self.wait > 0)
164         {
165                 self.think = multi_wait;
166                 self.nextthink = time + self.wait;
167         }
168         else if (self.wait == 0)
169         {
170                 multi_wait(); // waiting finished
171         }
172         else
173         {       // we can't just remove (self) here, because this is a touch function
174                 // called wheil C code is looping through area links...
175                 self.touch = SUB_Null;
176         }
177 };
178
179 void multi_use()
180 {
181         self.goalentity = other;
182         self.enemy = activator;
183         multi_trigger();
184 };
185
186 void multi_touch()
187 {
188         if not(self.spawnflags & 2)
189         {
190                 if not(other.iscreature)
191                         return;
192
193                 if(self.team)
194                 if(self.team == other.team)
195                         return;
196         }
197
198 // if the trigger has an angles field, check player's facing direction
199         if (self.movedir != '0 0 0')
200         {
201                 makevectors (other.angles);
202                 if (v_forward * self.movedir < 0)
203                         return;         // not facing the right way
204         }
205
206         EXACTTRIGGER_TOUCH;
207
208         self.enemy = other;
209         self.goalentity = other;
210         multi_trigger ();
211 };
212
213 void multi_eventdamage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
214 {
215         if (!self.takedamage)
216                 return;
217         if(self.spawnflags & DOOR_NOSPLASH)
218                 if(!(DEATH_ISSPECIAL(deathtype)) && (deathtype & HITTYPE_SPLASH))
219                         return;
220         self.health = self.health - damage;
221         if (self.health <= 0)
222         {
223                 self.enemy = attacker;
224                 self.goalentity = inflictor;
225                 multi_trigger();
226         }
227 }
228
229 void multi_reset()
230 {
231         if ( !(self.spawnflags & SPAWNFLAG_NOTOUCH) )
232                 self.touch = multi_touch;
233         if (self.max_health)
234         {
235                 self.health = self.max_health;
236                 self.takedamage = DAMAGE_YES;
237                 self.solid = SOLID_BBOX;
238         }
239         self.think = SUB_Null;
240         self.team = self.team_saved;
241 }
242
243 /*QUAKED spawnfunc_trigger_multiple (.5 .5 .5) ? notouch
244 Variable sized repeatable trigger.  Must be targeted at one or more entities.  If "health" is set, the trigger must be killed to activate each time.
245 If "delay" is set, the trigger waits some time after activating before firing.
246 "wait" : Seconds between triggerings. (.2 default)
247 If notouch is set, the trigger is only fired by other entities, not by touching.
248 NOTOUCH has been obsoleted by spawnfunc_trigger_relay!
249 sounds
250 1)      secret
251 2)      beep beep
252 3)      large switch
253 4)
254 set "message" to text string
255 */
256 void spawnfunc_trigger_multiple()
257 {
258         self.reset = multi_reset;
259         if (self.sounds == 1)
260         {
261                 precache_sound ("misc/secret.wav");
262                 self.noise = "misc/secret.wav";
263         }
264         else if (self.sounds == 2)
265         {
266                 precache_sound ("misc/talk.wav");
267                 self.noise = "misc/talk.wav";
268         }
269         else if (self.sounds == 3)
270         {
271                 precache_sound ("misc/trigger1.wav");
272                 self.noise = "misc/trigger1.wav";
273         }
274
275         if (!self.wait)
276                 self.wait = 0.2;
277         else if(self.wait < -1)
278                 self.wait = 0;
279         self.use = multi_use;
280
281         EXACTTRIGGER_INIT;
282
283         self.team_saved = self.team;
284
285         if (self.health)
286         {
287                 if (self.spawnflags & SPAWNFLAG_NOTOUCH)
288                         objerror ("health and notouch don't make sense\n");
289                 self.max_health = self.health;
290                 self.event_damage = multi_eventdamage;
291                 self.takedamage = DAMAGE_YES;
292                 self.solid = SOLID_BBOX;
293                 setorigin (self, self.origin);  // make sure it links into the world
294         }
295         else
296         {
297                 if ( !(self.spawnflags & SPAWNFLAG_NOTOUCH) )
298                 {
299                         self.touch = multi_touch;
300                         setorigin (self, self.origin);  // make sure it links into the world
301                 }
302         }
303 };
304
305
306 /*QUAKED spawnfunc_trigger_once (.5 .5 .5) ? notouch
307 Variable sized trigger. Triggers once, then removes itself.  You must set the key "target" to the name of another object in the level that has a matching
308 "targetname".  If "health" is set, the trigger must be killed to activate.
309 If notouch is set, the trigger is only fired by other entities, not by touching.
310 if "killtarget" is set, any objects that have a matching "target" will be removed when the trigger is fired.
311 if "angle" is set, the trigger will only fire when someone is facing the direction of the angle.  Use "360" for an angle of 0.
312 sounds
313 1)      secret
314 2)      beep beep
315 3)      large switch
316 4)
317 set "message" to text string
318 */
319 void spawnfunc_trigger_once()
320 {
321         self.wait = -1;
322         spawnfunc_trigger_multiple();
323 };
324
325 //=============================================================================
326
327 /*QUAKED spawnfunc_trigger_relay (.5 .5 .5) (-8 -8 -8) (8 8 8)
328 This fixed size trigger cannot be touched, it can only be fired by other events.  It can contain killtargets, targets, delays, and messages.
329 */
330 void spawnfunc_trigger_relay()
331 {
332         self.use = SUB_UseTargets;
333         self.reset = spawnfunc_trigger_relay; // this spawnfunc resets fully
334 };
335
336 void delay_use()
337 {
338     self.think = SUB_UseTargets;
339     self.nextthink = self.wait;
340 }
341
342 void delay_reset()
343 {
344         self.think = SUB_Null;
345 }
346
347 void spawnfunc_trigger_delay()
348 {
349     if(!self.wait)
350         self.wait = 1;
351
352     self.use = delay_use;
353     self.reset = delay_reset;
354 }
355
356 //=============================================================================
357
358
359 void counter_use()
360 {
361         self.count = self.count - 1;
362         if (self.count < 0)
363                 return;
364
365         if (self.count != 0)
366         {
367                 if (activator.classname == "player"
368                 && (self.spawnflags & SPAWNFLAG_NOMESSAGE) == 0)
369                 {
370                         if (self.count >= 4)
371                                 centerprint (activator, "There are more to go...");
372                         else if (self.count == 3)
373                                 centerprint (activator, "Only 3 more to go...");
374                         else if (self.count == 2)
375                                 centerprint (activator, "Only 2 more to go...");
376                         else
377                                 centerprint (activator, "Only 1 more to go...");
378                 }
379                 return;
380         }
381
382         if (activator.classname == "player"
383         && (self.spawnflags & SPAWNFLAG_NOMESSAGE) == 0)
384                 centerprint(activator, "Sequence completed!");
385         self.enemy = activator;
386         multi_trigger ();
387 };
388
389 void counter_reset()
390 {
391         self.count = self.cnt;
392         multi_reset();
393 }
394
395 /*QUAKED spawnfunc_trigger_counter (.5 .5 .5) ? nomessage
396 Acts as an intermediary for an action that takes multiple inputs.
397
398 If nomessage is not set, t will print "1 more.. " etc when triggered and "sequence complete" when finished.
399
400 After the counter has been triggered "count" times (default 2), it will fire all of it's targets and remove itself.
401 */
402 void spawnfunc_trigger_counter()
403 {
404         self.wait = -1;
405         if (!self.count)
406                 self.count = 2;
407         self.cnt = self.count;
408
409         self.use = counter_use;
410         self.reset = counter_reset;
411 };
412
413 .float triggerhurttime;
414 void trigger_hurt_touch()
415 {
416         // only do the EXACTTRIGGER_TOUCH checks when really needed (saves some cpu)
417         if (other.iscreature)
418         {
419                 if (other.takedamage)
420                 if (other.triggerhurttime < time)
421                 {
422                         EXACTTRIGGER_TOUCH;
423                         other.triggerhurttime = time + 1;
424                         Damage (other, self, self, self.dmg, DEATH_HURTTRIGGER, other.origin, '0 0 0');
425                 }
426         }
427         else
428         {
429                 if (!other.owner)
430                 {
431                         if (other.items & IT_KEY1 || other.items & IT_KEY2)     // reset flag
432                         {
433                                 EXACTTRIGGER_TOUCH;
434                                 other.pain_finished = min(other.pain_finished, time + 2);
435                         }
436                         else if (other.classname == "rune")                     // reset runes
437                         {
438                                 EXACTTRIGGER_TOUCH;
439                                 other.nextthink = min(other.nextthink, time + 1);
440                         }
441                 }
442         }
443
444         return;
445 };
446
447 /*QUAKED spawnfunc_trigger_hurt (.5 .5 .5) ?
448 Any object touching this will be hurt
449 set dmg to damage amount
450 defalt dmg = 5
451 */
452 .entity trigger_hurt_next;
453 entity trigger_hurt_last;
454 entity trigger_hurt_first;
455 void spawnfunc_trigger_hurt()
456 {
457         EXACTTRIGGER_INIT;
458         self.touch = trigger_hurt_touch;
459         if (!self.dmg)
460                 self.dmg = 1000;
461         if (!self.message)
462                 self.message = "was in the wrong place";
463         if (!self.message2)
464                 self.message2 = "was thrown into a world of hurt by";
465
466         if(!trigger_hurt_first)
467                 trigger_hurt_first = self;
468         if(trigger_hurt_last)
469                 trigger_hurt_last.trigger_hurt_next = self;
470         trigger_hurt_last = self;
471 };
472
473 float tracebox_hits_trigger_hurt(vector start, vector mi, vector ma, vector end)
474 {
475         entity th;
476
477         for(th = trigger_hurt_first; th; th = th.trigger_hurt_next)
478                 if(tracebox_hits_box(start, mi, ma, end, th.absmin, th.absmax))
479                         return TRUE;
480
481         return FALSE;
482 }
483
484 //////////////////////////////////////////////////////////////
485 //
486 //
487 //
488 //Trigger heal --a04191b92fbd93aa67214ef7e72d6d2e
489 //
490 //////////////////////////////////////////////////////////////
491
492 .float triggerhealtime;
493 void trigger_heal_touch()
494 {
495         // only do the EXACTTRIGGER_TOUCH checks when really needed (saves some cpu)
496         if (other.iscreature)
497         {
498                 if (other.takedamage)
499                 if (other.triggerhealtime < time)
500                 {
501                         EXACTTRIGGER_TOUCH;
502                         other.triggerhealtime = time + 1;
503                         
504                         if (other.health < self.max_health)
505                         {
506                                 other.health = min(other.health + self.health, self.max_health);
507                                 other.pauserothealth_finished = max(other.pauserothealth_finished, time + cvar("g_balance_pause_health_rot"));
508                                 sound (other, CHAN_AUTO, self.noise, VOL_BASE, ATTN_NORM);
509                         }
510                 }
511         }
512 };
513
514 void spawnfunc_trigger_heal()
515 {
516         EXACTTRIGGER_INIT;
517         self.touch = trigger_heal_touch;
518         if (!self.health)
519                 self.health = 10;
520         if (!self.max_health)
521                 self.max_health = 200; //Max health topoff for field
522         if(self.noise == "")
523                 self.noise = "misc/mediumhealth.wav";
524         precache_sound(self.noise);
525 };
526
527
528 //////////////////////////////////////////////////////////////
529 //
530 //
531 //
532 //End trigger_heal
533 //
534 //////////////////////////////////////////////////////////////
535
536 .float triggergravity, triggergravitytime, oldgravity;
537 .entity trigger_gravity_check;
538 void trigger_gravity_check_think()
539 {
540         // Entity that spawns when you enter a gravity zone, and checks if you left it
541         if(self.owner.triggergravitytime < time - 0.1) // need to figure out a correct formula here
542         {
543                 dprint("XXXXXXXXXXXXXXXXXXXXXXXXXX ");
544                 self.owner.gravity = self.owner.oldgravity;
545                 self.owner.triggergravity = 0;
546                 remove(self);
547         }
548         else
549                 self.nextthink = time + 0.01;
550 }
551
552 void trigger_gravity_touch()
553 {
554         if(sv_gravity != 800)
555                 return;
556         // only do the EXACTTRIGGER_TOUCH checks when really needed (saves some cpu)
557         if (other.triggergravitytime < time)
558         {
559                 EXACTTRIGGER_TOUCH;
560                 other.triggergravitytime = time + 0.1;
561
562                 if(!other.triggergravity)
563                 {
564                         other.triggergravity = 1;
565                         other.trigger_gravity_check = spawn();
566                         other.trigger_gravity_check.owner = other;
567                         other.trigger_gravity_check.think = trigger_gravity_check_think;
568                         other.trigger_gravity_check.nextthink = time;
569                 }
570
571                 if (other.gravity != self.gravity)
572                 {
573                         other.oldgravity = other.gravity;
574                         other.gravity = self.gravity;
575                         if(self.noise != "")
576                                 sound (other, CHAN_AUTO, self.noise, VOL_BASE, ATTN_NORM);
577                 }
578         }
579 };
580
581 void spawnfunc_trigger_gravity()
582 {
583         if(!self.gravity)
584                 return;
585         EXACTTRIGGER_INIT;
586         self.touch = trigger_gravity_touch;
587         if(self.noise != "")
588                 precache_sound(self.noise);
589 };
590
591 // TODO add a way to do looped sounds with sound(); then complete this entity
592 .float volume, atten;
593 void target_speaker_use() {sound(self, CHAN_TRIGGER, self.noise, VOL_BASE * self.volume, self.atten);}
594
595 void spawnfunc_target_speaker()
596 {
597         if(self.noise)
598                 precache_sound (self.noise);
599         IFTARGETED
600         {
601                 if(!self.atten)
602                         self.atten = ATTN_NORM;
603                 else if(self.atten < 0)
604                         self.atten = 0;
605                 if(!self.volume)
606                         self.volume = 1;
607                 self.use = target_speaker_use;
608         }
609         else
610         {
611                 if(!self.atten)
612                         self.atten = ATTN_STATIC;
613                 else if(self.atten < 0)
614                         self.atten = 0;
615                 if(!self.volume)
616                         self.volume = 1;
617                 ambientsound (self.origin, self.noise, VOL_BASE * self.volume, self.atten);
618         }
619 };
620
621
622 void spawnfunc_func_stardust() {
623         self.effects = EF_STARDUST;
624 }
625
626 .string bgmscript;
627 .float bgmscriptattack;
628 .float bgmscriptdecay;
629 .float bgmscriptsustain;
630 .float bgmscriptrelease;
631 float pointparticles_SendEntity(entity to, float fl)
632 {
633         WriteByte(MSG_ENTITY, ENT_CLIENT_POINTPARTICLES);
634
635         // optional features to save space
636         fl = fl & 0x0F;
637         if(self.spawnflags & 2)
638                 fl |= 0x10; // absolute count on toggle-on
639         if(self.movedir != '0 0 0' || self.velocity != '0 0 0')
640                 fl |= 0x20; // 4 bytes - saves CPU
641         if(self.waterlevel || self.count != 1)
642                 fl |= 0x40; // 4 bytes - obscure features almost never used
643         if(self.mins != '0 0 0' || self.maxs != '0 0 0')
644                 fl |= 0x80; // 14 bytes - saves lots of space
645
646         WriteByte(MSG_ENTITY, fl);
647         if(fl & 2)
648         {
649                 if(self.state)
650                         WriteCoord(MSG_ENTITY, self.impulse);
651                 else
652                         WriteCoord(MSG_ENTITY, 0); // off
653         }
654         if(fl & 4)
655         {
656                 WriteCoord(MSG_ENTITY, self.origin_x);
657                 WriteCoord(MSG_ENTITY, self.origin_y);
658                 WriteCoord(MSG_ENTITY, self.origin_z);
659         }
660         if(fl & 1)
661         {
662                 if(self.model != "null")
663                 {
664                         WriteShort(MSG_ENTITY, self.modelindex);
665                         if(fl & 0x80)
666                         {
667                                 WriteCoord(MSG_ENTITY, self.mins_x);
668                                 WriteCoord(MSG_ENTITY, self.mins_y);
669                                 WriteCoord(MSG_ENTITY, self.mins_z);
670                                 WriteCoord(MSG_ENTITY, self.maxs_x);
671                                 WriteCoord(MSG_ENTITY, self.maxs_y);
672                                 WriteCoord(MSG_ENTITY, self.maxs_z);
673                         }
674                 }
675                 else
676                 {
677                         WriteShort(MSG_ENTITY, 0);
678                         if(fl & 0x80)
679                         {
680                                 WriteCoord(MSG_ENTITY, self.maxs_x);
681                                 WriteCoord(MSG_ENTITY, self.maxs_y);
682                                 WriteCoord(MSG_ENTITY, self.maxs_z);
683                         }
684                 }
685                 WriteShort(MSG_ENTITY, self.cnt);
686                 if(fl & 0x20)
687                 {
688                         WriteShort(MSG_ENTITY, compressShortVector(self.velocity));
689                         WriteShort(MSG_ENTITY, compressShortVector(self.movedir));
690                 }
691                 if(fl & 0x40)
692                 {
693                         WriteShort(MSG_ENTITY, self.waterlevel * 16.0);
694                         WriteByte(MSG_ENTITY, self.count * 16.0);
695                 }
696                 WriteString(MSG_ENTITY, self.noise);
697                 if(self.noise != "")
698                 {
699                         WriteByte(MSG_ENTITY, floor(self.atten * 64));
700                         WriteByte(MSG_ENTITY, floor(self.volume * 255));
701                 }
702                 WriteString(MSG_ENTITY, self.bgmscript);
703                 if(self.bgmscript != "")
704                 {
705                         WriteByte(MSG_ENTITY, floor(self.bgmscriptattack * 64));
706                         WriteByte(MSG_ENTITY, floor(self.bgmscriptdecay * 64));
707                         WriteByte(MSG_ENTITY, floor(self.bgmscriptsustain * 255));
708                         WriteByte(MSG_ENTITY, floor(self.bgmscriptrelease * 64));
709                 }
710         }
711         return 1;
712 }
713
714 void pointparticles_use()
715 {
716         self.state = !self.state;
717         self.SendFlags |= 2;
718 }
719
720 void pointparticles_think()
721 {
722         if(self.origin != self.oldorigin)
723         {
724                 self.SendFlags |= 4;
725                 self.oldorigin = self.origin;
726         }
727         self.nextthink = time;
728 }
729
730 void pointparticles_reset()
731 {
732         if(self.spawnflags & 1)
733                 self.state = 1;
734         else
735                 self.state = 0;
736 }
737
738 void spawnfunc_func_pointparticles()
739 {
740         if(self.model != "")
741                 setmodel(self, self.model);
742         if(self.noise != "")
743                 precache_sound (self.noise);
744         
745         if(!self.bgmscriptsustain)
746                 self.bgmscriptsustain = 1;
747         else if(self.bgmscriptsustain < 0)
748                 self.bgmscriptsustain = 0;
749
750         if(!self.atten)
751                 self.atten = ATTN_NORM;
752         else if(self.atten < 0)
753                 self.atten = 0;
754         if(!self.volume)
755                 self.volume = 1;
756         if(!self.count)
757                 self.count = 1;
758         if(!self.impulse)
759                 self.impulse = 1;
760
761         if(!self.modelindex)
762         {
763                 setorigin(self, self.origin + self.mins);
764                 setsize(self, '0 0 0', self.maxs - self.mins);
765         }
766         if(!self.cnt)
767                 self.cnt = particleeffectnum(self.mdl);
768
769         Net_LinkEntity(self, FALSE, 0, pointparticles_SendEntity);
770
771         IFTARGETED
772         {
773                 self.use = pointparticles_use;
774                 self.reset = pointparticles_reset;
775                 self.reset();
776         }
777         else
778                 self.state = 1;
779         self.think = pointparticles_think;
780         self.nextthink = time;
781 }
782
783 void spawnfunc_func_sparks()
784 {
785         // self.cnt is the amount of sparks that one burst will spawn
786         if(self.cnt < 1) {
787                 self.cnt = 25.0; // nice default value
788         }
789
790         // self.wait is the probability that a sparkthink will spawn a spark shower
791         // range: 0 - 1, but 0 makes little sense, so...
792         if(self.wait < 0.05) {
793                 self.wait = 0.25; // nice default value
794         }
795
796         self.count = self.cnt;
797         self.mins = '0 0 0';
798         self.maxs = '0 0 0';
799         self.velocity = '0 0 -1';
800         self.mdl = "TE_SPARK";
801         self.impulse = 10 * self.wait; // by default 2.5/sec
802         self.wait = 0;
803         self.cnt = 0; // use mdl
804
805         spawnfunc_func_pointparticles();
806 }
807
808 float rainsnow_SendEntity(entity to, float sf)
809 {
810         WriteByte(MSG_ENTITY, ENT_CLIENT_RAINSNOW);
811         WriteByte(MSG_ENTITY, self.state);
812         WriteCoord(MSG_ENTITY, self.origin_x + self.mins_x);
813         WriteCoord(MSG_ENTITY, self.origin_y + self.mins_y);
814         WriteCoord(MSG_ENTITY, self.origin_z + self.mins_z);
815         WriteCoord(MSG_ENTITY, self.maxs_x - self.mins_x);
816         WriteCoord(MSG_ENTITY, self.maxs_y - self.mins_y);
817         WriteCoord(MSG_ENTITY, self.maxs_z - self.mins_z);
818         WriteShort(MSG_ENTITY, compressShortVector(self.dest));
819         WriteShort(MSG_ENTITY, self.count);
820         WriteByte(MSG_ENTITY, self.cnt);
821         return 1;
822 };
823
824 /*QUAKED spawnfunc_func_rain (0 .5 .8) ?
825 This is an invisible area like a trigger, which rain falls inside of.
826
827 Keys:
828 "velocity"
829  falling direction (should be something like '0 0 -700', use the X and Y velocity for wind)
830 "cnt"
831  sets color of rain (default 12 - white)
832 "count"
833  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
834 */
835 void spawnfunc_func_rain()
836 {
837         self.dest = self.velocity;
838         self.velocity = '0 0 0';
839         if (!self.dest)
840                 self.dest = '0 0 -700';
841         self.angles = '0 0 0';
842         self.movetype = MOVETYPE_NONE;
843         self.solid = SOLID_NOT;
844         SetBrushEntityModel();
845         if (!self.cnt)
846                 self.cnt = 12;
847         if (!self.count)
848                 self.count = 2000;
849         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
850         if (self.count < 1)
851                 self.count = 1;
852         if(self.count > 65535)
853                 self.count = 65535;
854
855         self.state = 1; // 1 is rain, 0 is snow
856         self.Version = 1;
857
858         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
859 };
860
861
862 /*QUAKED spawnfunc_func_snow (0 .5 .8) ?
863 This is an invisible area like a trigger, which snow falls inside of.
864
865 Keys:
866 "velocity"
867  falling direction (should be something like '0 0 -300', use the X and Y velocity for wind)
868 "cnt"
869  sets color of rain (default 12 - white)
870 "count"
871  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
872 */
873 void spawnfunc_func_snow()
874 {
875         self.dest = self.velocity;
876         self.velocity = '0 0 0';
877         if (!self.dest)
878                 self.dest = '0 0 -300';
879         self.angles = '0 0 0';
880         self.movetype = MOVETYPE_NONE;
881         self.solid = SOLID_NOT;
882         SetBrushEntityModel();
883         if (!self.cnt)
884                 self.cnt = 12;
885         if (!self.count)
886                 self.count = 2000;
887         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
888         if (self.count < 1)
889                 self.count = 1;
890         if(self.count > 65535)
891                 self.count = 65535;
892
893         self.state = 0; // 1 is rain, 0 is snow
894         self.Version = 1;
895
896         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
897 };
898
899
900 void FireRailgunBullet (vector start, vector end, float bdamage, float bforce, float mindist, float maxdist, float halflifedist, float forcehalflifedist, float deathtype);
901
902 .float modelscale;
903 void misc_laser_aim()
904 {
905         vector a;
906         if(self.enemy)
907         {
908                 if(self.spawnflags & 2)
909                 {
910                         if(self.enemy.origin != self.mangle)
911                         {
912                                 self.mangle = self.enemy.origin;
913                                 self.SendFlags |= 2;
914                         }
915                 }
916                 else
917                 {
918                         a = vectoangles(self.enemy.origin - self.origin);
919                         a_x = -a_x;
920                         if(a != self.mangle)
921                         {
922                                 self.mangle = a;
923                                 self.SendFlags |= 2;
924                         }
925                 }
926         }
927         else
928         {
929                 if(self.angles != self.mangle)
930                 {
931                         self.mangle = self.angles;
932                         self.SendFlags |= 2;
933                 }
934         }
935         if(self.origin != self.oldorigin)
936         {
937                 self.SendFlags |= 1;
938                 self.oldorigin = self.origin;
939         }
940 }
941
942 void misc_laser_init()
943 {
944         if(self.target != "")
945                 self.enemy = find(world, targetname, self.target);
946 }
947
948 .entity pusher;
949 void misc_laser_think()
950 {
951         vector o;
952         entity oldself;
953
954         self.nextthink = time;
955
956         if(!self.state)
957                 return;
958
959         misc_laser_aim();
960
961         if(self.enemy)
962         {
963                 o = self.enemy.origin;
964                 if not(self.spawnflags & 2)
965                         o = self.origin + normalize(o - self.origin) * 32768;
966         }
967         else
968         {
969                 makevectors(self.mangle);
970                 o = self.origin + v_forward * 32768;
971         }
972
973         if(self.dmg)
974         {
975                 if(self.dmg < 0)
976                         FireRailgunBullet(self.origin, o, 100000, 0, 0, 0, 0, 0, DEATH_HURTTRIGGER);
977                 else
978                         FireRailgunBullet(self.origin, o, self.dmg * frametime, 0, 0, 0, 0, 0, DEATH_HURTTRIGGER);
979         }
980
981         if(self.enemy.target != "") // DETECTOR laser
982         {
983                 traceline(self.origin, o, MOVE_NORMAL, self);
984                 if(trace_ent.iscreature)
985                 {
986                         self.pusher = trace_ent;
987                         if(!self.count)
988                         {
989                                 self.count = 1;
990
991                                 oldself = self;
992                                 self = self.enemy;
993                                 activator = self.pusher;
994                                 SUB_UseTargets();
995                                 self = oldself;
996                         }
997                 }
998                 else
999                 {
1000                         if(self.count)
1001                         {
1002                                 self.count = 0;
1003
1004                                 oldself = self;
1005                                 self = self.enemy;
1006                                 activator = self.pusher;
1007                                 SUB_UseTargets();
1008                                 self = oldself;
1009                         }
1010                 }
1011         }
1012 }
1013
1014 float laser_SendEntity(entity to, float fl)
1015 {
1016         WriteByte(MSG_ENTITY, ENT_CLIENT_LASER);
1017         fl = fl - (fl & 0xE0); // use that bit to indicate finite length laser
1018         if(self.spawnflags & 2)
1019                 fl |= 0x80;
1020         if(self.alpha)
1021                 fl |= 0x40;
1022         if(self.scale != 1 || self.modelscale != 1)
1023                 fl |= 0x20;
1024         WriteByte(MSG_ENTITY, fl);
1025         if(fl & 1)
1026         {
1027                 WriteCoord(MSG_ENTITY, self.origin_x);
1028                 WriteCoord(MSG_ENTITY, self.origin_y);
1029                 WriteCoord(MSG_ENTITY, self.origin_z);
1030         }
1031         if(fl & 8)
1032         {
1033                 WriteByte(MSG_ENTITY, self.colormod_x * 255.0);
1034                 WriteByte(MSG_ENTITY, self.colormod_y * 255.0);
1035                 WriteByte(MSG_ENTITY, self.colormod_z * 255.0);
1036                 if(fl & 0x40)
1037                         WriteByte(MSG_ENTITY, self.alpha * 255.0);
1038                 if(fl & 0x20)
1039                 {
1040                         WriteByte(MSG_ENTITY, bound(0, self.scale * 16.0, 255));
1041                         WriteByte(MSG_ENTITY, bound(0, self.modelscale * 16.0, 255));
1042                 }
1043                 WriteShort(MSG_ENTITY, self.cnt + 1);
1044         }
1045         if(fl & 2)
1046         {
1047                 if(fl & 0x80)
1048                 {
1049                         WriteCoord(MSG_ENTITY, self.enemy.origin_x);
1050                         WriteCoord(MSG_ENTITY, self.enemy.origin_y);
1051                         WriteCoord(MSG_ENTITY, self.enemy.origin_z);
1052                 }
1053                 else
1054                 {
1055                         WriteAngle(MSG_ENTITY, self.mangle_x);
1056                         WriteAngle(MSG_ENTITY, self.mangle_y);
1057                 }
1058         }
1059         if(fl & 4)
1060                 WriteByte(MSG_ENTITY, self.state);
1061         return 1;
1062 }
1063
1064 /*QUAKED spawnfunc_misc_laser (.5 .5 .5) ? START_ON DEST_IS_FIXED
1065 Any object touching the beam will be hurt
1066 Keys:
1067 "target"
1068  spawnfunc_target_position where the laser ends
1069 "mdl"
1070  name of beam end effect to use
1071 "colormod"
1072  color of the beam (default: red)
1073 "dmg"
1074  damage per second (-1 for a laser that kills immediately)
1075 */
1076 void laser_use()
1077 {
1078         self.state = !self.state;
1079         self.SendFlags |= 4;
1080         misc_laser_aim();
1081 }
1082
1083 void laser_reset()
1084 {
1085         if(self.spawnflags & 1)
1086                 self.state = 1;
1087         else
1088                 self.state = 0;
1089 }
1090
1091 void spawnfunc_misc_laser()
1092 {
1093         if(self.mdl)
1094         {
1095                 if(self.mdl == "none")
1096                         self.cnt = -1;
1097                 else
1098                 {
1099                         self.cnt = particleeffectnum(self.mdl);
1100                         if(self.cnt < 0)
1101                                 if(self.dmg)
1102                                         self.cnt = particleeffectnum("laser_deadly");
1103                 }
1104         }
1105         else if(!self.cnt)
1106         {
1107                 if(self.dmg)
1108                         self.cnt = particleeffectnum("laser_deadly");
1109                 else
1110                         self.cnt = -1;
1111         }
1112         if(self.cnt < 0)
1113                 self.cnt = -1;
1114
1115         if(self.colormod == '0 0 0')
1116                 if(!self.alpha)
1117                         self.colormod = '1 0 0';
1118         if(!self.message)
1119                 self.message = "saw the light";
1120         if (!self.message2)
1121                 self.message2 = "was pushed into a laser by";
1122         if(!self.scale)
1123                 self.scale = 1;
1124         if(!self.modelscale)
1125                 self.modelscale = 1;
1126         self.think = misc_laser_think;
1127         self.nextthink = time;
1128         InitializeEntity(self, misc_laser_init, INITPRIO_FINDTARGET);
1129
1130         self.mangle = self.angles;
1131
1132         Net_LinkEntity(self, FALSE, 0, laser_SendEntity);
1133
1134         IFTARGETED
1135         {
1136                 self.reset = laser_reset;
1137                 laser_reset();
1138                 self.use = laser_use;
1139         }
1140         else
1141                 self.state = 1;
1142 }
1143
1144 // tZorks trigger impulse / gravity
1145 .float radius;
1146 .float falloff;
1147 .float strength;
1148 .float lastpushtime;
1149
1150 // targeted (directional) mode
1151 void trigger_impulse_touch1()
1152 {
1153         entity targ;
1154     float pushdeltatime;
1155     float str;
1156
1157         // FIXME: Better checking for what to push and not.
1158         if not(other.iscreature)
1159         if (other.classname != "corpse")
1160         if (other.classname != "body")
1161         if (other.classname != "gib")
1162         if (other.classname != "missile")
1163         if (other.classname != "rocket")
1164         if (other.classname != "casing")
1165         if (other.classname != "grenade")
1166         if (other.classname != "plasma")
1167         if (other.classname != "plasma_prim")
1168         if (other.classname != "plasma_chain")
1169         if (other.classname != "droppedweapon")
1170         if (other.classname != "nexball_basketball")
1171         if (other.classname != "nexball_football")
1172                 return;
1173
1174         if (other.deadflag && other.iscreature)
1175                 return;
1176
1177         EXACTTRIGGER_TOUCH;
1178
1179     targ = find(world, targetname, self.target);
1180     if(!targ)
1181     {
1182         objerror("trigger_force without a (valid) .target!\n");
1183         remove(self);
1184         return;
1185     }
1186
1187     if(self.falloff == 1)
1188         str = (str / self.radius) * self.strength;
1189     else if(self.falloff == 2)
1190         str = (1 - (str / self.radius)) * self.strength;
1191     else
1192         str = self.strength;
1193
1194     pushdeltatime = time - other.lastpushtime;
1195     if (pushdeltatime > 0.15) pushdeltatime = 0;
1196     other.lastpushtime = time;
1197     if(!pushdeltatime) return;
1198
1199     other.velocity = other.velocity + normalize(targ.origin - self.origin) * str * pushdeltatime;
1200         other.flags &~= FL_ONGROUND;
1201 }
1202
1203 // Directionless (accelerator/decelerator) mode
1204 void trigger_impulse_touch2()
1205 {
1206     float pushdeltatime;
1207
1208         // FIXME: Better checking for what to push and not.
1209         if not(other.iscreature)
1210         if (other.classname != "corpse")
1211         if (other.classname != "body")
1212         if (other.classname != "gib")
1213         if (other.classname != "missile")
1214         if (other.classname != "rocket")
1215         if (other.classname != "casing")
1216         if (other.classname != "grenade")
1217         if (other.classname != "plasma")
1218         if (other.classname != "plasma_prim")
1219         if (other.classname != "plasma_chain")
1220         if (other.classname != "droppedweapon")
1221         if (other.classname != "nexball_basketball")
1222         if (other.classname != "nexball_football")
1223                 return;
1224
1225         if (other.deadflag && other.iscreature)
1226                 return;
1227
1228         EXACTTRIGGER_TOUCH;
1229
1230     pushdeltatime = time - other.lastpushtime;
1231     if (pushdeltatime > 0.15) pushdeltatime = 0;
1232     other.lastpushtime = time;
1233     if(!pushdeltatime) return;
1234
1235     // div0: ticrate independent, 1 = identity (not 20)
1236     other.velocity = other.velocity * pow(self.strength, pushdeltatime);
1237 }
1238
1239 // Spherical (gravity/repulsor) mode
1240 void trigger_impulse_touch3()
1241 {
1242     float pushdeltatime;
1243     float str;
1244
1245         // FIXME: Better checking for what to push and not.
1246         if not(other.iscreature)
1247         if (other.classname != "corpse")
1248         if (other.classname != "body")
1249         if (other.classname != "gib")
1250         if (other.classname != "missile")
1251         if (other.classname != "rocket")
1252         if (other.classname != "casing")
1253         if (other.classname != "grenade")
1254         if (other.classname != "plasma")
1255         if (other.classname != "plasma_prim")
1256         if (other.classname != "plasma_chain")
1257         if (other.classname != "droppedweapon")
1258         if (other.classname != "nexball_basketball")
1259         if (other.classname != "nexball_football")
1260                 return;
1261
1262         if (other.deadflag && other.iscreature)
1263                 return;
1264
1265         EXACTTRIGGER_TOUCH;
1266
1267     pushdeltatime = time - other.lastpushtime;
1268     if (pushdeltatime > 0.15) pushdeltatime = 0;
1269     other.lastpushtime = time;
1270     if(!pushdeltatime) return;
1271
1272     setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1273
1274         str = min(self.radius, vlen(self.origin - other.origin));
1275
1276     if(self.falloff == 1)
1277         str = (1 - str / self.radius) * self.strength; // 1 in the inside
1278     else if(self.falloff == 2)
1279         str = (str / self.radius) * self.strength; // 0 in the inside
1280     else
1281         str = self.strength;
1282
1283     other.velocity = other.velocity + normalize(other.origin - self.origin) * str * pushdeltatime;
1284 }
1285
1286 /*QUAKED spawnfunc_trigger_impulse (.5 .5 .5) ?
1287 -------- KEYS --------
1288 target : If this is set, this points to the spawnfunc_target_position to which the player will get pushed.
1289          If not, this trigger acts like a damper/accelerator field.
1290
1291 strength : This is how mutch force to add in the direction of .target each second
1292            when .target is set. If not, this is hoe mutch to slow down/accelerate
1293            someting cought inside this trigger. (1=no change, 0,5 half speed rougthly each tic, 2 = doubble)
1294
1295 radius   : If set, act as a spherical device rather then a liniar one.
1296
1297 falloff : 0 = none, 1 = liniar, 2 = inverted liniar
1298
1299 -------- NOTES --------
1300 Use a brush textured with common/origin in the trigger entity to determine the origin of the force
1301 in directional and sperical mode. For damper/accelerator mode this is not nessesary (and has no effect).
1302 */
1303
1304 void spawnfunc_trigger_impulse()
1305 {
1306         EXACTTRIGGER_INIT;
1307     if(self.radius)
1308     {
1309         if(!self.strength) self.strength = 2000 * cvar("g_triggerimpulse_radial_multiplier");
1310         setorigin(self, self.origin);
1311         setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1312         self.touch = trigger_impulse_touch3;
1313     }
1314     else
1315     {
1316         if(self.target)
1317         {
1318             if(!self.strength) self.strength = 950 * cvar("g_triggerimpulse_directional_multiplier");
1319             self.touch = trigger_impulse_touch1;
1320         }
1321         else
1322         {
1323             if(!self.strength) self.strength = 0.9;
1324                         self.strength = pow(self.strength, cvar("g_triggerimpulse_accel_power")) * cvar("g_triggerimpulse_accel_multiplier");
1325             self.touch = trigger_impulse_touch2;
1326         }
1327     }
1328 }
1329
1330 /*QUAKED spawnfunc_trigger_flipflop (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ENABLED
1331 "Flip-flop" trigger gate... lets only every second trigger event through
1332 */
1333 void flipflop_use()
1334 {
1335         self.state = !self.state;
1336         if(self.state)
1337                 SUB_UseTargets();
1338 }
1339
1340 void spawnfunc_trigger_flipflop()
1341 {
1342         if(self.spawnflags & 1)
1343                 self.state = 1;
1344         self.use = flipflop_use;
1345         self.reset = spawnfunc_trigger_flipflop; // perfect resetter
1346 }
1347
1348 /*QUAKED spawnfunc_trigger_monoflop (.5 .5 .5) (-8 -8 -8) (8 8 8)
1349 "Mono-flop" trigger gate... turns one trigger event into one "on" and one "off" event, separated by a delay of "wait"
1350 */
1351 void monoflop_use()
1352 {
1353         self.nextthink = time + self.wait;
1354         self.enemy = activator;
1355         if(self.state)
1356                 return;
1357         self.state = 1;
1358         SUB_UseTargets();
1359 }
1360 void monoflop_fixed_use()
1361 {
1362         if(self.state)
1363                 return;
1364         self.nextthink = time + self.wait;
1365         self.state = 1;
1366         self.enemy = activator;
1367         SUB_UseTargets();
1368 }
1369
1370 void monoflop_think()
1371 {
1372         self.state = 0;
1373         activator = self.enemy;
1374         SUB_UseTargets();
1375 }
1376
1377 void monoflop_reset()
1378 {
1379         self.state = 0;
1380         self.nextthink = 0;
1381 }
1382
1383 void spawnfunc_trigger_monoflop()
1384 {
1385         if(!self.wait)
1386                 self.wait = 1;
1387         if(self.spawnflags & 1)
1388                 self.use = monoflop_fixed_use;
1389         else
1390                 self.use = monoflop_use;
1391         self.think = monoflop_think;
1392         self.state = 0;
1393         self.reset = monoflop_reset;
1394 }
1395
1396 void multivibrator_send()
1397 {
1398         float newstate;
1399         float cyclestart;
1400
1401         cyclestart = floor((time + self.phase) / (self.wait + self.respawntime)) * (self.wait + self.respawntime) - self.phase;
1402
1403         newstate = (time < cyclestart + self.wait);
1404
1405         activator = self;
1406         if(self.state != newstate)
1407                 SUB_UseTargets();
1408         self.state = newstate;
1409
1410         if(self.state)
1411                 self.nextthink = cyclestart + self.wait + 0.01;
1412         else
1413                 self.nextthink = cyclestart + self.wait + self.respawntime + 0.01;
1414 }
1415
1416 void multivibrator_toggle()
1417 {
1418         if(self.nextthink == 0)
1419         {
1420                 multivibrator_send();
1421         }
1422         else
1423         {
1424                 if(self.state)
1425                 {
1426                         SUB_UseTargets();
1427                         self.state = 0;
1428                 }
1429                 self.nextthink = 0;
1430         }
1431 }
1432
1433 void multivibrator_reset()
1434 {
1435         if(!(self.spawnflags & 1))
1436                 self.nextthink = 0; // wait for a trigger event
1437         else
1438                 self.nextthink = max(1, time);
1439 }
1440
1441 /*QUAKED trigger_multivibrator (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ON
1442 "Multivibrator" trigger gate... repeatedly sends trigger events. When triggered, turns on or off.
1443 -------- KEYS --------
1444 target: trigger all entities with this targetname when it goes off
1445 targetname: name that identifies this entity so it can be triggered; when off, it always uses the OFF state
1446 phase: offset of the timing
1447 wait: "on" cycle time (default: 1)
1448 respawntime: "off" cycle time (default: same as wait)
1449 -------- SPAWNFLAGS --------
1450 START_ON: assume it is already turned on (when targeted)
1451 */
1452 void spawnfunc_trigger_multivibrator()
1453 {
1454         if(!self.wait)
1455                 self.wait = 1;
1456         if(!self.respawntime)
1457                 self.respawntime = self.wait;
1458
1459         self.state = 0;
1460         self.use = multivibrator_toggle;
1461         self.think = multivibrator_send;
1462         self.nextthink = time;
1463
1464         IFTARGETED
1465                 multivibrator_reset();
1466 }
1467
1468
1469 void follow_init()
1470 {
1471         entity src, dst;
1472         src = world;
1473         dst = world;
1474         if(self.killtarget != "")
1475                 src = find(world, targetname, self.killtarget);
1476         if(self.target != "")
1477                 dst = find(world, targetname, self.target);
1478
1479         if(!src && !dst)
1480         {
1481                 objerror("follow: could not find target/killtarget");
1482                 return;
1483         }
1484
1485         if(self.jointtype)
1486         {
1487                 // already done :P entity must stay
1488                 self.aiment = src;
1489                 self.enemy = dst;
1490         }
1491         else if(!src || !dst)
1492         {
1493                 objerror("follow: could not find target/killtarget");
1494                 return;
1495         }
1496         else if(self.spawnflags & 1)
1497         {
1498                 // attach
1499                 if(self.spawnflags & 2)
1500                 {
1501                         setattachment(dst, src, self.message);
1502                 }
1503                 else
1504                 {
1505                         attach_sameorigin(dst, src, self.message);
1506                 }
1507
1508                 remove(self);
1509         }
1510         else
1511         {
1512                 if(self.spawnflags & 2)
1513                 {
1514                         dst.movetype = MOVETYPE_FOLLOW;
1515                         dst.aiment = src;
1516                         // dst.punchangle = '0 0 0'; // keep unchanged
1517                         dst.view_ofs = dst.origin;
1518                         dst.v_angle = dst.angles;
1519                 }
1520                 else
1521                 {
1522                         follow_sameorigin(dst, src);
1523                 }
1524
1525                 remove(self);
1526         }
1527 }
1528
1529 void spawnfunc_misc_follow()
1530 {
1531         InitializeEntity(self, follow_init, INITPRIO_FINDTARGET);
1532 }
1533
1534
1535
1536 void gamestart_use() {
1537         activator = self;
1538         SUB_UseTargets();
1539         remove(self);
1540 }
1541
1542 void spawnfunc_trigger_gamestart() {
1543         self.use = gamestart_use;
1544         self.reset2 = spawnfunc_trigger_gamestart;
1545
1546         if(self.wait)
1547         {
1548                 self.think = self.use;
1549                 self.nextthink = game_starttime + self.wait;
1550         }
1551         else
1552                 InitializeEntity(self, gamestart_use, INITPRIO_FINDTARGET);
1553 }
1554
1555
1556
1557
1558 .entity voicescript; // attached voice script
1559 .float voicescript_index; // index of next voice, or -1 to use the randomized ones
1560 .float voicescript_nextthink; // time to play next voice
1561 .float voicescript_voiceend; // time when this voice ends
1562
1563 void target_voicescript_clear(entity pl)
1564 {
1565         pl.voicescript = world;
1566 }
1567
1568 void target_voicescript_use()
1569 {
1570         if(activator.voicescript != self)
1571         {
1572                 activator.voicescript = self;
1573                 activator.voicescript_index = 0;
1574                 activator.voicescript_nextthink = time + self.delay;
1575         }
1576 }
1577
1578 void target_voicescript_next(entity pl)
1579 {
1580         entity vs;
1581         float i, n, dt;
1582
1583         vs = pl.voicescript;
1584         if(!vs)
1585                 return;
1586         if(vs.message == "")
1587                 return;
1588         if(pl.classname != "player")
1589                 return;
1590         if(gameover)
1591                 return;
1592
1593         if(time >= pl.voicescript_voiceend)
1594         {
1595                 if(time >= pl.voicescript_nextthink)
1596                 {
1597                         // get the next voice...
1598                         n = tokenize_console(vs.message);
1599
1600                         if(pl.voicescript_index < vs.cnt)
1601                                 i = pl.voicescript_index * 2;
1602                         else if(n > vs.cnt * 2)
1603                                 i = mod(pl.voicescript_index - vs.cnt, (n - vs.cnt * 2 - 1) / 2) * 2 + vs.cnt * 2 + 1;
1604                         else
1605                                 i = -1;
1606
1607                         if(i >= 0)
1608                         {
1609                                 play2(pl, strcat(vs.netname, "/", argv(i), ".wav"));
1610                                 dt = stof(argv(i + 1));
1611                                 if(dt >= 0)
1612                                 {
1613                                         pl.voicescript_voiceend = time + dt;
1614                                         pl.voicescript_nextthink = pl.voicescript_voiceend + vs.wait * (0.5 + random());
1615                                 }
1616                                 else
1617                                 {
1618                                         pl.voicescript_voiceend = time - dt;
1619                                         pl.voicescript_nextthink = pl.voicescript_voiceend;
1620                                 }
1621
1622                                 pl.voicescript_index += 1;
1623                         }
1624                         else
1625                         {
1626                                 pl.voicescript = world; // stop trying then
1627                         }
1628                 }
1629         }
1630 }
1631
1632 void spawnfunc_target_voicescript()
1633 {
1634         // netname: directory of the sound files
1635         // message: list of "sound file" duration "sound file" duration, a *, and again a list
1636         //          foo1 4.1 foo2 4.0 foo3 -3.1 * fool1 1.1 fool2 7.1 fool3 9.1 fool4 3.7
1637         //          Here, a - in front of the duration means that no delay is to be
1638         //          added after this message
1639         // wait: average time between messages
1640         // delay: initial delay before the first message
1641         
1642         float i, n;
1643         self.use = target_voicescript_use;
1644
1645         n = tokenize_console(self.message);
1646         self.cnt = n / 2;
1647         for(i = 0; i+1 < n; i += 2)
1648         {
1649                 if(argv(i) == "*")
1650                 {
1651                         self.cnt = i / 2;
1652                         ++i;
1653                 }
1654                 precache_sound(strcat(self.netname, "/", argv(i), ".wav"));
1655         }
1656 }
1657
1658
1659
1660 void trigger_relay_teamcheck_use()
1661 {
1662         if(activator.team)
1663         {
1664                 if(self.spawnflags & 2)
1665                 {
1666                         if(activator.team != self.team)
1667                                 SUB_UseTargets();
1668                 }
1669                 else
1670                 {
1671                         if(activator.team == self.team)
1672                                 SUB_UseTargets();
1673                 }
1674         }
1675         else
1676         {
1677                 if(self.spawnflags & 1)
1678                         SUB_UseTargets();
1679         }
1680 }
1681
1682 void trigger_relay_teamcheck_reset()
1683 {
1684         self.team = self.team_saved;
1685 }
1686
1687 void spawnfunc_trigger_relay_teamcheck()
1688 {
1689         self.team_saved = self.team;
1690         self.use = trigger_relay_teamcheck_use;
1691         self.reset = trigger_relay_teamcheck_reset;
1692 }
1693
1694
1695
1696 void trigger_disablerelay_use()
1697 {
1698         entity e;
1699
1700         float a, b;
1701         a = b = 0;
1702
1703         for(e = world; (e = find(e, targetname, self.target)); )
1704         {
1705                 if(e.use == SUB_UseTargets)
1706                 {
1707                         e.use = SUB_DontUseTargets;
1708                         ++a;
1709                 }
1710                 else if(e.use == SUB_DontUseTargets)
1711                 {
1712                         e.use = SUB_UseTargets;
1713                         ++b;
1714                 }
1715         }
1716
1717         if((!a) == (!b))
1718                 print("Invalid use of trigger_disablerelay: ", ftos(a), " relays were on, ", ftos(b), " relays were off!\n");
1719 }
1720
1721 void spawnfunc_trigger_disablerelay()
1722 {
1723         self.use = trigger_disablerelay_use;
1724 }
1725
1726 float magicear_matched;
1727 string trigger_magicear_processmessage(entity ear, entity source, float teamsay, entity privatesay, string msgin)
1728 {
1729         float domatch, dotrigger, matchstart, l;
1730         string s, msg;
1731         entity oldself;
1732
1733         magicear_matched = FALSE;
1734
1735         dotrigger = ((self.classname == "player") && (self.deadflag == DEAD_NO) && ((ear.radius == 0) || (vlen(source.origin - ear.origin) <= ear.radius)));
1736         domatch = ((ear.spawnflags & 32) || dotrigger);
1737         if not(domatch)
1738                 return msgin;
1739
1740         if(privatesay)
1741         {
1742                 if(ear.spawnflags & 4)
1743                         return msgin;
1744         }
1745         else
1746         {
1747                 if(!teamsay)
1748                         if(ear.spawnflags & 1)
1749                                 return msgin;
1750                 if(teamsay > 0)
1751                         if(ear.spawnflags & 2)
1752                                 return msgin;
1753                 if(teamsay < 0)
1754                         if(ear.spawnflags & 8)
1755                                 return msgin;
1756         }
1757         
1758         matchstart = -1;
1759         l = strlen(ear.message);
1760
1761         if(self.spawnflags & 128)
1762                 msg = msgin;
1763         else
1764                 msg = strdecolorize(msgin);
1765
1766         if(substring(ear.message, 0, 1) == "*")
1767         {
1768                 if(substring(ear.message, -1, 1) == "*")
1769                 {
1770                         // two wildcards
1771                         // as we need multi-replacement here...
1772                         s = substring(ear.message, 1, -2);
1773                         l -= 2;
1774                         if(strstrofs(msg, s, 0) >= 0)
1775                                 matchstart = -2; // we use strreplace on s
1776                 }
1777                 else
1778                 {
1779                         // match at start
1780                         s = substring(ear.message, 1, -1);
1781                         l -= 1;
1782                         if(substring(msg, -l, l) == s)
1783                                 matchstart = strlen(msg) - l;
1784                 }
1785         }
1786         else
1787         {
1788                 if(substring(ear.message, -1, 1) == "*")
1789                 {
1790                         // match at end
1791                         s = substring(ear.message, 0, -2);
1792                         l -= 1;
1793                         if(substring(msg, 0, l) == s)
1794                                 matchstart = 0;
1795                 }
1796                 else
1797                 {
1798                         // full match
1799                         s = ear.message;
1800                         if(msg == ear.message)
1801                                 matchstart = 0;
1802                 }
1803         }
1804
1805         if(matchstart == -1) // no match
1806                 return msgin;
1807
1808         magicear_matched = TRUE;
1809
1810         if(dotrigger)
1811         {
1812                 oldself = activator = self;
1813                 self = ear;
1814                 SUB_UseTargets();
1815                 self = oldself;
1816         }
1817
1818         if(ear.spawnflags & 16)
1819         {
1820                 return ear.netname;
1821         }
1822         else if(ear.netname != "")
1823         {
1824                 if(matchstart < 0)
1825                         return strreplace(s, ear.netname, msg);
1826                 else
1827                         return strcat(
1828                                 substring(msg, 0, matchstart),
1829                                 ear.netname,
1830                                 substring(msg, matchstart + l, -1)
1831                         );
1832         }
1833         else
1834                 return msgin;
1835 }
1836
1837 entity magicears;
1838 string trigger_magicear_processmessage_forallears(entity source, float teamsay, entity privatesay, string msgin)
1839 {
1840         entity ear;
1841         string msgout;
1842         for(ear = magicears; ear; ear = ear.enemy)
1843         {
1844                 msgout = trigger_magicear_processmessage(ear, source, teamsay, privatesay, msgin);
1845                 if not(ear.spawnflags & 64)
1846                         if(magicear_matched)
1847                                 return msgout;
1848                 msgin = msgout;
1849         }
1850         return msgin;
1851 }
1852
1853 void spawnfunc_trigger_magicear()
1854 {
1855         self.enemy = magicears;
1856         magicears = self;
1857
1858         // actually handled in "say" processing
1859         // spawnflags:
1860         //   1 = ignore say
1861         //   2 = ignore teamsay
1862         //   4 = ignore tell
1863         //   8 = ignore tell to unknown player
1864         //   16 = let netname replace the whole message (otherwise, netname is a word replacement if set)
1865         //   32 = perform the replacement even if outside the radius or dead
1866         //   64 = continue replacing/triggering even if this one matched
1867         // message: either
1868         //   *pattern*
1869         // or
1870         //   *pattern
1871         // or
1872         //   pattern*
1873         // or
1874         //   pattern
1875         // netname:
1876         //   if set, replacement for the matched text
1877         // radius:
1878         //   "hearing distance"
1879         // target:
1880         //   what to trigger
1881 }