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