]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/race.qc
experimental race penalty time system (entity fields race_penalty and race_penalty_re...
[divverent/nexuiz.git] / data / qcsrc / server / race.qc
1 #define MAX_CHECKPOINTS 255
2
3 .float race_penalty;
4 .float race_penalty_accumulator;
5 .string race_penalty_reason;
6 .float race_checkpoint; // player: next checkpoint that has to be reached
7 .float race_laptime;
8 .entity race_lastpenalty;
9
10 .entity sprite;
11
12 float race_checkpoint_records[MAX_CHECKPOINTS];
13 string race_checkpoint_recordholders[MAX_CHECKPOINTS];
14 float race_checkpoint_lasttimes[MAX_CHECKPOINTS];
15 float race_checkpoint_lastlaps[MAX_CHECKPOINTS];
16 entity race_checkpoint_lastplayers[MAX_CHECKPOINTS];
17
18 float race_highest_checkpoint;
19 float race_timed_checkpoint;
20
21 float race_NextCheckpoint(float f)
22 {
23         if(f >= race_highest_checkpoint)
24                 return 0;
25         else
26                 return f + 1;
27 }
28
29 float race_PreviousCheckpoint(float f)
30 {
31         if(f == -1)
32                 return 0;
33         else if(f == 0)
34                 return race_highest_checkpoint;
35         else
36                 return f - 1;
37 }
38
39 // encode as:
40 //   0 = common start/finish
41 // 254 = start
42 // 255 = finish
43 float race_CheckpointNetworkID(float f)
44 {
45         if(race_timed_checkpoint)
46         {
47                 if(f == 0)
48                         return 254; // start
49                 else if(f == race_timed_checkpoint)
50                         return 255; // finish
51         }
52         return f;
53 }
54
55 void race_SendNextCheckpoint(entity e, float spec)
56 {
57         float recordtime;
58         string recordholder;
59         float cp;
60
61         if(!e.race_laptime)
62                 return;
63
64         cp = e.race_checkpoint;
65         recordtime = race_checkpoint_records[cp];
66         recordholder = race_checkpoint_recordholders[cp];
67         /*
68         recordtime = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/racerecord/", ftos(cp), "/time")));
69         recordholder = db_get(ServerProgsDB, strcat(GetMapname(), "/racerecord/", ftos(cp), "/netname"));
70         */
71         if(recordholder == e.netname)
72                 recordholder = "";
73
74         if(!spec)
75                 msg_entity = e;
76         WRITESPECTATABLE_MSG_ONE({
77                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
78                 WriteByte(MSG_ONE, TE_CSQC_RACE);
79                 if(spec)
80                 {
81                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_NEXT_SPEC_QUALIFYING);
82                         WriteCoord(MSG_ONE, e.race_laptime - e.race_penalty_accumulator);
83                 }
84                 else
85                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_NEXT_QUALIFYING);
86                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player will be at next
87                 WriteShort(MSG_ONE, recordtime);
88                 WriteString(MSG_ONE, recordholder);
89         });
90 }
91
92 void race_InitSpectator()
93 {
94         if(g_race_qualifying)
95                 if(msg_entity.enemy.race_laptime)
96                         race_SendNextCheckpoint(msg_entity.enemy, 1);
97 }
98
99 void race_SendTime(entity e, float cp, float t, float tvalid)
100 {
101         float snew, l;
102         entity p;
103
104         if(g_race_qualifying)
105                 t += e.race_penalty_accumulator;
106
107         t = floor(0.4 + 10 * t); // make integer
108         // adding just 0.4 so it rounds down in the .5 case (matching the timer display)
109
110         if(tvalid)
111         if(cp == race_timed_checkpoint) // finish line
112         if not(e.race_completed)
113         {
114                 float s;
115                 if(g_race_qualifying)
116                 {
117                         s = PlayerScore_Add(e, SP_RACE_FASTEST, 0);
118                         if(!s || t < s)
119                                 PlayerScore_Add(e, SP_RACE_FASTEST, t - s);
120                 }
121                 else
122                 {
123                         s = PlayerScore_Add(e, SP_RACE_TIME, 0);
124                         snew = floor(0.4 + 10 * (time - game_starttime));
125                         PlayerScore_Add(e, SP_RACE_TIME, snew - s);
126                         l = PlayerTeamScore_Add(e, SP_RACE_LAPS, ST_RACE_LAPS, 1);
127
128                         if(cvar("fraglimit"))
129                                 if(l >= cvar("fraglimit"))
130                                 {
131                                         race_completing = 1;
132                                         FOR_EACH_PLAYER(p)
133                                                 if(p.deadflag != DEAD_NO)
134                                                         race_AbandonRaceCheck(p);
135                                 }
136
137                         if(race_completing)
138                         {
139                                 e.race_completed = 1;
140                                 MAKE_INDEPENDENT_PLAYER(e);
141                                 bprint(e.netname, "^7 has finished the race.\n");
142                                 ClientData_Touch(e);
143                         }
144                 }
145         }
146
147         float recordtime;
148         string recordholder;
149
150         if(g_race_qualifying)
151         {
152                 if(tvalid)
153                 {
154                         recordtime = race_checkpoint_records[cp];
155                         recordholder = strcat1(race_checkpoint_recordholders[cp]); // make a tempstring copy, as we'll possibly strunzone it!
156                         if(recordholder == e.netname)
157                                 recordholder = "";
158
159                         if(t != 0)
160                         if(t < recordtime || recordtime == 0)
161                         {
162                                 race_checkpoint_records[cp] = t;
163                                 if(race_checkpoint_recordholders[cp])
164                                         strunzone(race_checkpoint_recordholders[cp]);
165                                 race_checkpoint_recordholders[cp] = strzone(e.netname);
166                                 if(cp == race_timed_checkpoint)
167                                 {
168                                         float grecordtime;
169                                         string grecordholder;
170                                         grecordtime = stof(db_get(ServerProgsDB, strcat(GetMapname(), "/racerecord/time")));
171                                         grecordholder = db_get(ServerProgsDB, strcat(GetMapname(), "/racerecord/netname"));
172                                         if(grecordholder == e.netname)
173                                                 grecordholder = "";
174                                         if(grecordtime == 0)
175                                         {
176                                                 bprint(e.netname, "^7 set the all-time fastest lap record with ", mmsss(t), "\n");
177                                                 db_put(ServerProgsDB, strcat(GetMapname(), "/racerecord/time"), ftos(t));
178                                                 db_put(ServerProgsDB, strcat(GetMapname(), "/racerecord/netname"), e.netname);
179                                                 write_recordmarker(e, time - t/10, t/10);
180                                         }
181                                         else if(t < grecordtime)
182                                         {
183                                                 if(grecordholder == "")
184                                                         bprint(e.netname, "^7 broke his all-time fastest lap record with ", mmsss(t), "\n");
185                                                 else
186                                                         bprint(e.netname, "^7 broke ", grecordholder, "^7's all-time fastest lap record with ", mmsss(t), "\n");
187                                                 db_put(ServerProgsDB, strcat(GetMapname(), "/racerecord/time"), ftos(t));
188                                                 db_put(ServerProgsDB, strcat(GetMapname(), "/racerecord/netname"), e.netname);
189                                                 write_recordmarker(e, time - t/10, t/10);
190                                         }
191                                         else
192                                         {
193                                                 if(grecordholder == "")
194                                                         bprint(e.netname, "^7's new fastest lap could not break his all-time fastest lap record of ", mmsss(grecordtime), "\n");
195                                                 else
196                                                         bprint(e.netname, "^7's new fastest lap could not break ", grecordholder, "^7's all-time fastest lap record of ", mmsss(grecordtime), "\n");
197                                         }
198                                 }
199
200                                 if(g_race_qualifying)
201                                 {
202                                         FOR_EACH_REALPLAYER(p)
203                                                 if(p.race_checkpoint == cp)
204                                                         race_SendNextCheckpoint(p, 0);
205                                 }
206                         }
207                 }
208                 else
209                 {
210                         // dummies
211                         t = 0;
212                         recordtime = 0;
213                         recordholder = "";
214                 }
215
216                 msg_entity = e;
217                 if(g_race_qualifying)
218                 {
219                         WRITESPECTATABLE_MSG_ONE_VARNAME(dummy1, {
220                                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
221                                 WriteByte(MSG_ONE, TE_CSQC_RACE);
222                                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_QUALIFYING);
223                                 WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
224                                 WriteShort(MSG_ONE, t); // time to that intermediate
225                                 WriteShort(MSG_ONE, recordtime); // previously best time
226                                 WriteString(MSG_ONE, recordholder); // record holder
227                         });
228                 }
229         }
230         else // RACE! Not Qualifying
231         {
232                 float lself, lother, othtime;
233                 entity oth;
234                 oth = race_checkpoint_lastplayers[cp];
235                 if(oth)
236                 {
237                         lself = PlayerScore_Add(e, SP_RACE_LAPS, 0);
238                         lother = race_checkpoint_lastlaps[cp];
239                         othtime = race_checkpoint_lasttimes[cp];
240                 }
241                 else
242                         lself = lother = othtime = 0;
243
244                 msg_entity = e;
245                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy2, {
246                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
247                         WriteByte(MSG_ONE, TE_CSQC_RACE);
248                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE);
249                         WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
250                         if(e == oth)
251                         {
252                                 WriteShort(MSG_ONE, 0);
253                                 WriteByte(MSG_ONE, 0);
254                                 WriteString(MSG_ONE, "");
255                         }
256                         else
257                         {
258                                 WriteShort(MSG_ONE, floor(10 * (time - race_checkpoint_lasttimes[cp]) + 0.5));
259                                 WriteByte(MSG_ONE, lself - lother);
260                                 WriteString(MSG_ONE, oth.netname); // record holder
261                         }
262                 });
263
264                 race_checkpoint_lastplayers[cp] = e;
265                 race_checkpoint_lasttimes[cp] = time;
266                 race_checkpoint_lastlaps[cp] = lself;
267
268                 msg_entity = oth;
269                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy3, {
270                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
271                         WriteByte(MSG_ONE, TE_CSQC_RACE);
272                         WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_HIT_RACE_BY_OPPONENT);
273                         WriteByte(MSG_ONE, race_CheckpointNetworkID(cp)); // checkpoint the player now is at
274                         if(e == oth)
275                         {
276                                 WriteShort(MSG_ONE, 0);
277                                 WriteByte(MSG_ONE, 0);
278                                 WriteString(MSG_ONE, "");
279                         }
280                         else
281                         {
282                                 WriteShort(MSG_ONE, floor(10 * (time - othtime) + 0.5));
283                                 WriteByte(MSG_ONE, lother - lself);
284                                 WriteString(MSG_ONE, e.netname); // record holder
285                         }
286                 });
287         }
288 }
289
290 void race_ClearTime(entity e)
291 {
292         e.race_checkpoint = -1;
293         e.race_laptime = 0;
294         e.race_penalty_accumulator = 0;
295         e.race_lastpenalty = world;
296
297         msg_entity = e;
298         WRITESPECTATABLE_MSG_ONE({
299                 WriteByte(MSG_ONE, SVC_TEMPENTITY);
300                 WriteByte(MSG_ONE, TE_CSQC_RACE);
301                 WriteByte(MSG_ONE, RACE_NET_CHECKPOINT_CLEAR); // next
302         });
303 }
304
305 void dumpsurface(entity e)
306 {
307         float n, si, ni;
308         vector norm, vec;
309         print("Surfaces of ", etos(e), ":\n");
310
311         print("TEST = ", ftos(getsurfacenearpoint(e, '0 0 0')), "\n");
312
313         for(si = 0; ; ++si)
314         {
315                 n = getsurfacenumpoints(e, si);
316                 if(n <= 0)
317                         break;
318                 print("  Surface ", ftos(si), ":\n");
319                 norm = getsurfacenormal(e, si);
320                 print("    Normal = ", vtos(norm), "\n");
321                 for(ni = 0; ni < n; ++ni)
322                 {
323                         vec = getsurfacepoint(e, si, ni);
324                         print("    Point ", ftos(ni), " = ", vtos(vec), " (", ftos(norm * vec), ")\n");
325                 }
326         }
327 }
328
329 void checkpoint_passed()
330 {
331         string oldmsg;
332
333         if(other.classname == "porto")
334         {
335                 // do not allow portalling through checkpoints
336                 trace_plane_normal = normalize(-1 * other.velocity);
337                 self = other;
338                 W_Porto_Fail(0);
339                 return;
340         }
341
342         /*
343          * Trigger targets
344          */
345         if not(self.spawnflags & 2)
346         {
347                 activator = other;
348                 oldmsg = self.message;
349                 self.message = "";
350                 SUB_UseTargets();
351                 self.message = oldmsg;
352         }
353
354         if(other.classname != "player")
355                 return;
356
357         /*
358          * Remove unauthorized equipment
359          */
360         Portal_ClearAll(other);
361
362         other.porto_forbidden = 2; // decreased by 1 each StartFrame
363
364         if(other.race_checkpoint == -1 || other.race_checkpoint == self.race_checkpoint)
365         {
366                 /*
367                  * Trigger targets
368                  */
369                 if(self.spawnflags & 2)
370                 {
371                         activator = other;
372                         oldmsg = self.message;
373                         self.message = "";
374                         SUB_UseTargets();
375                         self.message = oldmsg;
376                 }
377
378                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
379
380                 other.race_checkpoint = race_NextCheckpoint(self.race_checkpoint);
381
382                 race_SendTime(other, self.race_checkpoint, time - other.race_laptime, !!other.race_laptime);
383
384                 if(!self.race_checkpoint) // start line
385                 {
386                         other.race_laptime = time;
387                         other.race_penalty_accumulator = 0;
388                         other.race_lastpenalty = world;
389                 }
390
391                 if(g_race_qualifying)
392                         race_SendNextCheckpoint(other, 0);
393         }
394         else if(other.race_checkpoint == race_NextCheckpoint(self.race_checkpoint))
395         {
396                 // ignored
397         }
398         else
399         {
400                 if(self.spawnflags & 4)
401                         Damage (other, self, self, 10000, DEATH_HURTTRIGGER, other.origin, '0 0 0');
402         }
403 }
404
405 void checkpoint_touch()
406 {
407         EXACTTRIGGER_TOUCH;
408         checkpoint_passed();
409 }
410
411 void checkpoint_use()
412 {
413         if(other.classname == "info_player_deathmatch") // a spawn, a spawn
414                 return;
415
416         other = activator;
417         checkpoint_passed();
418 }
419
420 float race_waypointsprite_visible_for_player(entity e)
421 {
422         if(e.race_checkpoint == -1)
423                 return TRUE;
424         else if(e.race_checkpoint == self.owner.race_checkpoint)
425                 return TRUE;
426         else
427                 return FALSE;
428 }
429
430 float have_verified;
431 void trigger_race_checkpoint_verify()
432 {
433         entity oldself, cp;
434         float i, p;
435         float qual;
436
437         if(have_verified)
438                 return;
439         have_verified = 1;
440         
441         qual = g_race_qualifying;
442
443         oldself = self;
444         self = spawn();
445         self.classname = "player";
446
447         for(i = 0; i <= race_highest_checkpoint; ++i)
448         {
449                 self.race_checkpoint = race_NextCheckpoint(i);
450
451                 // race only (middle of the race)
452                 g_race_qualifying = 0;
453                 self.race_place = 0;
454                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE))
455                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for respawning in race) - bailing out"));
456
457                 if(i == 0)
458                 {
459                         // qualifying only
460                         g_race_qualifying = 1;
461                         self.race_place = race_lowest_place_spawn;
462                         if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE))
463                                 error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for qualifying) - bailing out"));
464                         
465                         // race only (initial spawn)
466                         g_race_qualifying = 0;
467                         for(p = 1; p <= race_highest_place_spawn; ++p)
468                         {
469                                 self.race_place = p;
470                                 if(!Spawn_FilterOutBadSpots(findchain(classname, "info_player_deathmatch"), world, 0, FALSE))
471                                         error(strcat("Checkpoint ", ftos(i), " misses a spawnpoint with race_place==", ftos(self.race_place), " (used for initially spawning in race) - bailing out"));
472                         }
473                 }
474         }
475
476         g_race_qualifying = qual;
477
478         if(race_timed_checkpoint)
479                 for(cp = world; (cp = find(cp, classname, "trigger_race_checkpoint")); )
480                         if(cp.sprite)
481                         {
482                                 if(cp.race_checkpoint == 0)
483                                         WaypointSprite_UpdateSprites(cp.sprite, "race-start", "", "");
484                                 else if(cp.race_checkpoint == race_timed_checkpoint)
485                                         WaypointSprite_UpdateSprites(cp.sprite, "race-finish", "", "");
486                         }
487
488         remove(self);
489         self = oldself;
490 }
491
492 void spawnfunc_trigger_race_checkpoint()
493 {
494         vector o;
495         if(!g_race)
496         {
497                 remove(self);
498                 return;
499         }
500
501         EXACTTRIGGER_INIT;
502
503         self.use = checkpoint_use;
504         if not(self.spawnflags & 1)
505                 self.touch = checkpoint_touch;
506
507         o = (self.absmin + self.absmax) * 0.5;
508         tracebox(o, PL_MIN, PL_MAX, o - '0 0 1' * (o_z - self.absmin_z), MOVE_NORMAL, self);
509         self.nearestwaypoint = waypoint_spawn(trace_endpos, trace_endpos, WAYPOINTFLAG_GENERATED);
510         self.nearestwaypointtimeout = time + 1000000000;
511
512         if(!self.message)
513                 self.message = "went backwards";
514         if (!self.message2)
515                 self.message2 = "was pushed backwards by";
516         if (!self.race_penalty_reason)
517                 self.race_penalty_reason = "missing a checkpoint";
518         
519         self.race_checkpoint = self.cnt;
520
521         if(self.race_checkpoint > race_highest_checkpoint)
522         {
523                 race_highest_checkpoint = self.race_checkpoint;
524                 if(self.spawnflags & 8)
525                         race_timed_checkpoint = self.race_checkpoint;
526                 else
527                         race_timed_checkpoint = 0;
528         }
529
530         if(!self.race_penalty)
531         {
532                 if(self.race_checkpoint)
533                         WaypointSprite_SpawnFixed("race-checkpoint", o, self, sprite);
534                 else
535                         WaypointSprite_SpawnFixed("race-finish", o, self, sprite);
536         }
537
538         self.sprite.waypointsprite_visible_for_player = race_waypointsprite_visible_for_player;
539
540         InitializeEntity(self, trigger_race_checkpoint_verify, INITPRIO_FINDTARGET);
541 }
542
543 void race_AbandonRaceCheck(entity p)
544 {
545         if(race_completing && !p.race_completed)
546         {
547                 p.race_completed = 1;
548                 MAKE_INDEPENDENT_PLAYER(p);
549                 bprint(p.netname, "^7 has abandoned the race.\n");
550                 ClientData_Touch(p);
551         }
552 }
553
554 void race_PreparePlayer()
555 {
556         race_ClearTime(self);
557         self.race_place = 0;
558 }
559
560 void race_RetractPlayer()
561 {
562         if(!g_race)
563                 return;
564         self.race_checkpoint = race_PreviousCheckpoint(self.race_checkpoint);
565         if(self.race_checkpoint == 0)
566         {
567                 race_ClearTime(self);
568                 self.race_checkpoint = 0;
569         }
570 }
571
572 void race_PreDie()
573 {
574         if(!g_race)
575                 return;
576
577         race_AbandonRaceCheck(self);
578 }
579
580 void race_PreSpawn()
581 {
582         if(!g_race)
583                 return;
584         if(self.killcount == -666 || g_race_qualifying)
585                 race_PreparePlayer();
586
587         race_AbandonRaceCheck(self);
588 }
589
590 void race_PostSpawn(entity spot)
591 {
592         if(!g_race)
593                 return;
594         if(self.killcount != -666 && !g_race_qualifying)
595         {
596                 if(spot.target == "")
597                         // let the player run without timing, if he did not spawn at a targetting spawnpoint
598                         race_PreparePlayer();
599                 else
600                         race_RetractPlayer();
601         }
602
603         if(spot.target != "" && self.race_checkpoint == -1)
604                 self.race_checkpoint = 0;
605
606         self.race_place = 0;
607 }
608
609 void race_PreSpawnObserver()
610 {
611         if(!g_race)
612                 return;
613         race_PreparePlayer();
614 }
615
616 void spawnfunc_info_player_race (void)
617 {
618         if(!g_race)
619         {
620                 remove(self);
621                 return;
622         }
623         ++race_spawns;
624         spawnfunc_info_player_deathmatch();
625
626         if(self.race_place > race_highest_place_spawn)
627                 race_highest_place_spawn = self.race_place;
628         if(self.race_place < race_lowest_place_spawn)
629                 race_lowest_place_spawn = self.race_place;
630 }
631
632 void race_ClearRecords()
633 {
634         float i;
635         entity e;
636
637         for(i = 0; i < MAX_CHECKPOINTS; ++i)
638         {
639                 race_checkpoint_records[i] = 0;
640                 if(race_checkpoint_recordholders[i])
641                         strunzone(race_checkpoint_recordholders[i]);
642                 race_checkpoint_recordholders[i] = string_null;
643         }
644
645         FOR_EACH_CLIENT(e)
646                 race_ClearTime(e);
647 }
648
649 void race_ReadyRestart()
650 {
651         Score_NicePrint(world);
652
653         race_ClearRecords();
654         PlayerScore_Sort(race_place);
655
656         entity e;
657         FOR_EACH_CLIENT(e)
658                 print(e.netname, " = ", ftos(e.race_place), "\n");
659
660         if(g_race_qualifying == 2)
661         {
662                 g_race_qualifying = 0;
663                 independent_players = 0;
664                 cvar_set("fraglimit", ftos(race_fraglimit));
665                 cvar_set("leadlimit", ftos(race_leadlimit));
666                 ScoreRules_race();
667         }
668 }
669
670 void race_ImposePenaltyTime(entity pl, float penalty, string reason)
671 {
672         if(g_race_qualifying)
673         {
674                 pl.race_penalty_accumulator += penalty;
675                 msg_entity = pl;
676                 WRITESPECTATABLE_MSG_ONE({
677                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
678                         WriteByte(MSG_ONE, TE_CSQC_RACE);
679                         WriteByte(MSG_ONE, RACE_NET_PENALTY_QUALIFYING);
680                         WriteByte(MSG_ONE, floor(0.4 + 10 * penalty));
681                         WriteString(MSG_ONE, reason);
682                 });
683         }
684         else
685         {
686                 pl.race_penalty = time + penalty;
687                 msg_entity = pl;
688                 WRITESPECTATABLE_MSG_ONE_VARNAME(dummy, {
689                         WriteByte(MSG_ONE, SVC_TEMPENTITY);
690                         WriteByte(MSG_ONE, TE_CSQC_RACE);
691                         WriteByte(MSG_ONE, RACE_NET_PENALTY_RACE);
692                         WriteByte(MSG_ONE, floor(0.4 + 10 * penalty));
693                         WriteString(MSG_ONE, reason);
694                 });
695         }
696 }
697
698 void penalty_touch()
699 {
700         EXACTTRIGGER_TOUCH;
701         race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
702 }
703
704 void penalty_use()
705 {
706         other = activator;
707         if(other.race_lastpenalty != self)
708         {
709                 other.race_lastpenalty = self;
710                 race_ImposePenaltyTime(other, self.race_penalty, self.race_penalty_reason);
711         }
712 }
713
714 void spawnfunc_trigger_race_penalty()
715 {
716         EXACTTRIGGER_INIT;
717
718         self.use = penalty_use;
719         if not(self.spawnflags & 1)
720                 self.touch = penalty_touch;
721
722         if (!self.race_penalty_reason)
723                 self.race_penalty_reason = "missing a checkpoint";
724         if (!self.race_penalty)
725                 self.race_penalty = 5;
726 }