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