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