]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/server/cl_player.qc
pinata
[divverent/nexuiz.git] / data / qcsrc / server / cl_player.qc
1
2 // changes by LordHavoc on 03/29/04 and 03/30/04 at Vermeulen's request
3 // merged player_run and player_stand to player_anim
4 // added death animations to player_anim
5 // can now spawn thrown weapons from anywhere, not just from players
6 // thrown weapons now fade out after 20 seconds
7 // created PlayerGib function
8 // PlayerDie no longer uses hitloc or damage
9 // PlayerDie now supports dying animations as well as gibbing
10 // cleaned up PlayerDie a lot
11 // added CopyBody
12
13 .entity pusher;
14 .float pushltime;
15
16 void CopyBody(float keepvelocity)
17 {
18         local entity oldself;
19         if (self.effects & EF_NODRAW)
20                 return;
21         oldself = self;
22         self = spawn();
23         self.enemy = oldself;
24         self.lip = oldself.lip;
25         self.colormap = oldself.colormap;
26         self.iscreature = oldself.iscreature;
27         self.angles = oldself.angles;
28         self.avelocity = oldself.avelocity;
29         self.classname = "body";
30         self.damageforcescale = oldself.damageforcescale;
31         self.effects = oldself.effects;
32         self.event_damage = oldself.event_damage;
33         self.animstate_startframe = oldself.animstate_startframe;
34         self.animstate_numframes = oldself.animstate_numframes;
35         self.animstate_framerate = oldself.animstate_framerate;
36         self.animstate_starttime = oldself.animstate_starttime;
37         self.animstate_endtime = oldself.animstate_endtime;
38         self.animstate_override = oldself.animstate_override;
39         self.animstate_looping = oldself.animstate_looping;
40         self.frame = oldself.frame;
41         self.dead_frame = oldself.dead_frame;
42         self.pain_finished = oldself.pain_finished;
43         self.health = oldself.health;
44         self.armorvalue = oldself.armorvalue;
45         self.armortype = oldself.armortype;
46         self.model = oldself.model;
47         self.modelindex = oldself.modelindex;
48         self.movetype = oldself.movetype;
49         self.nextthink = oldself.nextthink;
50         self.skin = oldself.skin;
51         self.solid = oldself.solid;
52         self.takedamage = oldself.takedamage;
53         self.think = oldself.think;
54         self.customizeentityforclient = oldself.customizeentityforclient;
55         if (keepvelocity == 1)
56                 self.velocity = oldself.velocity;
57         self.oldvelocity = self.velocity;
58         self.fade_time = oldself.fade_time;
59         self.fade_rate = oldself.fade_rate;
60         //self.weapon = oldself.weapon;
61         setorigin(self, oldself.origin);
62         setsize(self, oldself.mins, oldself.maxs);
63         self.oldorigin = oldself.origin;
64         self.reset = SUB_Remove;
65         self = oldself;
66 }
67
68 float animparseerror;
69 vector animparseline(float animfile)
70 {
71         local string line;
72         local float c;
73         local vector anim;
74         if (animfile < 0)
75                 return '0 1 2';
76         line = fgets(animfile);
77         c = tokenize_sane(line);
78         if (c != 3)
79         {
80                 animparseerror = TRUE;
81                 return '0 1 2';
82         }
83         anim_x = stof(argv(0));
84         anim_y = stof(argv(1));
85         anim_z = stof(argv(2));
86         // don't allow completely bogus values
87         if (anim_x < 0 || anim_y < 1 || anim_z < 0.001)
88                 anim = '0 1 2';
89         return anim;
90 };
91
92 void player_setupanimsformodel()
93 {
94         local string animfilename;
95         local float animfile;
96         // defaults for legacy .zym models without animinfo files
97         self.anim_die1 = '0 1 0.5'; // 2 seconds
98         self.anim_die2 = '1 1 0.5'; // 2 seconds
99         self.anim_draw = '2 1 3'; // TODO: analyze models and set framerate
100         self.anim_duck = '3 1 100'; // this anim seems bogus in most models, so make it play VERY briefly!
101         self.anim_duckwalk = '4 1 1';
102         self.anim_duckjump = '5 1 100'; // zym anims keep playing until changed, so this only has to start the anim, landing will end it
103         self.anim_duckidle = '6 1 1';
104         self.anim_idle = '7 1 1';
105         self.anim_jump = '8 1 100'; // zym anims keep playing until changed, so this only has to start the anim, landing will end it
106         self.anim_pain1 = '9 1 2'; // 0.5 seconds
107         self.anim_pain2 = '10 1 2'; // 0.5 seconds
108         self.anim_shoot = '11 1 5'; // TODO: analyze models and set framerate
109         self.anim_taunt = '12 1 1'; // FIXME?  there is no code using this anim
110         self.anim_run = '13 1 1';
111         self.anim_runbackwards = '14 1 1';
112         self.anim_strafeleft = '15 1 1';
113         self.anim_straferight = '16 1 1';
114         self.anim_dead1 = '17 1 1';
115         self.anim_dead2 = '18 1 1';
116         self.anim_forwardright = '19 1 1';
117         self.anim_forwardleft = '20 1 1';
118         self.anim_backright = '21 1 1';
119         self.anim_backleft  = '22 1 1';
120         animparseerror = FALSE;
121         animfilename = strcat(self.model, ".animinfo");
122         animfile = fopen(animfilename, FILE_READ);
123         if (animfile >= 0)
124         {
125                 self.anim_die1         = animparseline(animfile);
126                 self.anim_die2         = animparseline(animfile);
127                 self.anim_draw         = animparseline(animfile);
128                 self.anim_duck         = animparseline(animfile);
129                 self.anim_duckwalk     = animparseline(animfile);
130                 self.anim_duckjump     = animparseline(animfile);
131                 self.anim_duckidle     = animparseline(animfile);
132                 self.anim_idle         = animparseline(animfile);
133                 self.anim_jump         = animparseline(animfile);
134                 self.anim_pain1        = animparseline(animfile);
135                 self.anim_pain2        = animparseline(animfile);
136                 self.anim_shoot        = animparseline(animfile);
137                 self.anim_taunt        = animparseline(animfile);
138                 self.anim_run          = animparseline(animfile);
139                 self.anim_runbackwards = animparseline(animfile);
140                 self.anim_strafeleft   = animparseline(animfile);
141                 self.anim_straferight  = animparseline(animfile);
142                 self.anim_forwardright = animparseline(animfile);
143                 self.anim_forwardleft  = animparseline(animfile);
144                 self.anim_backright    = animparseline(animfile);
145                 self.anim_backleft     = animparseline(animfile);
146                 fclose(animfile);
147
148                 // derived anims
149                 self.anim_dead1 = '0 1 1' + '1 0 0' * (self.anim_die1_x + self.anim_die1_y - 1);
150                 self.anim_dead2 = '0 1 1' + '1 0 0' * (self.anim_die2_x + self.anim_die2_y - 1);
151
152                 if (animparseerror)
153                         print("Parse error in ", animfilename, ", some player animations are broken\n");
154         }
155         else
156                 dprint("File ", animfilename, " not found, assuming legacy .zym model animation timings\n");
157         // reset animstate now
158         player_setanim(self.anim_idle, TRUE, FALSE, TRUE);
159 };
160
161 void player_setanim(vector anim, float looping, float override, float restart)
162 {
163         if (!restart)
164         if (anim_x == self.animstate_startframe)
165         if (anim_y == self.animstate_numframes)
166         if (anim_z == self.animstate_framerate)
167                 return;
168         self.animstate_startframe = anim_x;
169         self.animstate_numframes = anim_y;
170         self.animstate_framerate = anim_z;
171         self.animstate_starttime = time;
172         self.animstate_endtime = time + self.animstate_numframes / self.animstate_framerate;
173         self.animstate_looping = looping;
174         self.animstate_override = override;
175         self.frame = self.animstate_startframe;
176 };
177
178 void player_updateframe()
179 {
180         if (time >= self.animstate_endtime)
181         {
182                 if (self.animstate_looping)
183                 {
184                         self.animstate_starttime = self.animstate_endtime;
185                         self.animstate_endtime = self.animstate_endtime + self.animstate_numframes / self.animstate_framerate;
186                 }
187                 self.animstate_override = FALSE;
188         }
189         self.frame = self.animstate_startframe + bound(0, (time - self.animstate_starttime) * self.animstate_framerate, self.animstate_numframes - 1);
190 };
191
192 void player_anim (void)
193 {
194         player_updateframe();
195
196         if (self.deadflag != DEAD_NO)
197         {
198                 if (time > self.animstate_endtime)
199                 {
200                         if (self.maxs_z > 5)
201                         {
202                                 self.maxs_z = 5;
203                                 setsize(self, self.mins, self.maxs);
204                         }
205                         self.frame = self.dead_frame;
206                 }
207                 return;
208         }
209
210         if (!self.animstate_override)
211         {
212                 if (!(self.flags & FL_ONGROUND))
213                 {
214                         if (self.crouch)
215                                 player_setanim(self.anim_duckjump, FALSE, TRUE, FALSE);
216                         else
217                                 player_setanim(self.anim_jump, FALSE, TRUE, FALSE);
218                 }
219                 else if (self.crouch)
220                 {
221                         if (self.movement_x * self.movement_x + self.movement_y * self.movement_y > 20)
222                                 player_setanim(self.anim_duckwalk, TRUE, FALSE, FALSE);
223                         else
224                                 player_setanim(self.anim_duckidle, TRUE, FALSE, FALSE);
225                 }
226                 else if ((self.movement_x * self.movement_x + self.movement_y * self.movement_y) > 20)
227                 {
228                         if (self.movement_x > 0 && self.movement_y == 0)
229                                 player_setanim(self.anim_run, TRUE, FALSE, FALSE);
230                         else if (self.movement_x < 0 && self.movement_y == 0)
231                                 player_setanim(self.anim_runbackwards, TRUE, FALSE, FALSE);
232                         else if (self.movement_x == 0 && self.movement_y > 0)
233                                 player_setanim(self.anim_straferight, TRUE, FALSE, FALSE);
234                         else if (self.movement_x == 0 && self.movement_y < 0)
235                                 player_setanim(self.anim_strafeleft, TRUE, FALSE, FALSE);
236                         else if (self.movement_x > 0 && self.movement_y > 0)
237                                 player_setanim(self.anim_forwardright, TRUE, FALSE, FALSE);
238                         else if (self.movement_x > 0 && self.movement_y < 0)
239                                 player_setanim(self.anim_forwardleft, TRUE, FALSE, FALSE);
240                         else if (self.movement_x < 0 && self.movement_y > 0)
241                                 player_setanim(self.anim_backright, TRUE, FALSE, FALSE);
242                         else if (self.movement_x < 0 && self.movement_y < 0)
243                                 player_setanim(self.anim_backleft, TRUE, FALSE, FALSE);
244                         else
245                                 player_setanim(self.anim_run, TRUE, FALSE, FALSE);
246                 }
247                 else
248                         player_setanim(self.anim_idle, TRUE, FALSE, FALSE);
249         }
250 }
251 //End change by Supajoe on 11:44 PM EST 11/16/03 (Subject: Player animations)
252
253 void SpawnThrownWeapon (vector org, float w)
254 {
255         if(g_pinata)
256         {
257                 float j;
258                 for(j = WEP_FIRST; j <= WEP_LAST; ++j)
259                 {
260                         if(self.weapons == self.weapons | W_WeaponBit(j))
261                                 W_ThrowNewWeapon(self, j, FALSE, self.origin, randomvec() * 175 + '0 0 325');
262                 }
263         }
264         else
265                 W_ThrowWeapon(randomvec() * 125 + '0 0 200', org - self.origin, FALSE);
266 }
267
268 void PlayerCorpseDamage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
269 {
270         local float take, save;
271         Violence_GibSplash_At(hitloc, '0 0 0', '0 0 0', force, 2, bound(0, damage, 200) / 16);
272
273         // damage resistance (ignore most of the damage from a bullet or similar)
274         damage = max(damage - 5, 1);
275
276         save = bound(0, damage * cvar("g_balance_armor_blockpercent"), self.armorvalue);
277         take = bound(0, damage - save, damage);
278
279         if (save > 10)
280                 sound (self, CHAN_PROJECTILE, "misc/armorimpact.wav", VOL_BASE, ATTN_NORM);
281         else if (take > 30)
282                 sound (self, CHAN_PROJECTILE, "misc/bodyimpact2.wav", VOL_BASE, ATTN_NORM);
283         else if (take > 10)
284                 sound (self, CHAN_PROJECTILE, "misc/bodyimpact1.wav", VOL_BASE, ATTN_NORM);
285
286         if (take > 50)
287                 Violence_GibSplash_At(hitloc, '0 0 0', '0 0 0', force * -0.1, 3, 1);
288         if (take > 100)
289                 Violence_GibSplash_At(hitloc, '0 0 0', '0 0 0', force * -0.2, 3, 1);
290
291         if (!(self.flags & FL_GODMODE))
292         {
293                 self.armorvalue = self.armorvalue - save;
294                 self.health = self.health - take;
295                 // pause regeneration for 5 seconds
296                 self.pauseregen_finished = max(self.pauseregen_finished, time + cvar("g_balance_pause_health_regen"));
297         }
298         self.dmg_save = self.dmg_save + save;//max(save - 10, 0);
299         self.dmg_take = self.dmg_take + take;//max(take - 10, 0);
300         self.dmg_inflictor = inflictor;
301
302         if (self.health <= -75 && self.modelindex != 0)
303         {
304                 // don't use any animations as a gib
305                 self.frame = 0;
306                 self.dead_frame = 0;
307                 // view just above the floor
308                 self.view_ofs = '0 0 4';
309
310                 Violence_GibSplash(self, 1, 1);
311                 self.modelindex = 0; // restore later
312                 self.solid = SOLID_NOT; // restore later
313         }
314 }
315
316 void ClientKill_Now_TeamChange();
317
318 void PlayerDamage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
319 {
320         local float take, save, waves, sdelay;
321
322         if(!DEATH_ISSPECIAL(deathtype))
323         {
324                 damage *= sqrt(bound(1.0, self.cvar_cl_handicap, 100.0));
325                 if(self != attacker)
326                         damage /= sqrt(bound(1.0, attacker.cvar_cl_handicap, 100.0));
327         }
328
329         Violence_GibSplash_At(hitloc, '0 0 0', '0 0 0', force, 2, bound(0, damage, 200) / 16);
330
331         if(g_arena)
332         if(numspawned < 2)
333                 return;
334
335         if (!g_minstagib)
336         {
337                 save = bound(0, damage * cvar("g_balance_armor_blockpercent"), self.armorvalue);
338                 take = bound(0, damage - save, damage);
339         }
340         else
341         {
342                 save = 0;
343                 take = damage;
344         }
345
346         if (save > 10)
347                 sound (self, CHAN_PROJECTILE, "misc/armorimpact.wav", VOL_BASE, ATTN_NORM);
348         else if (take > 30)
349                 sound (self, CHAN_PROJECTILE, "misc/bodyimpact2.wav", VOL_BASE, ATTN_NORM);
350         else if (take > 10)
351                 sound (self, CHAN_PROJECTILE, "misc/bodyimpact1.wav", VOL_BASE, ATTN_NORM); // FIXME possibly remove them?
352
353         if (take > 50)
354                 Violence_GibSplash_At(hitloc, '0 0 0', '0 0 0', force * -0.1, 3, 1);
355         if (take > 100)
356                 Violence_GibSplash_At(hitloc, '0 0 0', '0 0 0', force * -0.2, 3, 1);
357
358         if (time > self.spawnshieldtime)
359         {
360                 if (!(self.flags & FL_GODMODE))
361                 {
362                         self.armorvalue = self.armorvalue - save;
363                         self.health = self.health - take;
364                         // pause regeneration for 5 seconds
365                         self.pauseregen_finished = max(self.pauseregen_finished, time + cvar("g_balance_pause_health_regen"));
366
367                         if (time > self.pain_finished)          //Don't switch pain sequences like crazy
368                         {
369                                 self.pain_finished = time + 0.5;        //Supajoe
370
371                                 if(sv_gentle < 1) {             
372                                         if (random() > 0.5)
373                                                 player_setanim(self.anim_pain1, FALSE, TRUE, TRUE);
374                                         else
375                                                 player_setanim(self.anim_pain2, FALSE, TRUE, TRUE);
376
377                                         if(!DEATH_ISWEAPON(deathtype, WEP_LASER) || attacker != self || self.health < 2 * cvar("g_balance_laser_primary_damage") * cvar("g_balance_selfdamagepercent") + 1)
378                                         // exclude pain sounds for laserjumps as long as you aren't REALLY low on health and would die of the next two
379                                         {
380                                                 if(self.health > 75) // TODO make a "gentle" version?
381                                                         PlayerSound(playersound_pain100, CHAN_PAIN, VOICETYPE_PLAYERSOUND);
382                                                 else if(self.health > 50)
383                                                         PlayerSound(playersound_pain75, CHAN_PAIN, VOICETYPE_PLAYERSOUND);
384                                                 else if(self.health > 25)
385                                                         PlayerSound(playersound_pain50, CHAN_PAIN, VOICETYPE_PLAYERSOUND);
386                                                 else if(self.health > 1)
387                                                         PlayerSound(playersound_pain25, CHAN_PAIN, VOICETYPE_PLAYERSOUND);
388                                         }
389                                 }
390
391                                 // throw off bot aim temporarily
392                                 local float shake;
393                                 shake = damage * 5 / (bound(0,skill,100) + 1);
394                                 self.v_angle_x = self.v_angle_x + (random() * 2 - 1) * shake;
395                                 self.v_angle_y = self.v_angle_y + (random() * 2 - 1) * shake;
396                         }
397                 }
398                 else
399                         self.max_armorvalue += (save + take);
400         }
401         self.dmg_save = self.dmg_save + save;//max(save - 10, 0);
402         self.dmg_take = self.dmg_take + take;//max(take - 10, 0);
403         self.dmg_inflictor = inflictor;
404
405         if(attacker == self)
406         {
407                 // don't reset pushltime for self damage as it may be an attempt to
408                 // escape a lava pit or similar
409                 //self.pushltime = 0;
410         }
411         else if(attacker.classname == "player" || attacker.classname == "gib")
412         {
413                 self.pusher = attacker;
414                 self.pushltime = time + cvar("g_maxpushtime");
415         }
416         else if(time < self.pushltime)
417         {
418                 attacker = self.pusher;
419                 self.pushltime = max(self.pushltime, time + 0.6);
420         }
421         else
422                 self.pushltime = 0;
423
424         if (self.health < 1)
425         {
426                 float defer_ClientKill_Now_TeamChange;
427                 defer_ClientKill_Now_TeamChange = FALSE;
428
429                 if(sv_gentle < 1) // TODO make a "gentle" version?
430                 {
431                         if(deathtype == DEATH_DROWN)
432                                 PlayerSound(playersound_drown, CHAN_PAIN, VOICETYPE_PLAYERSOUND);
433                         else
434                                 PlayerSound(playersound_death, CHAN_PAIN, VOICETYPE_PLAYERSOUND);
435                 }
436
437                 // get rid of kill indicator
438                 if(self.killindicator)
439                 {
440                         remove(self.killindicator);
441                         self.killindicator = world;
442                         if(self.killindicator_teamchange)
443                                 defer_ClientKill_Now_TeamChange = TRUE;
444
445                         if(self.classname == "body")
446                         if(deathtype == DEATH_KILL)
447                         {
448                                 // for the lemmings fans, a small harmless explosion
449                                 pointparticles(particleeffectnum("rocket_explode"), self.origin, '0 0 0', 1);
450                         }
451                 }
452
453                 // become fully visible
454                 self.alpha = 1;
455                 // clear selected player display
456                 ClearSelectedPlayer();
457                 // throw a weapon
458                 SpawnThrownWeapon (self.origin + (self.mins + self.maxs) * 0.5, self.switchweapon);
459                 // print an obituary message
460                 Obituary (attacker, inflictor, self, deathtype);
461                 race_PreDie();
462                 DropAllRunes(self);
463                 if(self == attacker)
464                         kh_Key_DropAll(self, TRUE);
465                 else if(attacker.classname == "player" || attacker.classname == "gib")
466                         kh_Key_DropAll(self, FALSE);
467                 else
468                         kh_Key_DropAll(self, TRUE);
469                 if(self.flagcarried)
470                 {
471                         if(attacker.classname != "player" && attacker.classname != "gib")
472                                 DropFlag(self.flagcarried, self, attacker); // penalty for flag loss by suicide
473                         else if(attacker.team == self.team)
474                                 DropFlag(self.flagcarried, attacker, attacker); // penalty for flag loss by suicide/teamkill
475                         else
476                                 DropFlag(self.flagcarried, world, attacker);
477                 }
478                 Portal_ClearAllLater(self);
479                 // clear waypoints
480                 WaypointSprite_PlayerDead();
481                 // make the corpse upright (not tilted)
482                 self.angles_x = 0;
483                 self.angles_z = 0;
484                 // don't spin
485                 self.avelocity = '0 0 0';
486                 // view from the floor
487                 self.view_ofs = '0 0 -8';
488                 // toss the corpse
489                 self.movetype = MOVETYPE_TOSS;
490                 // shootable corpse
491                 self.solid = SOLID_CORPSE;
492                 // don't stick to the floor
493                 self.flags &~= FL_ONGROUND;
494                 // dying animation
495                 self.deadflag = DEAD_DYING;
496                 // when to allow respawn
497                 sdelay = 0;
498                 waves = 0;
499                 if(cvar("g_respawn_mapsettings"))
500                 {
501                         sdelay = cvar("g_respawn_mapsettings_delay");
502                         waves = cvar("g_respawn_mapsettings_waves");
503                 }
504                 if(!sdelay)
505                         sdelay = cvar(strcat("g_", GetGametype(), "_respawn_delay"));
506                 if(!sdelay)
507                         sdelay = cvar("g_respawn_delay");
508                 if(!waves)
509                         waves = cvar(strcat("g_", GetGametype(), "_respawn_waves"));
510                 if(!waves)
511                         waves = cvar("g_respawn_waves");
512                 if(waves)
513                         self.death_time = ceil((time + sdelay) / waves) * waves;
514                 else
515                         self.death_time = time + sdelay;
516                 if((sdelay + waves >= 5.0) && (self.death_time - time > 1.75))
517                         self.respawn_countdown = 10; // first number to count down from is 10
518                 else
519                         self.respawn_countdown = -1; // do not count down
520                 if (random() < 0.5)
521                 {
522                         player_setanim(self.anim_die1, FALSE, TRUE, TRUE);
523                         self.dead_frame = self.anim_dead1_x;
524                 }
525                 else
526                 {
527                         player_setanim(self.anim_die2, FALSE, TRUE, TRUE);
528                         self.dead_frame = self.anim_dead2_x;
529                 }
530                 // set damage function to corpse damage
531                 self.event_damage = PlayerCorpseDamage;
532                 // call the corpse damage function just in case it wants to gib
533                 self.event_damage(inflictor, attacker, 0, deathtype, hitloc, force);
534                 // set up to fade out later
535                 SUB_SetFade (self, time + 12 + random () * 4, 1);
536
537                 // remove laserdot
538                 if(self.weaponentity)
539                         if(self.weaponentity.lasertarget)
540                                 remove(self.weaponentity.lasertarget);
541
542                 if(clienttype(self) == CLIENTTYPE_REAL)
543                 {
544                         self.fixangle = TRUE;
545                         //msg_entity = self;
546                         //WriteByte (MSG_ONE, SVC_SETANGLE);
547                         //WriteAngle (MSG_ONE, self.v_angle_x);
548                         //WriteAngle (MSG_ONE, self.v_angle_y);
549                         //WriteAngle (MSG_ONE, 80);
550                 }
551
552                 if(g_arena)
553                         Spawnqueue_Unmark(self);
554
555                 if(defer_ClientKill_Now_TeamChange)
556                         ClientKill_Now_TeamChange();
557
558                 if(sv_gentle > 0) {
559                         // remove corpse
560                         PlayerCorpseDamage (inflictor, attacker, 100.0, deathtype, hitloc, force);
561                 }
562         }
563 }
564
565 float UpdateSelectedPlayer_countvalue(float v)
566 {
567         return max(0, (v - 1.0) / 0.5);
568 }
569
570 // returns: -2 if no hit, otherwise cos of the angle
571 // uses the global v_angle
572 float UpdateSelectedPlayer_canSee(entity p, float mincosangle, float maxdist)
573 {
574         vector so, d;
575         float c;
576
577         if(p == self)
578                 return -2;
579
580         if(p.deadflag)
581                 return -2;
582
583         so = self.origin + self.view_ofs;
584         d = p.origin - so;
585
586         // misaimed?
587         if(dist_point_line(d, '0 0 0', v_forward) > maxdist)
588                 return -2;
589
590         // now find the cos of the angle...
591         c = normalize(d) * v_forward;
592
593         if(c <= mincosangle)
594                 return -2;
595
596         traceline(so, p.origin, MOVE_NOMONSTERS, self);
597         if(trace_fraction < 1)
598                 return -2;
599
600         return c;
601 }
602
603 void ClearSelectedPlayer()
604 {
605         if(self.selected_player)
606         {
607                 centerprint_expire(self, CENTERPRIO_POINT);
608                 self.selected_player = world;
609                 self.selected_player_display_needs_update = FALSE;
610         }
611 }
612
613 void UpdateSelectedPlayer()
614 {
615         entity selected;
616         float selected_score;
617         selected = world;
618         selected_score = 0.95; // 18 degrees
619
620         if(!cvar("sv_allow_shownames"))
621                 return;
622
623         if(clienttype(self) != CLIENTTYPE_REAL)
624                 return;
625
626         if(self.cvar_cl_shownames == 0)
627                 return;
628
629         if(self.cvar_cl_shownames == 1 && !teams_matter)
630                 return;
631
632         makevectors(self.v_angle); // sets v_forward
633
634         // 1. cursor trace is always right
635         if(self.cursor_trace_ent && self.cursor_trace_ent.classname == "player" && !self.cursor_trace_ent.deadflag)
636         {
637                 selected = self.cursor_trace_ent;
638         }
639         else
640         {
641                 // 2. if we don't have a cursor trace, find the player which is least
642                 //    mis-aimed at
643                 entity p;
644                 FOR_EACH_PLAYER(p)
645                 {
646                         float c;
647                         c = UpdateSelectedPlayer_canSee(p, selected_score, 100); // 100 = 2.5 meters
648                         if(c >= -1)
649                         {
650                                 selected = p;
651                                 selected_score = c;
652                         }
653                 }
654         }
655
656         if(selected)
657         {
658                 self.selected_player_display_timeout = time + self.cvar_scr_centertime;
659         }
660         else
661         {
662                 if(time < self.selected_player_display_timeout)
663                         if(UpdateSelectedPlayer_canSee(self.selected_player, 0.7, 200) >= -1) // 5 meters, 45 degrees
664                                 selected = self.selected_player;
665         }
666
667         if(selected)
668         {
669                 if(selected == self.selected_player)
670                 {
671                         float save;
672                         save = UpdateSelectedPlayer_countvalue(self.selected_player_count);
673                         self.selected_player_count = self.selected_player_count + frametime;
674                         if(save != UpdateSelectedPlayer_countvalue(self.selected_player_count))
675                         {
676                                 string namestr, healthstr;
677                                 namestr = playername(selected);
678                                 if(teams_matter)
679                                 {
680                                         healthstr = ftos(floor(selected.health));
681                                         if(self.team == selected.team)
682                                         {
683                                                 namestr = strcat(namestr, " (", healthstr, "%)");
684                                                 self.selected_player_display_needs_update = TRUE;
685                                         }
686                                 }
687                                 centerprint_atprio(self, CENTERPRIO_POINT, namestr);
688                         }
689                 }
690                 else
691                 {
692                         ClearSelectedPlayer();
693                         self.selected_player = selected;
694                         self.selected_player_time = time;
695                         self.selected_player_count = 0;
696                         self.selected_player_display_needs_update = FALSE;
697                 }
698         }
699         else
700         {
701                 ClearSelectedPlayer();
702         }
703
704         if(self.selected_player)
705                 self.last_selected_player = self.selected_player;
706 }
707
708 .float floodcontrol_chat;
709 .float floodcontrol_chatteam;
710 void Say(entity source, float teamsay, string msgin, float floodcontrol)
711 {
712         string msgstr, colorstr, cmsgstr, namestr;
713         float flood;
714         entity head;
715
716         if(Ban_MaybeEnforceBan(source))
717                 return;
718
719         if(!teamsay)
720                 if(substring(msgin, 0, 1) == " ")
721                         msgin = substring(msgin, 1, strlen(msgin) - 1); // work around DP say bug (say_team does not have this!)
722
723         msgin = formatmessage(msgin);
724
725         if(msgin == "")
726                 return;
727
728         if(source.classname != "player")
729                 colorstr = "^0"; // black for spectators
730         else if(teams_matter)
731                 colorstr = Team_ColorCode(source.team);
732         else
733                 teamsay = FALSE;
734
735         if(intermission_running)
736                 teamsay = FALSE;
737
738         /*
739          * using bprint solves this... me stupid
740         // how can we prevent the message from appearing in a listen server?
741         // for now, just give "say" back and only handle say_team
742         if(!teamsay)
743         {
744                 clientcommand(self, strcat("say ", msgin));
745                 return;
746         }
747         */
748
749         if(cvar("g_chat_teamcolors"))
750                 namestr = playername(source);
751         else
752                 namestr = source.netname;
753         if(teamsay)
754         {
755                 msgstr = strcat("\{1}\{13}", colorstr, "(^3", namestr, colorstr, ") ^7", msgin);
756                 cmsgstr = strcat(colorstr, "(^3", namestr, colorstr, ")\n^7", msgin);
757         }
758         else
759                 msgstr = strcat("\{1}", namestr, "^7: ", msgin);
760         
761         msgstr = strcat(strreplace("\n", " ", msgstr), "\n"); // newlines only are good for centerprint
762
763         // FLOOD CONTROL
764         flood = 0;
765         if(floodcontrol)
766         {
767                 float flood_spl;
768                 float flood_burst;
769                 float flood_lmax;
770                 var .float flood_field;
771                 float lines;
772                 if(teamsay)
773                 {
774                         flood_spl = cvar("g_chat_flood_spl_team");
775                         flood_burst = cvar("g_chat_flood_burst_team");
776                         flood_lmax = cvar("g_chat_flood_lmax_team");
777                         flood_field = floodcontrol_chatteam;
778                 }
779                 else
780                 {
781                         flood_spl = cvar("g_chat_flood_spl");
782                         flood_burst = cvar("g_chat_flood_burst");
783                         flood_lmax = cvar("g_chat_flood_lmax");
784                         flood_field = floodcontrol_chat;
785                 }
786                 flood_burst = max(0, flood_burst - 1);
787                 // to match explanation in default.cfg, a value of 3 must allow three-line bursts and not four!
788                 lines = ceil(strlennocol(msgstr) / 75);
789                 if(flood_lmax && lines > flood_lmax)
790                         flood = 2;
791                 else if(time >= self.flood_field)
792                         self.flood_field = max(time - flood_burst * flood_spl, self.flood_field) + lines * flood_spl;
793                 else
794                         flood = 1;
795         }
796
797         if(flood)
798         {
799                 if(cvar("g_chat_flood_notify_flooder"))
800                 {
801                         if(flood == 1)
802                                 sprint(self, strcat("^3FLOOD CONTROL: ^7wait ^1", ftos(self.flood_field - time), "^3 seconds\n"));
803                         else if(flood == 2)
804                                 sprint(self, "^3FLOOD CONTROL: ^7message too long\n");
805                 }
806                 else
807                         sprint(self, msgstr);
808                 print("NOTE: ", playername(self), "^7 is flooding.\n");
809         }
810         else if(teamsay)
811         {
812                 if(source.classname == "player")
813                 {
814                         FOR_EACH_REALPLAYER(head)
815                         {
816                                 if(head.team == source.team)
817                                 {
818                                         sprint(head, msgstr);
819                                         centerprint(head, cmsgstr);
820                                 }
821                         }
822                 }
823                 else
824                 {
825                         FOR_EACH_REALCLIENT(head) if(head.classname != "player")
826                         {
827                                 sprint(head, msgstr);
828                                 centerprint(head, cmsgstr);
829                         }
830                 }
831         }
832         else
833         {
834                 if(
835                         (cvar("g_chat_nospectators") == 1 && source.classname != "player")
836                         || (cvar("g_chat_nospectators") == 2 && source.classname != "player" && !inWarmupStage)
837                 ) {
838                         FOR_EACH_REALCLIENT(head) if(head.classname != "player") {
839                                 sprint(head, msgstr);
840                         }
841                 }
842                 else
843                         bprint(msgstr);
844         }
845 }
846
847 float GetVoiceMessageVoiceType(string type)
848 {
849         if(type == "taunt")
850                 return VOICETYPE_TAUNT;
851         if(type == "teamshoot")
852                 return VOICETYPE_LASTATTACKER;
853         return VOICETYPE_TEAMRADIO;
854 }
855
856 string allvoicesamples;
857 float GetPlayerSoundSampleField_notFound;
858 float GetPlayerSoundSampleField_fixed;
859 .string GetVoiceMessageSampleField(string type)
860 {
861         GetPlayerSoundSampleField_notFound = 0;
862         GetPlayerSoundSampleField_fixed = 0;
863         switch(type)
864         {
865 #define _VOICEMSG(m) case #m: return playersound_##m;
866                 ALLVOICEMSGS
867 #undef _VOICEMSG
868         }
869         GetPlayerSoundSampleField_notFound = 1;
870         return playersound_taunt;
871 }
872
873 .string GetPlayerSoundSampleField(string type)
874 {
875         GetPlayerSoundSampleField_notFound = 0;
876         GetPlayerSoundSampleField_fixed = 0;
877         switch(type)
878         {
879 #define _VOICEMSG(m) case #m: return playersound_##m;
880                 ALLPLAYERSOUNDS
881 #undef _VOICEMSG
882         }
883         GetPlayerSoundSampleField_notFound = 1;
884         return playersound_taunt;
885 }
886
887 void PrecacheGlobalSound(string samplestring)
888 {
889         float n, i;
890         tokenize_sane(samplestring);
891         n = stof(argv(1));
892         if(n > 0)
893         {
894                 for(i = 1; i <= n; ++i)
895                         precache_sound(strcat(argv(0), ftos(i), ".wav"));
896         }
897         else
898         {
899                 precache_sound(strcat(argv(0), ".wav"));
900         }
901 }
902
903 void PrecachePlayerSounds(string f)
904 {
905         float fh;
906         string s;
907         fh = fopen(f, FILE_READ);
908         if(fh < 0)
909                 return;
910         while((s = fgets(fh)))
911         {
912                 if(tokenize_sane(s) != 3)
913                 {
914                         dprint("Invalid sound info line: ", s, "\n");
915                         continue;
916                 }
917                 PrecacheGlobalSound(strcat(argv(1), " ", argv(2)));
918         }
919         fclose(fh);
920
921         if not(allvoicesamples)
922         {
923 #define _VOICEMSG(m) allvoicesamples = strcat(allvoicesamples, " ", #m);
924                 ALLVOICEMSGS
925 #undef _VOICEMSG
926                 allvoicesamples = strzone(substring(allvoicesamples, 1, strlen(allvoicesamples) - 1));
927         }
928 }
929
930 void ClearPlayerSounds()
931 {
932 #define _VOICEMSG(m) if(self.playersound_##m) { strunzone(self.playersound_##m); self.playersound_##m = string_null; }
933         ALLPLAYERSOUNDS
934         ALLVOICEMSGS
935 #undef _VOICEMSG
936 }
937
938 void LoadPlayerSounds(string f, float first)
939 {
940         float fh;
941         string s;
942         var .string field;
943         fh = fopen(f, FILE_READ);
944         if(fh < 0)
945                 return;
946         while((s = fgets(fh)))
947         {
948                 if(tokenize_sane(s) != 3)
949                         continue;
950                 field = GetPlayerSoundSampleField(argv(0));
951                 if(GetPlayerSoundSampleField_notFound)
952                         field = GetVoiceMessageSampleField(argv(0));
953                 if(GetPlayerSoundSampleField_notFound)
954                         continue;
955                 if(GetPlayerSoundSampleField_fixed)
956                         if not(first)
957                                 continue;
958                 if(self.field)
959                         strunzone(self.field);
960                 self.field = strzone(strcat(argv(1), " ", argv(2)));
961         }
962         fclose(fh);
963 }
964
965 .float modelindex_for_playersound;
966 void UpdatePlayerSounds()
967 {
968         if(self.modelindex == self.modelindex_for_playersound)
969                 return;
970         self.modelindex_for_playersound = self.modelindex;
971         ClearPlayerSounds();
972         LoadPlayerSounds("sound/player/default.sounds", 1);
973         LoadPlayerSounds(strcat(self.model, ".sounds"), 0);
974 }
975
976 void GlobalSound(string sample, float chan, float voicetype)
977 {
978         float n;
979         float tauntrand;
980
981         if(sample == "")
982                 return;
983
984         tokenize_sane(sample);
985         n = stof(argv(1));
986         if(n > 0)
987                 sample = strcat(argv(0), ftos(floor(random() * n + 1)), ".wav"); // randomization
988         else
989                 sample = strcat(argv(0), ".wav"); // randomization
990         
991         switch(voicetype)
992         {
993                 case VOICETYPE_LASTATTACKER_ONLY:
994                         if(self.pusher)
995                                 if(self.pusher.team == self.team)
996                                 {
997                                         msg_entity = self.pusher;
998                                         if(clienttype(msg_entity) == CLIENTTYPE_REAL)
999                                         {
1000                                                 if(msg_entity.cvar_cl_voice_directional == 1)
1001                                                         soundto(MSG_ONE, self, chan, sample, VOL_BASEVOICE, ATTN_MIN);
1002                                                 else
1003                                                         soundto(MSG_ONE, self, chan, sample, VOL_BASEVOICE, ATTN_NONE);
1004                                         }
1005                                 }
1006                         break;
1007                 case VOICETYPE_LASTATTACKER:
1008                         if(self.pusher)
1009                                 if(self.pusher.team == self.team)
1010                                 {
1011                                         msg_entity = self.pusher;
1012                                         if(clienttype(msg_entity) == CLIENTTYPE_REAL)
1013                                         {
1014                                                 if(msg_entity.cvar_cl_voice_directional == 1)
1015                                                         soundto(MSG_ONE, self, chan, sample, VOL_BASEVOICE, ATTN_MIN);
1016                                                 else
1017                                                         soundto(MSG_ONE, self, chan, sample, VOL_BASEVOICE, ATTN_NONE);
1018                                         }
1019                                         msg_entity = self;
1020                                         if(clienttype(msg_entity) == CLIENTTYPE_REAL)
1021                                                 soundto(MSG_ONE, self, chan, sample, VOL_BASE, ATTN_NONE);
1022                                 }
1023                         break;
1024                 case VOICETYPE_TEAMRADIO:
1025                         FOR_EACH_REALCLIENT(msg_entity)
1026                                 if(!teams_matter || msg_entity.team == self.team)
1027                                 {
1028                                         if(msg_entity.cvar_cl_voice_directional == 1)
1029                                                 soundto(MSG_ONE, self, chan, sample, VOL_BASEVOICE, ATTN_MIN);
1030                                         else
1031                                                 soundto(MSG_ONE, self, chan, sample, VOL_BASEVOICE, ATTN_NONE);
1032                                 }
1033                         break;
1034                 case VOICETYPE_AUTOTAUNT:
1035                         tauntrand = random();
1036                         FOR_EACH_REALCLIENT(msg_entity)
1037                                 if (tauntrand < msg_entity.cvar_cl_autotaunt)
1038                                 {
1039                                         if (msg_entity.cvar_cl_voice_directional >= 1)
1040                                                 soundto(MSG_ONE, self, chan, sample, VOL_BASEVOICE, bound(ATTN_MIN, msg_entity.cvar_cl_voice_directional_taunt_attenuation, ATTN_MAX));
1041                                         else
1042                                                 soundto(MSG_ONE, self, chan, sample, VOL_BASEVOICE, ATTN_NONE);
1043                                 }
1044                         break;
1045                 case VOICETYPE_TAUNT:
1046                         FOR_EACH_REALCLIENT(msg_entity)
1047                         {
1048                                 if (msg_entity.cvar_cl_voice_directional >= 1)
1049                                         soundto(MSG_ONE, self, chan, sample, VOL_BASEVOICE, bound(ATTN_MIN, msg_entity.cvar_cl_voice_directional_taunt_attenuation, ATTN_MAX));
1050                                 else
1051                                         soundto(MSG_ONE, self, chan, sample, VOL_BASEVOICE, ATTN_NONE);
1052                         }
1053                 case VOICETYPE_PLAYERSOUND:
1054                         sound(self, chan, sample, VOL_BASE, ATTN_NORM);
1055                         break;
1056                 default:
1057                         backtrace("Invalid voice type!");
1058                         break;
1059         }
1060 }
1061
1062 void PlayerSound(.string samplefield, float chan, float voicetype)
1063 {
1064         string sample;
1065         sample = self.samplefield;
1066         GlobalSound(sample, chan, voicetype);
1067 }
1068
1069 .float floodcontrol_voice;
1070 .float floodcontrol_voiceteam;
1071 void VoiceMessage(string type, string msg)
1072 {
1073         var .string sample;
1074         var float voicetype, ownteam;
1075         sample = GetVoiceMessageSampleField(type);
1076
1077         if(GetPlayerSoundSampleField_notFound)
1078         {
1079                 sprint(self, strcat("Invalid voice. Use one of: ", allvoicesamples, "\n"));
1080                 return;
1081         }
1082
1083         voicetype = GetVoiceMessageVoiceType(type);
1084         ownteam = (voicetype == VOICETYPE_TEAMRADIO);
1085
1086         float flood;
1087         float flood_spv;
1088         var .float flood_field;
1089
1090         flood = 0;
1091         if(ownteam)
1092         {
1093                 flood_spv = cvar("g_voice_flood_spv_team");
1094                 flood_field = floodcontrol_voiceteam;
1095         }
1096         else
1097         {
1098                 flood_spv = cvar("g_voice_flood_spv");
1099                 flood_field = floodcontrol_voice;
1100         }
1101
1102         if(time >= self.flood_field)
1103                 self.flood_field = max(time, self.flood_field) + flood_spv;
1104         else
1105                 flood = 1;
1106
1107         if (msg != "")
1108                 Say(self, ownteam, msg, 0);
1109
1110         if (!flood)
1111                 PlayerSound(sample, CHAN_VOICE, voicetype);
1112 }