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