]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/server/g_triggers.qc
Working untouch code, but delayed by 1 second. I think the only way to fix this would...
[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;
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 = 0;
545                 self.nextthink = self.owner.triggergravity = 0;
546                 remove(self);
547         }
548         else
549                 self.nextthink = time + 0.1;
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 + 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.gravity = self.gravity;
574                         if(self.noise != "")
575                                 sound (other, CHAN_AUTO, self.noise, VOL_BASE, ATTN_NORM);
576                 }
577         }
578 };
579
580 void spawnfunc_trigger_gravity()
581 {
582         if(!self.gravity)
583                 return;
584         EXACTTRIGGER_INIT;
585         self.touch = trigger_gravity_touch;
586         if(self.noise != "")
587                 precache_sound(self.noise);
588 };
589
590 // TODO add a way to do looped sounds with sound(); then complete this entity
591 .float volume, atten;
592 void target_speaker_use() {sound(self, CHAN_TRIGGER, self.noise, VOL_BASE * self.volume, self.atten);}
593
594 void spawnfunc_target_speaker()
595 {
596         if(self.noise)
597                 precache_sound (self.noise);
598         IFTARGETED
599         {
600                 if(!self.atten)
601                         self.atten = ATTN_NORM;
602                 else if(self.atten < 0)
603                         self.atten = 0;
604                 if(!self.volume)
605                         self.volume = 1;
606                 self.use = target_speaker_use;
607         }
608         else
609         {
610                 if(!self.atten)
611                         self.atten = ATTN_STATIC;
612                 else if(self.atten < 0)
613                         self.atten = 0;
614                 if(!self.volume)
615                         self.volume = 1;
616                 ambientsound (self.origin, self.noise, VOL_BASE * self.volume, self.atten);
617         }
618 };
619
620
621 void spawnfunc_func_stardust() {
622         self.effects = EF_STARDUST;
623 }
624
625 .string bgmscript;
626 .float bgmscriptattack;
627 .float bgmscriptdecay;
628 .float bgmscriptsustain;
629 .float bgmscriptrelease;
630 float pointparticles_SendEntity(entity to, float fl)
631 {
632         WriteByte(MSG_ENTITY, ENT_CLIENT_POINTPARTICLES);
633
634         // optional features to save space
635         fl = fl & 0x0F;
636         if(self.spawnflags & 2)
637                 fl |= 0x10; // absolute count on toggle-on
638         if(self.movedir != '0 0 0' || self.velocity != '0 0 0')
639                 fl |= 0x20; // 4 bytes - saves CPU
640         if(self.waterlevel || self.count != 1)
641                 fl |= 0x40; // 4 bytes - obscure features almost never used
642         if(self.mins != '0 0 0' || self.maxs != '0 0 0')
643                 fl |= 0x80; // 14 bytes - saves lots of space
644
645         WriteByte(MSG_ENTITY, fl);
646         if(fl & 2)
647         {
648                 if(self.state)
649                         WriteCoord(MSG_ENTITY, self.impulse);
650                 else
651                         WriteCoord(MSG_ENTITY, 0); // off
652         }
653         if(fl & 4)
654         {
655                 WriteCoord(MSG_ENTITY, self.origin_x);
656                 WriteCoord(MSG_ENTITY, self.origin_y);
657                 WriteCoord(MSG_ENTITY, self.origin_z);
658         }
659         if(fl & 1)
660         {
661                 if(self.model != "null")
662                 {
663                         WriteShort(MSG_ENTITY, self.modelindex);
664                         if(fl & 0x80)
665                         {
666                                 WriteCoord(MSG_ENTITY, self.mins_x);
667                                 WriteCoord(MSG_ENTITY, self.mins_y);
668                                 WriteCoord(MSG_ENTITY, self.mins_z);
669                                 WriteCoord(MSG_ENTITY, self.maxs_x);
670                                 WriteCoord(MSG_ENTITY, self.maxs_y);
671                                 WriteCoord(MSG_ENTITY, self.maxs_z);
672                         }
673                 }
674                 else
675                 {
676                         WriteShort(MSG_ENTITY, 0);
677                         if(fl & 0x80)
678                         {
679                                 WriteCoord(MSG_ENTITY, self.maxs_x);
680                                 WriteCoord(MSG_ENTITY, self.maxs_y);
681                                 WriteCoord(MSG_ENTITY, self.maxs_z);
682                         }
683                 }
684                 WriteShort(MSG_ENTITY, self.cnt);
685                 if(fl & 0x20)
686                 {
687                         WriteShort(MSG_ENTITY, compressShortVector(self.velocity));
688                         WriteShort(MSG_ENTITY, compressShortVector(self.movedir));
689                 }
690                 if(fl & 0x40)
691                 {
692                         WriteShort(MSG_ENTITY, self.waterlevel * 16.0);
693                         WriteByte(MSG_ENTITY, self.count * 16.0);
694                 }
695                 WriteString(MSG_ENTITY, self.noise);
696                 if(self.noise != "")
697                 {
698                         WriteByte(MSG_ENTITY, floor(self.atten * 64));
699                         WriteByte(MSG_ENTITY, floor(self.volume * 255));
700                 }
701                 WriteString(MSG_ENTITY, self.bgmscript);
702                 if(self.bgmscript != "")
703                 {
704                         WriteByte(MSG_ENTITY, floor(self.bgmscriptattack * 64));
705                         WriteByte(MSG_ENTITY, floor(self.bgmscriptdecay * 64));
706                         WriteByte(MSG_ENTITY, floor(self.bgmscriptsustain * 255));
707                         WriteByte(MSG_ENTITY, floor(self.bgmscriptrelease * 64));
708                 }
709         }
710         return 1;
711 }
712
713 void pointparticles_use()
714 {
715         self.state = !self.state;
716         self.SendFlags |= 2;
717 }
718
719 void pointparticles_think()
720 {
721         if(self.origin != self.oldorigin)
722         {
723                 self.SendFlags |= 4;
724                 self.oldorigin = self.origin;
725         }
726         self.nextthink = time;
727 }
728
729 void pointparticles_reset()
730 {
731         if(self.spawnflags & 1)
732                 self.state = 1;
733         else
734                 self.state = 0;
735 }
736
737 void spawnfunc_func_pointparticles()
738 {
739         if(self.model != "")
740                 setmodel(self, self.model);
741         if(self.noise != "")
742                 precache_sound (self.noise);
743         
744         if(!self.bgmscriptsustain)
745                 self.bgmscriptsustain = 1;
746         else if(self.bgmscriptsustain < 0)
747                 self.bgmscriptsustain = 0;
748
749         if(!self.atten)
750                 self.atten = ATTN_NORM;
751         else if(self.atten < 0)
752                 self.atten = 0;
753         if(!self.volume)
754                 self.volume = 1;
755         if(!self.count)
756                 self.count = 1;
757         if(!self.impulse)
758                 self.impulse = 1;
759
760         if(!self.modelindex)
761         {
762                 setorigin(self, self.origin + self.mins);
763                 setsize(self, '0 0 0', self.maxs - self.mins);
764         }
765         if(!self.cnt)
766                 self.cnt = particleeffectnum(self.mdl);
767
768         Net_LinkEntity(self, FALSE, 0, pointparticles_SendEntity);
769
770         IFTARGETED
771         {
772                 self.use = pointparticles_use;
773                 self.reset = pointparticles_reset;
774                 self.reset();
775         }
776         else
777                 self.state = 1;
778         self.think = pointparticles_think;
779         self.nextthink = time;
780 }
781
782 void spawnfunc_func_sparks()
783 {
784         // self.cnt is the amount of sparks that one burst will spawn
785         if(self.cnt < 1) {
786                 self.cnt = 25.0; // nice default value
787         }
788
789         // self.wait is the probability that a sparkthink will spawn a spark shower
790         // range: 0 - 1, but 0 makes little sense, so...
791         if(self.wait < 0.05) {
792                 self.wait = 0.25; // nice default value
793         }
794
795         self.count = self.cnt;
796         self.mins = '0 0 0';
797         self.maxs = '0 0 0';
798         self.velocity = '0 0 -1';
799         self.mdl = "TE_SPARK";
800         self.impulse = 10 * self.wait; // by default 2.5/sec
801         self.wait = 0;
802         self.cnt = 0; // use mdl
803
804         spawnfunc_func_pointparticles();
805 }
806
807 float rainsnow_SendEntity(entity to, float sf)
808 {
809         WriteByte(MSG_ENTITY, ENT_CLIENT_RAINSNOW);
810         WriteByte(MSG_ENTITY, self.state);
811         WriteCoord(MSG_ENTITY, self.origin_x + self.mins_x);
812         WriteCoord(MSG_ENTITY, self.origin_y + self.mins_y);
813         WriteCoord(MSG_ENTITY, self.origin_z + self.mins_z);
814         WriteCoord(MSG_ENTITY, self.maxs_x - self.mins_x);
815         WriteCoord(MSG_ENTITY, self.maxs_y - self.mins_y);
816         WriteCoord(MSG_ENTITY, self.maxs_z - self.mins_z);
817         WriteShort(MSG_ENTITY, compressShortVector(self.dest));
818         WriteShort(MSG_ENTITY, self.count);
819         WriteByte(MSG_ENTITY, self.cnt);
820         return 1;
821 };
822
823 /*QUAKED spawnfunc_func_rain (0 .5 .8) ?
824 This is an invisible area like a trigger, which rain falls inside of.
825
826 Keys:
827 "velocity"
828  falling direction (should be something like '0 0 -700', use the X and Y velocity for wind)
829 "cnt"
830  sets color of rain (default 12 - white)
831 "count"
832  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
833 */
834 void spawnfunc_func_rain()
835 {
836         self.dest = self.velocity;
837         self.velocity = '0 0 0';
838         if (!self.dest)
839                 self.dest = '0 0 -700';
840         self.angles = '0 0 0';
841         self.movetype = MOVETYPE_NONE;
842         self.solid = SOLID_NOT;
843         SetBrushEntityModel();
844         if (!self.cnt)
845                 self.cnt = 12;
846         if (!self.count)
847                 self.count = 2000;
848         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
849         if (self.count < 1)
850                 self.count = 1;
851         if(self.count > 65535)
852                 self.count = 65535;
853
854         self.state = 1; // 1 is rain, 0 is snow
855         self.Version = 1;
856
857         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
858 };
859
860
861 /*QUAKED spawnfunc_func_snow (0 .5 .8) ?
862 This is an invisible area like a trigger, which snow falls inside of.
863
864 Keys:
865 "velocity"
866  falling direction (should be something like '0 0 -300', use the X and Y velocity for wind)
867 "cnt"
868  sets color of rain (default 12 - white)
869 "count"
870  adjusts density, this many particles fall every second for a 1024x1024 area, default is 2000
871 */
872 void spawnfunc_func_snow()
873 {
874         self.dest = self.velocity;
875         self.velocity = '0 0 0';
876         if (!self.dest)
877                 self.dest = '0 0 -300';
878         self.angles = '0 0 0';
879         self.movetype = MOVETYPE_NONE;
880         self.solid = SOLID_NOT;
881         SetBrushEntityModel();
882         if (!self.cnt)
883                 self.cnt = 12;
884         if (!self.count)
885                 self.count = 2000;
886         self.count = 0.01 * self.count * (self.size_x / 1024) * (self.size_y / 1024);
887         if (self.count < 1)
888                 self.count = 1;
889         if(self.count > 65535)
890                 self.count = 65535;
891
892         self.state = 0; // 1 is rain, 0 is snow
893         self.Version = 1;
894
895         Net_LinkEntity(self, FALSE, 0, rainsnow_SendEntity);
896 };
897
898
899 void FireRailgunBullet (vector start, vector end, float bdamage, float bforce, float mindist, float maxdist, float halflifedist, float forcehalflifedist, float deathtype);
900
901 .float modelscale;
902 void misc_laser_aim()
903 {
904         vector a;
905         if(self.enemy)
906         {
907                 if(self.spawnflags & 2)
908                 {
909                         if(self.enemy.origin != self.mangle)
910                         {
911                                 self.mangle = self.enemy.origin;
912                                 self.SendFlags |= 2;
913                         }
914                 }
915                 else
916                 {
917                         a = vectoangles(self.enemy.origin - self.origin);
918                         a_x = -a_x;
919                         if(a != self.mangle)
920                         {
921                                 self.mangle = a;
922                                 self.SendFlags |= 2;
923                         }
924                 }
925         }
926         else
927         {
928                 if(self.angles != self.mangle)
929                 {
930                         self.mangle = self.angles;
931                         self.SendFlags |= 2;
932                 }
933         }
934         if(self.origin != self.oldorigin)
935         {
936                 self.SendFlags |= 1;
937                 self.oldorigin = self.origin;
938         }
939 }
940
941 void misc_laser_init()
942 {
943         if(self.target != "")
944                 self.enemy = find(world, targetname, self.target);
945 }
946
947 .entity pusher;
948 void misc_laser_think()
949 {
950         vector o;
951         entity oldself;
952
953         self.nextthink = time;
954
955         if(!self.state)
956                 return;
957
958         misc_laser_aim();
959
960         if(self.enemy)
961         {
962                 o = self.enemy.origin;
963                 if not(self.spawnflags & 2)
964                         o = self.origin + normalize(o - self.origin) * 32768;
965         }
966         else
967         {
968                 makevectors(self.mangle);
969                 o = self.origin + v_forward * 32768;
970         }
971
972         if(self.dmg)
973         {
974                 if(self.dmg < 0)
975                         FireRailgunBullet(self.origin, o, 100000, 0, 0, 0, 0, 0, DEATH_HURTTRIGGER);
976                 else
977                         FireRailgunBullet(self.origin, o, self.dmg * frametime, 0, 0, 0, 0, 0, DEATH_HURTTRIGGER);
978         }
979
980         if(self.enemy.target != "") // DETECTOR laser
981         {
982                 traceline(self.origin, o, MOVE_NORMAL, self);
983                 if(trace_ent.iscreature)
984                 {
985                         self.pusher = trace_ent;
986                         if(!self.count)
987                         {
988                                 self.count = 1;
989
990                                 oldself = self;
991                                 self = self.enemy;
992                                 activator = self.pusher;
993                                 SUB_UseTargets();
994                                 self = oldself;
995                         }
996                 }
997                 else
998                 {
999                         if(self.count)
1000                         {
1001                                 self.count = 0;
1002
1003                                 oldself = self;
1004                                 self = self.enemy;
1005                                 activator = self.pusher;
1006                                 SUB_UseTargets();
1007                                 self = oldself;
1008                         }
1009                 }
1010         }
1011 }
1012
1013 float laser_SendEntity(entity to, float fl)
1014 {
1015         WriteByte(MSG_ENTITY, ENT_CLIENT_LASER);
1016         fl = fl - (fl & 0xE0); // use that bit to indicate finite length laser
1017         if(self.spawnflags & 2)
1018                 fl |= 0x80;
1019         if(self.alpha)
1020                 fl |= 0x40;
1021         if(self.scale != 1 || self.modelscale != 1)
1022                 fl |= 0x20;
1023         WriteByte(MSG_ENTITY, fl);
1024         if(fl & 1)
1025         {
1026                 WriteCoord(MSG_ENTITY, self.origin_x);
1027                 WriteCoord(MSG_ENTITY, self.origin_y);
1028                 WriteCoord(MSG_ENTITY, self.origin_z);
1029         }
1030         if(fl & 8)
1031         {
1032                 WriteByte(MSG_ENTITY, self.colormod_x * 255.0);
1033                 WriteByte(MSG_ENTITY, self.colormod_y * 255.0);
1034                 WriteByte(MSG_ENTITY, self.colormod_z * 255.0);
1035                 if(fl & 0x40)
1036                         WriteByte(MSG_ENTITY, self.alpha * 255.0);
1037                 if(fl & 0x20)
1038                 {
1039                         WriteByte(MSG_ENTITY, bound(0, self.scale * 16.0, 255));
1040                         WriteByte(MSG_ENTITY, bound(0, self.modelscale * 16.0, 255));
1041                 }
1042                 WriteShort(MSG_ENTITY, self.cnt + 1);
1043         }
1044         if(fl & 2)
1045         {
1046                 if(fl & 0x80)
1047                 {
1048                         WriteCoord(MSG_ENTITY, self.enemy.origin_x);
1049                         WriteCoord(MSG_ENTITY, self.enemy.origin_y);
1050                         WriteCoord(MSG_ENTITY, self.enemy.origin_z);
1051                 }
1052                 else
1053                 {
1054                         WriteAngle(MSG_ENTITY, self.mangle_x);
1055                         WriteAngle(MSG_ENTITY, self.mangle_y);
1056                 }
1057         }
1058         if(fl & 4)
1059                 WriteByte(MSG_ENTITY, self.state);
1060         return 1;
1061 }
1062
1063 /*QUAKED spawnfunc_misc_laser (.5 .5 .5) ? START_ON DEST_IS_FIXED
1064 Any object touching the beam will be hurt
1065 Keys:
1066 "target"
1067  spawnfunc_target_position where the laser ends
1068 "mdl"
1069  name of beam end effect to use
1070 "colormod"
1071  color of the beam (default: red)
1072 "dmg"
1073  damage per second (-1 for a laser that kills immediately)
1074 */
1075 void laser_use()
1076 {
1077         self.state = !self.state;
1078         self.SendFlags |= 4;
1079         misc_laser_aim();
1080 }
1081
1082 void laser_reset()
1083 {
1084         if(self.spawnflags & 1)
1085                 self.state = 1;
1086         else
1087                 self.state = 0;
1088 }
1089
1090 void spawnfunc_misc_laser()
1091 {
1092         if(self.mdl)
1093         {
1094                 if(self.mdl == "none")
1095                         self.cnt = -1;
1096                 else
1097                 {
1098                         self.cnt = particleeffectnum(self.mdl);
1099                         if(self.cnt < 0)
1100                                 if(self.dmg)
1101                                         self.cnt = particleeffectnum("laser_deadly");
1102                 }
1103         }
1104         else if(!self.cnt)
1105         {
1106                 if(self.dmg)
1107                         self.cnt = particleeffectnum("laser_deadly");
1108                 else
1109                         self.cnt = -1;
1110         }
1111         if(self.cnt < 0)
1112                 self.cnt = -1;
1113
1114         if(self.colormod == '0 0 0')
1115                 if(!self.alpha)
1116                         self.colormod = '1 0 0';
1117         if(!self.message)
1118                 self.message = "saw the light";
1119         if (!self.message2)
1120                 self.message2 = "was pushed into a laser by";
1121         if(!self.scale)
1122                 self.scale = 1;
1123         if(!self.modelscale)
1124                 self.modelscale = 1;
1125         self.think = misc_laser_think;
1126         self.nextthink = time;
1127         InitializeEntity(self, misc_laser_init, INITPRIO_FINDTARGET);
1128
1129         self.mangle = self.angles;
1130
1131         Net_LinkEntity(self, FALSE, 0, laser_SendEntity);
1132
1133         IFTARGETED
1134         {
1135                 self.reset = laser_reset;
1136                 laser_reset();
1137                 self.use = laser_use;
1138         }
1139         else
1140                 self.state = 1;
1141 }
1142
1143 // tZorks trigger impulse / gravity
1144 .float radius;
1145 .float falloff;
1146 .float strength;
1147 .float lastpushtime;
1148
1149 // targeted (directional) mode
1150 void trigger_impulse_touch1()
1151 {
1152         entity targ;
1153     float pushdeltatime;
1154     float str;
1155
1156         // FIXME: Better checking for what to push and not.
1157         if not(other.iscreature)
1158         if (other.classname != "corpse")
1159         if (other.classname != "body")
1160         if (other.classname != "gib")
1161         if (other.classname != "missile")
1162         if (other.classname != "rocket")
1163         if (other.classname != "casing")
1164         if (other.classname != "grenade")
1165         if (other.classname != "plasma")
1166         if (other.classname != "plasma_prim")
1167         if (other.classname != "plasma_chain")
1168         if (other.classname != "droppedweapon")
1169         if (other.classname != "nexball_basketball")
1170         if (other.classname != "nexball_football")
1171                 return;
1172
1173         if (other.deadflag && other.iscreature)
1174                 return;
1175
1176         EXACTTRIGGER_TOUCH;
1177
1178     targ = find(world, targetname, self.target);
1179     if(!targ)
1180     {
1181         objerror("trigger_force without a (valid) .target!\n");
1182         remove(self);
1183         return;
1184     }
1185
1186     if(self.falloff == 1)
1187         str = (str / self.radius) * self.strength;
1188     else if(self.falloff == 2)
1189         str = (1 - (str / self.radius)) * self.strength;
1190     else
1191         str = self.strength;
1192
1193     pushdeltatime = time - other.lastpushtime;
1194     if (pushdeltatime > 0.15) pushdeltatime = 0;
1195     other.lastpushtime = time;
1196     if(!pushdeltatime) return;
1197
1198     other.velocity = other.velocity + normalize(targ.origin - self.origin) * str * pushdeltatime;
1199         other.flags &~= FL_ONGROUND;
1200 }
1201
1202 // Directionless (accelerator/decelerator) mode
1203 void trigger_impulse_touch2()
1204 {
1205     float pushdeltatime;
1206
1207         // FIXME: Better checking for what to push and not.
1208         if not(other.iscreature)
1209         if (other.classname != "corpse")
1210         if (other.classname != "body")
1211         if (other.classname != "gib")
1212         if (other.classname != "missile")
1213         if (other.classname != "rocket")
1214         if (other.classname != "casing")
1215         if (other.classname != "grenade")
1216         if (other.classname != "plasma")
1217         if (other.classname != "plasma_prim")
1218         if (other.classname != "plasma_chain")
1219         if (other.classname != "droppedweapon")
1220         if (other.classname != "nexball_basketball")
1221         if (other.classname != "nexball_football")
1222                 return;
1223
1224         if (other.deadflag && other.iscreature)
1225                 return;
1226
1227         EXACTTRIGGER_TOUCH;
1228
1229     pushdeltatime = time - other.lastpushtime;
1230     if (pushdeltatime > 0.15) pushdeltatime = 0;
1231     other.lastpushtime = time;
1232     if(!pushdeltatime) return;
1233
1234     // div0: ticrate independent, 1 = identity (not 20)
1235     other.velocity = other.velocity * pow(self.strength, pushdeltatime);
1236 }
1237
1238 // Spherical (gravity/repulsor) mode
1239 void trigger_impulse_touch3()
1240 {
1241     float pushdeltatime;
1242     float str;
1243
1244         // FIXME: Better checking for what to push and not.
1245         if not(other.iscreature)
1246         if (other.classname != "corpse")
1247         if (other.classname != "body")
1248         if (other.classname != "gib")
1249         if (other.classname != "missile")
1250         if (other.classname != "rocket")
1251         if (other.classname != "casing")
1252         if (other.classname != "grenade")
1253         if (other.classname != "plasma")
1254         if (other.classname != "plasma_prim")
1255         if (other.classname != "plasma_chain")
1256         if (other.classname != "droppedweapon")
1257         if (other.classname != "nexball_basketball")
1258         if (other.classname != "nexball_football")
1259                 return;
1260
1261         if (other.deadflag && other.iscreature)
1262                 return;
1263
1264         EXACTTRIGGER_TOUCH;
1265
1266     pushdeltatime = time - other.lastpushtime;
1267     if (pushdeltatime > 0.15) pushdeltatime = 0;
1268     other.lastpushtime = time;
1269     if(!pushdeltatime) return;
1270
1271     setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1272
1273         str = min(self.radius, vlen(self.origin - other.origin));
1274
1275     if(self.falloff == 1)
1276         str = (1 - str / self.radius) * self.strength; // 1 in the inside
1277     else if(self.falloff == 2)
1278         str = (str / self.radius) * self.strength; // 0 in the inside
1279     else
1280         str = self.strength;
1281
1282     other.velocity = other.velocity + normalize(other.origin - self.origin) * str * pushdeltatime;
1283 }
1284
1285 /*QUAKED spawnfunc_trigger_impulse (.5 .5 .5) ?
1286 -------- KEYS --------
1287 target : If this is set, this points to the spawnfunc_target_position to which the player will get pushed.
1288          If not, this trigger acts like a damper/accelerator field.
1289
1290 strength : This is how mutch force to add in the direction of .target each second
1291            when .target is set. If not, this is hoe mutch to slow down/accelerate
1292            someting cought inside this trigger. (1=no change, 0,5 half speed rougthly each tic, 2 = doubble)
1293
1294 radius   : If set, act as a spherical device rather then a liniar one.
1295
1296 falloff : 0 = none, 1 = liniar, 2 = inverted liniar
1297
1298 -------- NOTES --------
1299 Use a brush textured with common/origin in the trigger entity to determine the origin of the force
1300 in directional and sperical mode. For damper/accelerator mode this is not nessesary (and has no effect).
1301 */
1302
1303 void spawnfunc_trigger_impulse()
1304 {
1305         EXACTTRIGGER_INIT;
1306     if(self.radius)
1307     {
1308         if(!self.strength) self.strength = 2000 * cvar("g_triggerimpulse_radial_multiplier");
1309         setorigin(self, self.origin);
1310         setsize(self, '-1 -1 -1' * self.radius,'1 1 1' * self.radius);
1311         self.touch = trigger_impulse_touch3;
1312     }
1313     else
1314     {
1315         if(self.target)
1316         {
1317             if(!self.strength) self.strength = 950 * cvar("g_triggerimpulse_directional_multiplier");
1318             self.touch = trigger_impulse_touch1;
1319         }
1320         else
1321         {
1322             if(!self.strength) self.strength = 0.9;
1323                         self.strength = pow(self.strength, cvar("g_triggerimpulse_accel_power")) * cvar("g_triggerimpulse_accel_multiplier");
1324             self.touch = trigger_impulse_touch2;
1325         }
1326     }
1327 }
1328
1329 /*QUAKED spawnfunc_trigger_flipflop (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ENABLED
1330 "Flip-flop" trigger gate... lets only every second trigger event through
1331 */
1332 void flipflop_use()
1333 {
1334         self.state = !self.state;
1335         if(self.state)
1336                 SUB_UseTargets();
1337 }
1338
1339 void spawnfunc_trigger_flipflop()
1340 {
1341         if(self.spawnflags & 1)
1342                 self.state = 1;
1343         self.use = flipflop_use;
1344         self.reset = spawnfunc_trigger_flipflop; // perfect resetter
1345 }
1346
1347 /*QUAKED spawnfunc_trigger_monoflop (.5 .5 .5) (-8 -8 -8) (8 8 8)
1348 "Mono-flop" trigger gate... turns one trigger event into one "on" and one "off" event, separated by a delay of "wait"
1349 */
1350 void monoflop_use()
1351 {
1352         self.nextthink = time + self.wait;
1353         self.enemy = activator;
1354         if(self.state)
1355                 return;
1356         self.state = 1;
1357         SUB_UseTargets();
1358 }
1359 void monoflop_fixed_use()
1360 {
1361         if(self.state)
1362                 return;
1363         self.nextthink = time + self.wait;
1364         self.state = 1;
1365         self.enemy = activator;
1366         SUB_UseTargets();
1367 }
1368
1369 void monoflop_think()
1370 {
1371         self.state = 0;
1372         activator = self.enemy;
1373         SUB_UseTargets();
1374 }
1375
1376 void monoflop_reset()
1377 {
1378         self.state = 0;
1379         self.nextthink = 0;
1380 }
1381
1382 void spawnfunc_trigger_monoflop()
1383 {
1384         if(!self.wait)
1385                 self.wait = 1;
1386         if(self.spawnflags & 1)
1387                 self.use = monoflop_fixed_use;
1388         else
1389                 self.use = monoflop_use;
1390         self.think = monoflop_think;
1391         self.state = 0;
1392         self.reset = monoflop_reset;
1393 }
1394
1395 void multivibrator_send()
1396 {
1397         float newstate;
1398         float cyclestart;
1399
1400         cyclestart = floor((time + self.phase) / (self.wait + self.respawntime)) * (self.wait + self.respawntime) - self.phase;
1401
1402         newstate = (time < cyclestart + self.wait);
1403
1404         activator = self;
1405         if(self.state != newstate)
1406                 SUB_UseTargets();
1407         self.state = newstate;
1408
1409         if(self.state)
1410                 self.nextthink = cyclestart + self.wait + 0.01;
1411         else
1412                 self.nextthink = cyclestart + self.wait + self.respawntime + 0.01;
1413 }
1414
1415 void multivibrator_toggle()
1416 {
1417         if(self.nextthink == 0)
1418         {
1419                 multivibrator_send();
1420         }
1421         else
1422         {
1423                 if(self.state)
1424                 {
1425                         SUB_UseTargets();
1426                         self.state = 0;
1427                 }
1428                 self.nextthink = 0;
1429         }
1430 }
1431
1432 void multivibrator_reset()
1433 {
1434         if(!(self.spawnflags & 1))
1435                 self.nextthink = 0; // wait for a trigger event
1436         else
1437                 self.nextthink = max(1, time);
1438 }
1439
1440 /*QUAKED trigger_multivibrator (.5 .5 .5) (-8 -8 -8) (8 8 8) START_ON
1441 "Multivibrator" trigger gate... repeatedly sends trigger events. When triggered, turns on or off.
1442 -------- KEYS --------
1443 target: trigger all entities with this targetname when it goes off
1444 targetname: name that identifies this entity so it can be triggered; when off, it always uses the OFF state
1445 phase: offset of the timing
1446 wait: "on" cycle time (default: 1)
1447 respawntime: "off" cycle time (default: same as wait)
1448 -------- SPAWNFLAGS --------
1449 START_ON: assume it is already turned on (when targeted)
1450 */
1451 void spawnfunc_trigger_multivibrator()
1452 {
1453         if(!self.wait)
1454                 self.wait = 1;
1455         if(!self.respawntime)
1456                 self.respawntime = self.wait;
1457
1458         self.state = 0;
1459         self.use = multivibrator_toggle;
1460         self.think = multivibrator_send;
1461         self.nextthink = time;
1462
1463         IFTARGETED
1464                 multivibrator_reset();
1465 }
1466
1467
1468 void follow_init()
1469 {
1470         entity src, dst;
1471         src = world;
1472         dst = world;
1473         if(self.killtarget != "")
1474                 src = find(world, targetname, self.killtarget);
1475         if(self.target != "")
1476                 dst = find(world, targetname, self.target);
1477
1478         if(!src && !dst)
1479         {
1480                 objerror("follow: could not find target/killtarget");
1481                 return;
1482         }
1483
1484         if(self.jointtype)
1485         {
1486                 // already done :P entity must stay
1487                 self.aiment = src;
1488                 self.enemy = dst;
1489         }
1490         else if(!src || !dst)
1491         {
1492                 objerror("follow: could not find target/killtarget");
1493                 return;
1494         }
1495         else if(self.spawnflags & 1)
1496         {
1497                 // attach
1498                 if(self.spawnflags & 2)
1499                 {
1500                         setattachment(dst, src, self.message);
1501                 }
1502                 else
1503                 {
1504                         attach_sameorigin(dst, src, self.message);
1505                 }
1506
1507                 remove(self);
1508         }
1509         else
1510         {
1511                 if(self.spawnflags & 2)
1512                 {
1513                         dst.movetype = MOVETYPE_FOLLOW;
1514                         dst.aiment = src;
1515                         // dst.punchangle = '0 0 0'; // keep unchanged
1516                         dst.view_ofs = dst.origin;
1517                         dst.v_angle = dst.angles;
1518                 }
1519                 else
1520                 {
1521                         follow_sameorigin(dst, src);
1522                 }
1523
1524                 remove(self);
1525         }
1526 }
1527
1528 void spawnfunc_misc_follow()
1529 {
1530         InitializeEntity(self, follow_init, INITPRIO_FINDTARGET);
1531 }
1532
1533
1534
1535 void gamestart_use() {
1536         activator = self;
1537         SUB_UseTargets();
1538         remove(self);
1539 }
1540
1541 void spawnfunc_trigger_gamestart() {
1542         self.use = gamestart_use;
1543         self.reset2 = spawnfunc_trigger_gamestart;
1544
1545         if(self.wait)
1546         {
1547                 self.think = self.use;
1548                 self.nextthink = game_starttime + self.wait;
1549         }
1550         else
1551                 InitializeEntity(self, gamestart_use, INITPRIO_FINDTARGET);
1552 }
1553
1554
1555
1556
1557 .entity voicescript; // attached voice script
1558 .float voicescript_index; // index of next voice, or -1 to use the randomized ones
1559 .float voicescript_nextthink; // time to play next voice
1560 .float voicescript_voiceend; // time when this voice ends
1561
1562 void target_voicescript_clear(entity pl)
1563 {
1564         pl.voicescript = world;
1565 }
1566
1567 void target_voicescript_use()
1568 {
1569         if(activator.voicescript != self)
1570         {
1571                 activator.voicescript = self;
1572                 activator.voicescript_index = 0;
1573                 activator.voicescript_nextthink = time + self.delay;
1574         }
1575 }
1576
1577 void target_voicescript_next(entity pl)
1578 {
1579         entity vs;
1580         float i, n, dt;
1581
1582         vs = pl.voicescript;
1583         if(!vs)
1584                 return;
1585         if(vs.message == "")
1586                 return;
1587         if(pl.classname != "player")
1588                 return;
1589         if(gameover)
1590                 return;
1591
1592         if(time >= pl.voicescript_voiceend)
1593         {
1594                 if(time >= pl.voicescript_nextthink)
1595                 {
1596                         // get the next voice...
1597                         n = tokenize_console(vs.message);
1598
1599                         if(pl.voicescript_index < vs.cnt)
1600                                 i = pl.voicescript_index * 2;
1601                         else if(n > vs.cnt * 2)
1602                                 i = mod(pl.voicescript_index - vs.cnt, (n - vs.cnt * 2 - 1) / 2) * 2 + vs.cnt * 2 + 1;
1603                         else
1604                                 i = -1;
1605
1606                         if(i >= 0)
1607                         {
1608                                 play2(pl, strcat(vs.netname, "/", argv(i), ".wav"));
1609                                 dt = stof(argv(i + 1));
1610                                 if(dt >= 0)
1611                                 {
1612                                         pl.voicescript_voiceend = time + dt;
1613                                         pl.voicescript_nextthink = pl.voicescript_voiceend + vs.wait * (0.5 + random());
1614                                 }
1615                                 else
1616                                 {
1617                                         pl.voicescript_voiceend = time - dt;
1618                                         pl.voicescript_nextthink = pl.voicescript_voiceend;
1619                                 }
1620
1621                                 pl.voicescript_index += 1;
1622                         }
1623                         else
1624                         {
1625                                 pl.voicescript = world; // stop trying then
1626                         }
1627                 }
1628         }
1629 }
1630
1631 void spawnfunc_target_voicescript()
1632 {
1633         // netname: directory of the sound files
1634         // message: list of "sound file" duration "sound file" duration, a *, and again a list
1635         //          foo1 4.1 foo2 4.0 foo3 -3.1 * fool1 1.1 fool2 7.1 fool3 9.1 fool4 3.7
1636         //          Here, a - in front of the duration means that no delay is to be
1637         //          added after this message
1638         // wait: average time between messages
1639         // delay: initial delay before the first message
1640         
1641         float i, n;
1642         self.use = target_voicescript_use;
1643
1644         n = tokenize_console(self.message);
1645         self.cnt = n / 2;
1646         for(i = 0; i+1 < n; i += 2)
1647         {
1648                 if(argv(i) == "*")
1649                 {
1650                         self.cnt = i / 2;
1651                         ++i;
1652                 }
1653                 precache_sound(strcat(self.netname, "/", argv(i), ".wav"));
1654         }
1655 }
1656
1657
1658
1659 void trigger_relay_teamcheck_use()
1660 {
1661         if(activator.team)
1662         {
1663                 if(self.spawnflags & 2)
1664                 {
1665                         if(activator.team != self.team)
1666                                 SUB_UseTargets();
1667                 }
1668                 else
1669                 {
1670                         if(activator.team == self.team)
1671                                 SUB_UseTargets();
1672                 }
1673         }
1674         else
1675         {
1676                 if(self.spawnflags & 1)
1677                         SUB_UseTargets();
1678         }
1679 }
1680
1681 void trigger_relay_teamcheck_reset()
1682 {
1683         self.team = self.team_saved;
1684 }
1685
1686 void spawnfunc_trigger_relay_teamcheck()
1687 {
1688         self.team_saved = self.team;
1689         self.use = trigger_relay_teamcheck_use;
1690         self.reset = trigger_relay_teamcheck_reset;
1691 }
1692
1693
1694
1695 void trigger_disablerelay_use()
1696 {
1697         entity e;
1698
1699         float a, b;
1700         a = b = 0;
1701
1702         for(e = world; (e = find(e, targetname, self.target)); )
1703         {
1704                 if(e.use == SUB_UseTargets)
1705                 {
1706                         e.use = SUB_DontUseTargets;
1707                         ++a;
1708                 }
1709                 else if(e.use == SUB_DontUseTargets)
1710                 {
1711                         e.use = SUB_UseTargets;
1712                         ++b;
1713                 }
1714         }
1715
1716         if((!a) == (!b))
1717                 print("Invalid use of trigger_disablerelay: ", ftos(a), " relays were on, ", ftos(b), " relays were off!\n");
1718 }
1719
1720 void spawnfunc_trigger_disablerelay()
1721 {
1722         self.use = trigger_disablerelay_use;
1723 }
1724
1725 float magicear_matched;
1726 string trigger_magicear_processmessage(entity ear, entity source, float teamsay, entity privatesay, string msgin)
1727 {
1728         float domatch, dotrigger, matchstart, l;
1729         string s, msg;
1730         entity oldself;
1731
1732         magicear_matched = FALSE;
1733
1734         dotrigger = ((self.classname == "player") && (self.deadflag == DEAD_NO) && ((ear.radius == 0) || (vlen(source.origin - ear.origin) <= ear.radius)));
1735         domatch = ((ear.spawnflags & 32) || dotrigger);
1736         if not(domatch)
1737                 return msgin;
1738
1739         if(privatesay)
1740         {
1741                 if(ear.spawnflags & 4)
1742                         return msgin;
1743         }
1744         else
1745         {
1746                 if(!teamsay)
1747                         if(ear.spawnflags & 1)
1748                                 return msgin;
1749                 if(teamsay > 0)
1750                         if(ear.spawnflags & 2)
1751                                 return msgin;
1752                 if(teamsay < 0)
1753                         if(ear.spawnflags & 8)
1754                                 return msgin;
1755         }
1756         
1757         matchstart = -1;
1758         l = strlen(ear.message);
1759
1760         if(self.spawnflags & 128)
1761                 msg = msgin;
1762         else
1763                 msg = strdecolorize(msgin);
1764
1765         if(substring(ear.message, 0, 1) == "*")
1766         {
1767                 if(substring(ear.message, -1, 1) == "*")
1768                 {
1769                         // two wildcards
1770                         // as we need multi-replacement here...
1771                         s = substring(ear.message, 1, -2);
1772                         l -= 2;
1773                         if(strstrofs(msg, s, 0) >= 0)
1774                                 matchstart = -2; // we use strreplace on s
1775                 }
1776                 else
1777                 {
1778                         // match at start
1779                         s = substring(ear.message, 1, -1);
1780                         l -= 1;
1781                         if(substring(msg, -l, l) == s)
1782                                 matchstart = strlen(msg) - l;
1783                 }
1784         }
1785         else
1786         {
1787                 if(substring(ear.message, -1, 1) == "*")
1788                 {
1789                         // match at end
1790                         s = substring(ear.message, 0, -2);
1791                         l -= 1;
1792                         if(substring(msg, 0, l) == s)
1793                                 matchstart = 0;
1794                 }
1795                 else
1796                 {
1797                         // full match
1798                         s = ear.message;
1799                         if(msg == ear.message)
1800                                 matchstart = 0;
1801                 }
1802         }
1803
1804         if(matchstart == -1) // no match
1805                 return msgin;
1806
1807         magicear_matched = TRUE;
1808
1809         if(dotrigger)
1810         {
1811                 oldself = activator = self;
1812                 self = ear;
1813                 SUB_UseTargets();
1814                 self = oldself;
1815         }
1816
1817         if(ear.spawnflags & 16)
1818         {
1819                 return ear.netname;
1820         }
1821         else if(ear.netname != "")
1822         {
1823                 if(matchstart < 0)
1824                         return strreplace(s, ear.netname, msg);
1825                 else
1826                         return strcat(
1827                                 substring(msg, 0, matchstart),
1828                                 ear.netname,
1829                                 substring(msg, matchstart + l, -1)
1830                         );
1831         }
1832         else
1833                 return msgin;
1834 }
1835
1836 entity magicears;
1837 string trigger_magicear_processmessage_forallears(entity source, float teamsay, entity privatesay, string msgin)
1838 {
1839         entity ear;
1840         string msgout;
1841         for(ear = magicears; ear; ear = ear.enemy)
1842         {
1843                 msgout = trigger_magicear_processmessage(ear, source, teamsay, privatesay, msgin);
1844                 if not(ear.spawnflags & 64)
1845                         if(magicear_matched)
1846                                 return msgout;
1847                 msgin = msgout;
1848         }
1849         return msgin;
1850 }
1851
1852 void spawnfunc_trigger_magicear()
1853 {
1854         self.enemy = magicears;
1855         magicears = self;
1856
1857         // actually handled in "say" processing
1858         // spawnflags:
1859         //   1 = ignore say
1860         //   2 = ignore teamsay
1861         //   4 = ignore tell
1862         //   8 = ignore tell to unknown player
1863         //   16 = let netname replace the whole message (otherwise, netname is a word replacement if set)
1864         //   32 = perform the replacement even if outside the radius or dead
1865         //   64 = continue replacing/triggering even if this one matched
1866         // message: either
1867         //   *pattern*
1868         // or
1869         //   *pattern
1870         // or
1871         //   pattern*
1872         // or
1873         //   pattern
1874         // netname:
1875         //   if set, replacement for the matched text
1876         // radius:
1877         //   "hearing distance"
1878         // target:
1879         //   what to trigger
1880 }