]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/waypoints/waypointsprites.qc
Butcher another hidden gamemode specific block
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / mutators / mutator / waypoints / waypointsprites.qc
1 #include "waypointsprites.qh"
2
3 #ifdef IMPLEMENTATION
4
5 REGISTER_MUTATOR(waypointsprites, true);
6
7 #ifdef SVQC
8 /** flags origin [team displayrule] [spritename] [spritename2] [spritename3] [lifetime maxdistance hideable] */
9 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags)
10 {
11     WriteMutator(MSG_ENTITY, waypointsprites);
12
13     sendflags = sendflags & 0x7F;
14
15     if (g_nexball)
16         sendflags &= ~0x80;
17     else if (self.max_health || (self.pain_finished && (time < self.pain_finished + 0.25)))
18         sendflags |= 0x80;
19
20     WriteByte(MSG_ENTITY, sendflags);
21     WriteByte(MSG_ENTITY, self.wp_extra);
22
23     int f = 0;
24     if(self.currentammo)
25         f |= 1; // hideable
26     if(self.exteriormodeltoclient == to)
27         f |= 2; // my own
28
29     MUTATOR_CALLHOOK(SendWaypoint, this, to, sendflags, f);
30
31     if (sendflags & 0x80)
32     {
33         if (self.max_health)
34         {
35             WriteByte(MSG_ENTITY, (self.health / self.max_health) * 191.0);
36         }
37         else
38         {
39             float dt = self.pain_finished - time;
40             dt = bound(0, dt * 32, 16383);
41             WriteByte(MSG_ENTITY, (dt & 0xFF00) / 256 + 192);
42             WriteByte(MSG_ENTITY, (dt & 0x00FF));
43         }
44     }
45
46     if (sendflags & 64)
47     {
48         WriteCoord(MSG_ENTITY, self.origin.x);
49         WriteCoord(MSG_ENTITY, self.origin.y);
50         WriteCoord(MSG_ENTITY, self.origin.z);
51     }
52
53     if (sendflags & 1)
54     {
55         WriteByte(MSG_ENTITY, self.team);
56         WriteByte(MSG_ENTITY, self.rule);
57     }
58
59     if (sendflags & 2)
60         WriteString(MSG_ENTITY, self.model1);
61
62     if (sendflags & 4)
63         WriteString(MSG_ENTITY, self.model2);
64
65     if (sendflags & 8)
66         WriteString(MSG_ENTITY, self.model3);
67
68     if (sendflags & 16)
69     {
70         WriteCoord(MSG_ENTITY, self.fade_time);
71         WriteCoord(MSG_ENTITY, self.teleport_time);
72         WriteShort(MSG_ENTITY, self.fade_rate); // maxdist
73         WriteByte(MSG_ENTITY, f);
74     }
75
76     if (sendflags & 32)
77     {
78         WriteByte(MSG_ENTITY, self.cnt); // icon on radar
79         WriteByte(MSG_ENTITY, self.colormod.x * 255.0);
80         WriteByte(MSG_ENTITY, self.colormod.y * 255.0);
81         WriteByte(MSG_ENTITY, self.colormod.z * 255.0);
82
83         if (WaypointSprite_isteammate(self.owner, WaypointSprite_getviewentity(to)))
84         {
85             float dt = (self.waypointsprite_helpmetime - time) / 0.1;
86             if (dt < 0)
87                 dt = 0;
88             if (dt > 255)
89                 dt = 255;
90             WriteByte(MSG_ENTITY, dt);
91         }
92         else
93             WriteByte(MSG_ENTITY, 0);
94     }
95
96     return true;
97 }
98 #endif
99
100 #ifdef CSQC
101 void Ent_WaypointSprite();
102 MUTATOR_HOOKFUNCTION(waypointsprites, CSQC_Ent_Update) {
103     if (MUTATOR_RETURNVALUE) return false;
104     if (!ReadMutatorEquals(mutator_argv_int_0, waypointsprites)) return false;
105     Ent_WaypointSprite();
106     return true;
107 }
108
109 void Ent_RemoveWaypointSprite()
110 {SELFPARAM();
111     if (self.netname) strunzone(self.netname);
112     if (self.netname2) strunzone(self.netname2);
113     if (self.netname3) strunzone(self.netname3);
114 }
115
116 /** flags origin [team displayrule] [spritename] [spritename2] [spritename3] [lifetime maxdistance hideable] */
117 void Ent_WaypointSprite()
118 {SELFPARAM();
119     int sendflags = ReadByte();
120     self.wp_extra = ReadByte();
121
122     if (!self.spawntime)
123         self.spawntime = time;
124
125     self.draw2d = Draw_WaypointSprite;
126
127     InterpolateOrigin_Undo();
128     self.iflags |= IFLAG_ORIGIN;
129
130     if (sendflags & 0x80)
131     {
132         int t = ReadByte();
133         if (t < 192)
134         {
135             self.health = t / 191.0;
136             self.build_finished = 0;
137         }
138         else
139         {
140             t = (t - 192) * 256 + ReadByte();
141             self.build_started = servertime;
142             if (self.build_finished)
143                 self.build_starthealth = bound(0, self.health, 1);
144             else
145                 self.build_starthealth = 0;
146             self.build_finished = servertime + t / 32;
147         }
148     }
149     else
150     {
151         self.health = -1;
152         self.build_finished = 0;
153     }
154
155     if (sendflags & 64)
156     {
157         // unfortunately, this needs to be exact (for the 3D display)
158         self.origin_x = ReadCoord();
159         self.origin_y = ReadCoord();
160         self.origin_z = ReadCoord();
161         setorigin(self, self.origin);
162     }
163
164     if (sendflags & 1)
165     {
166         self.team = ReadByte();
167         self.rule = ReadByte();
168     }
169
170     if (sendflags & 2)
171     {
172         if (self.netname)
173             strunzone(self.netname);
174         self.netname = strzone(ReadString());
175     }
176
177     if (sendflags & 4)
178     {
179         if (self.netname2)
180             strunzone(self.netname2);
181         self.netname2 = strzone(ReadString());
182     }
183
184     if (sendflags & 8)
185     {
186         if (self.netname3)
187             strunzone(self.netname3);
188         self.netname3 = strzone(ReadString());
189     }
190
191     if (sendflags & 16)
192     {
193         self.lifetime = ReadCoord();
194         self.fadetime = ReadCoord();
195         self.maxdistance = ReadShort();
196         self.hideflags = ReadByte();
197     }
198
199     if (sendflags & 32)
200     {
201         int f = ReadByte();
202         self.teamradar_icon = f & BITS(7);
203         if (f & BIT(7))
204         {
205             self.(teamradar_times[self.teamradar_time_index]) = time;
206             self.teamradar_time_index = (self.teamradar_time_index + 1) % MAX_TEAMRADAR_TIMES;
207         }
208         self.teamradar_color_x = ReadByte() / 255.0;
209         self.teamradar_color_y = ReadByte() / 255.0;
210         self.teamradar_color_z = ReadByte() / 255.0;
211         self.helpme = ReadByte() * 0.1;
212         if (self.helpme > 0)
213             self.helpme += servertime;
214     }
215
216     InterpolateOrigin_Note();
217
218     self.entremove = Ent_RemoveWaypointSprite;
219 }
220 #endif
221
222 #ifdef CSQC
223 float spritelookupblinkvalue(string s)
224 {SELFPARAM();
225     if (s == WP_Weapon.netname) {
226         if (get_weaponinfo(self.wp_extra).spawnflags & WEP_FLAG_SUPERWEAPON)
227             return 2;
228     }
229     if (s == WP_Item.netname) return Items[self.wp_extra].m_waypointblink;
230
231     return 1;
232 }
233
234 vector spritelookupcolor(entity this, string s, vector def)
235 {
236     if (s == WP_Weapon.netname  || s == RADARICON_Weapon.netname) return get_weaponinfo(this.wp_extra).wpcolor;
237     if (s == WP_Item.netname    || s == RADARICON_Item.netname) return Items[this.wp_extra].m_color;
238     if (s == WP_Buff.netname    || s == RADARICON_Buff.netname) return Buffs[this.wp_extra].m_color;
239     return def;
240 }
241
242 string spritelookuptext(string s)
243 {SELFPARAM();
244     if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
245     if (s == WP_Weapon.netname) return get_weaponinfo(self.wp_extra).message;
246     if (s == WP_Item.netname) return Items[self.wp_extra].m_waypoint;
247     if (s == WP_Buff.netname) return Buffs[self.wp_extra].m_prettyName;
248     if (s == WP_Monster.netname) return get_monsterinfo(self.wp_extra).monster_name;
249
250     // need to loop, as our netname could be one of three
251     FOREACH(Waypoints, it.netname == s, LAMBDA(
252         return it.m_name;
253     ));
254
255     return s;
256 }
257 #endif
258
259 #ifdef CSQC
260 void drawrotpic(vector org, float rot, string pic, vector sz, vector hotspot, vector rgb, float a, float f)
261 {
262     vector v1, v2, v3, v4;
263
264     hotspot = -1 * hotspot;
265
266     // hotspot-relative coordinates of the corners
267     v1 = hotspot;
268     v2 = hotspot + '1 0 0' * sz.x;
269     v3 = hotspot + '1 0 0' * sz.x + '0 1 0' * sz.y;
270     v4 = hotspot                  + '0 1 0' * sz.y;
271
272     // rotate them, and make them absolute
273     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
274     v1 = rotate(v1, rot) + org;
275     v2 = rotate(v2, rot) + org;
276     v3 = rotate(v3, rot) + org;
277     v4 = rotate(v4, rot) + org;
278
279     // draw them
280     R_BeginPolygon(pic, f);
281     R_PolygonVertex(v1, '0 0 0', rgb, a);
282     R_PolygonVertex(v2, '1 0 0', rgb, a);
283     R_PolygonVertex(v3, '1 1 0', rgb, a);
284     R_PolygonVertex(v4, '0 1 0', rgb, a);
285     R_EndPolygon();
286 }
287
288 void drawquad(vector o, vector ri, vector up, string pic, vector rgb, float a, float f)
289 {
290     R_BeginPolygon(pic, f);
291     R_PolygonVertex(o, '0 0 0', rgb, a);
292     R_PolygonVertex(o + ri, '1 0 0', rgb, a);
293     R_PolygonVertex(o + up + ri, '1 1 0', rgb, a);
294     R_PolygonVertex(o + up, '0 1 0', rgb, a);
295     R_EndPolygon();
296 }
297
298 void drawhealthbar(vector org, float rot, float h, vector sz, vector hotspot, float width, float theheight, float margin, float border, float align, vector rgb, float a, vector hrgb, float ha, float f)
299 {
300     vector o, ri, up;
301     float owidth; // outer width
302
303     hotspot = -1 * hotspot;
304
305     // hotspot-relative coordinates of the healthbar corners
306     o = hotspot;
307     ri = '1 0 0';
308     up = '0 1 0';
309
310     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
311     o = rotate(o, rot) + org;
312     ri = rotate(ri, rot);
313     up = rotate(up, rot);
314
315     owidth = width + 2 * border;
316     o = o - up * (margin + border + theheight) + ri * (sz.x - owidth) * 0.5;
317
318     drawquad(o - up * border,                               ri * owidth,    up * border,    "", rgb,  a,  f);
319     drawquad(o + up * theheight,                            ri * owidth,    up * border,    "", rgb,  a,  f);
320     drawquad(o,                                             ri * border,    up * theheight, "", rgb,  a,  f);
321     drawquad(o + ri * (owidth - border),                    ri * border,    up * theheight, "", rgb,  a,  f);
322     drawquad(o + ri * (border + align * ((1 - h) * width)), ri * width * h, up * theheight, "", hrgb, ha, f);
323 }
324
325 // returns location of sprite text
326 vector drawspritearrow(vector o, float ang, vector rgb, float a, float t)
327 {
328     float size   = 9.0 * t;
329     float border = 1.5 * t;
330     float margin = 4.0 * t;
331
332     float borderDiag = border * 1.414;
333     vector arrowX  = eX * size;
334     vector arrowY  = eY * (size+borderDiag);
335     vector borderX = eX * (size+borderDiag);
336     vector borderY = eY * (size+borderDiag+border);
337
338     R_BeginPolygon("", DRAWFLAG_NORMAL);
339     R_PolygonVertex(o,                                  '0 0 0', '0 0 0', a);
340     R_PolygonVertex(o + rotate(arrowY  - borderX, ang), '0 0 0', '0 0 0', a);
341     R_PolygonVertex(o + rotate(borderY - borderX, ang), '0 0 0', '0 0 0', a);
342     R_PolygonVertex(o + rotate(borderY + borderX, ang), '0 0 0', '0 0 0', a);
343     R_PolygonVertex(o + rotate(arrowY  + borderX, ang), '0 0 0', '0 0 0', a);
344     R_EndPolygon();
345
346     R_BeginPolygon("", DRAWFLAG_ADDITIVE);
347     R_PolygonVertex(o + rotate(eY * borderDiag, ang), '0 0 0', rgb, a);
348     R_PolygonVertex(o + rotate(arrowY - arrowX, ang), '0 0 0', rgb, a);
349     R_PolygonVertex(o + rotate(arrowY + arrowX, ang), '0 0 0', rgb, a);
350     R_EndPolygon();
351
352     return o + rotate(eY * (borderDiag+size+margin), ang);
353 }
354
355 // returns location of sprite healthbar
356 vector drawspritetext(vector o, float ang, float minwidth, vector rgb, float a, vector fontsize, string s)
357 {
358     float algnx, algny;
359     float sw, w, h;
360     float aspect, sa, ca;
361
362     sw = stringwidth(s, false, fontsize);
363     if (sw > minwidth)
364         w = sw;
365     else
366         w = minwidth;
367     h = fontsize.y;
368
369     // how do corners work?
370     aspect = vid_conwidth / vid_conheight;
371     sa = sin(ang);
372     ca = cos(ang) * aspect;
373     if (fabs(sa) > fabs(ca))
374     {
375         algnx = (sa < 0);
376         algny = 0.5 - 0.5 * ca / fabs(sa);
377     }
378     else
379     {
380         algnx = 0.5 - 0.5 * sa / fabs(ca);
381         algny = (ca < 0);
382     }
383
384     // align
385     o.x -= w * algnx;
386     o.y -= h * algny;
387
388     // we want to be onscreen
389     if (o.x < 0)
390         o.x = 0;
391     if (o.y < 0)
392         o.y = 0;
393     if (o.x > vid_conwidth - w)
394         o.x = vid_conwidth - w;
395     if (o.y > vid_conheight - h)
396         o.x = vid_conheight - h;
397
398     o.x += 0.5 * (w - sw);
399
400     drawstring(o, s, fontsize, rgb, a, DRAWFLAG_NORMAL);
401
402     o.x += 0.5 * sw;
403     o.y += 0.5 * h;
404
405     return o;
406 }
407
408 vector fixrgbexcess_move(vector rgb, vector src, vector dst)
409 {
410     vector yvec = '0.299 0.587 0.114';
411     return rgb + dst * ((src * yvec) / (dst * yvec)) * ((rgb - '1 1 1') * src);
412 }
413
414 vector fixrgbexcess(vector rgb)
415 {
416     if (rgb.x > 1) {
417         rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 1');
418         if (rgb.y > 1) {
419             rgb = fixrgbexcess_move(rgb, '0 1 0', '0 0 1');
420             if (rgb.z > 1) rgb.z = 1;
421         } else if (rgb.z > 1) {
422             rgb = fixrgbexcess_move(rgb, '0 0 1', '0 1 0');
423             if (rgb.y > 1) rgb.y = 1;
424         }
425     } else if (rgb.y > 1) {
426         rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 1');
427         if (rgb.x > 1) {
428             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 0 1');
429             if (rgb.z > 1) rgb.z = 1;
430         } else if (rgb.z > 1) {
431             rgb = fixrgbexcess_move(rgb, '0 0 1', '1 0 0');
432             if (rgb.x > 1) rgb.x = 1;
433         }
434     } else if (rgb.z > 1) {
435         rgb = fixrgbexcess_move(rgb, '0 0 1', '1 1 0');
436         if (rgb.x > 1) {
437             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 0');
438             if (rgb.y > 1) rgb.y = 1;
439         } else if (rgb.y > 1) {
440             rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 0');
441             if (rgb.x > 1) rgb.x = 1;
442         }
443     }
444     return rgb;
445 }
446
447 void Draw_WaypointSprite(entity this)
448 {
449     if (self.lifetime)
450         self.alpha = pow(bound(0, (self.fadetime - time) / self.lifetime, 1), waypointsprite_timealphaexponent);
451     else
452         self.alpha = 1;
453
454     if (self.hideflags & 2)
455         return; // radar only
456
457     if (autocvar_cl_hidewaypoints >= 2)
458         return;
459
460     if (self.hideflags & 1)
461         if (autocvar_cl_hidewaypoints)
462             return; // fixed waypoint
463
464     InterpolateOrigin_Do();
465
466     float t = GetPlayerColor(player_localnum) + 1;
467
468     string spriteimage = "";
469
470     // choose the sprite
471     switch (self.rule)
472     {
473         case SPRITERULE_SPECTATOR:
474             if (!(
475                 (autocvar_g_waypointsprite_itemstime == 1 && t == NUM_SPECTATOR + 1)
476             ||  (autocvar_g_waypointsprite_itemstime == 2 && (t == NUM_SPECTATOR + 1 || warmup_stage))
477                 ))
478                 return;
479             spriteimage = self.netname;
480             break;
481         case SPRITERULE_DEFAULT:
482             if (self.team)
483             {
484                 if (self.team == t)
485                     spriteimage = self.netname;
486                 else
487                     spriteimage = "";
488             }
489             else
490                 spriteimage = self.netname;
491             break;
492         case SPRITERULE_TEAMPLAY:
493             if (t == NUM_SPECTATOR + 1)
494                 spriteimage = self.netname3;
495             else if (self.team == t)
496                 spriteimage = self.netname2;
497             else
498                 spriteimage = self.netname;
499             break;
500         default:
501             error("Invalid waypointsprite rule!");
502             break;
503     }
504
505     if (spriteimage == "")
506         return;
507
508     ++waypointsprite_newcount;
509
510     float dist;
511     dist = vlen(self.origin - view_origin);
512
513     float a;
514     a = self.alpha * autocvar_hud_panel_fg_alpha;
515
516     if (self.maxdistance > waypointsprite_normdistance)
517         a *= pow(bound(0, (self.maxdistance - dist) / (self.maxdistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent);
518     else if (self.maxdistance > 0)
519         a *= pow(bound(0, (waypointsprite_fadedistance - dist) / (waypointsprite_fadedistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent) * (1 - waypointsprite_minalpha) + waypointsprite_minalpha;
520
521     vector rgb = spritelookupcolor(self, spriteimage, self.teamradar_color);
522     if (rgb == '0 0 0')
523     {
524         self.teamradar_color = '1 0 1';
525         LOG_INFOF("WARNING: sprite of name %s has no color, using pink so you notice it\n", spriteimage);
526     }
527
528     if (time - floor(time) > 0.5)
529     {
530         if (self.helpme && time < self.helpme)
531             a *= SPRITE_HELPME_BLINK;
532         else if (!self.lifetime) // fading out waypoints don't blink
533             a *= spritelookupblinkvalue(spriteimage);
534     }
535
536     if (a > 1)
537     {
538         rgb *= a;
539         a = 1;
540     }
541
542     if (a <= 0.003)
543         return;
544
545     rgb = fixrgbexcess(rgb);
546
547     vector o;
548     float ang;
549
550     o = project_3d_to_2d(self.origin);
551     if (o.z < 0
552     || o.x < (vid_conwidth * waypointsprite_edgeoffset_left)
553     || o.y < (vid_conheight * waypointsprite_edgeoffset_top)
554     || o.x > (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right))
555     || o.y > (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)))
556     {
557         // scale it to be just in view
558         vector d;
559         float f1, f2;
560
561         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
562         ang = atan2(-d.x, -d.y);
563         if (o.z < 0)
564             ang += M_PI;
565
566         f1 = d.x / vid_conwidth;
567         f2 = d.y / vid_conheight;
568
569         if (max(f1, -f1) > max(f2, -f2)) {
570             if (d.z * f1 > 0) {
571                 // RIGHT edge
572                 d = d * ((0.5 - waypointsprite_edgeoffset_right) / f1);
573             } else {
574                 // LEFT edge
575                 d = d * (-(0.5 - waypointsprite_edgeoffset_left) / f1);
576             }
577         } else {
578             if (d.z * f2 > 0) {
579                 // BOTTOM edge
580                 d = d * ((0.5 - waypointsprite_edgeoffset_bottom) / f2);
581             } else {
582                 // TOP edge
583                 d = d * (-(0.5 - waypointsprite_edgeoffset_top) / f2);
584             }
585         }
586
587         o = d + '0.5 0 0' * vid_conwidth + '0 0.5 0' * vid_conheight;
588     }
589     else
590     {
591 #if 1
592         ang = M_PI;
593 #else
594         vector d;
595         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
596         ang = atan2(-d.x, -d.y);
597 #endif
598     }
599     o.z = 0;
600
601     float edgedistance_min = min((o.y - (vid_conheight * waypointsprite_edgeoffset_top)),
602     (o.x - (vid_conwidth * waypointsprite_edgeoffset_left)),
603     (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right)) - o.x,
604     (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)) - o.y);
605
606     float vidscale = max(vid_conwidth / vid_width, vid_conheight / vid_height);
607
608     float crosshairdistance = sqrt( pow(o.x - vid_conwidth/2, 2) + pow(o.y - vid_conheight/2, 2) );
609
610     t = waypointsprite_scale * vidscale;
611     a *= waypointsprite_alpha;
612
613     {
614         a = a * (1 - (1 - waypointsprite_distancefadealpha) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
615         t = t * (1 - (1 - waypointsprite_distancefadescale) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
616     }
617     if (edgedistance_min < waypointsprite_edgefadedistance) {
618         a = a * (1 - (1 - waypointsprite_edgefadealpha) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
619         t = t * (1 - (1 - waypointsprite_edgefadescale) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
620     }
621     if (crosshairdistance < waypointsprite_crosshairfadedistance) {
622         a = a * (1 - (1 - waypointsprite_crosshairfadealpha) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
623         t = t * (1 - (1 - waypointsprite_crosshairfadescale) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
624     }
625
626     if (self.build_finished)
627     {
628         if (time < self.build_finished + 0.25)
629         {
630             if (time < self.build_started)
631                 self.health = self.build_starthealth;
632             else if (time < self.build_finished)
633                 self.health = (time - self.build_started) / (self.build_finished - self.build_started) * (1 - self.build_starthealth) + self.build_starthealth;
634             else
635                 self.health = 1;
636         }
637         else
638             self.health = -1;
639     }
640
641     o = drawspritearrow(o, ang, rgb, a, SPRITE_ARROW_SCALE * t);
642
643     string txt;
644     if (autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
645         txt = _("Spam");
646     else
647         txt = spritelookuptext(spriteimage);
648     if (self.helpme && time < self.helpme)
649         txt = sprintf(_("%s needing help!"), txt);
650     if (autocvar_g_waypointsprite_uppercase)
651         txt = strtoupper(txt);
652
653     draw_beginBoldFont();
654     if (self.health >= 0)
655     {
656         o = drawspritetext(o, ang, (SPRITE_HEALTHBAR_WIDTH + 2 * SPRITE_HEALTHBAR_BORDER) * t, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
657
658         float align, marg;
659         if (self.build_finished)
660             align = 0.5;
661         else
662             align = 0;
663         if (cos(ang) > 0)
664             marg = -(SPRITE_HEALTHBAR_MARGIN + SPRITE_HEALTHBAR_HEIGHT + 2 * SPRITE_HEALTHBAR_BORDER) * t - 0.5 * waypointsprite_fontsize;
665         else
666             marg = SPRITE_HEALTHBAR_MARGIN * t + 0.5 * waypointsprite_fontsize;
667         drawhealthbar(
668                 o,
669                 0,
670                 self.health,
671                 '0 0 0',
672                 '0 0 0',
673                 SPRITE_HEALTHBAR_WIDTH * t,
674                 SPRITE_HEALTHBAR_HEIGHT * t,
675                 marg,
676                 SPRITE_HEALTHBAR_BORDER * t,
677                 align,
678                 rgb,
679                 a * SPRITE_HEALTHBAR_BORDERALPHA,
680                 rgb,
681                 a * SPRITE_HEALTHBAR_HEALTHALPHA,
682                 DRAWFLAG_NORMAL
683                  );
684     }
685     else
686     {
687         o = drawspritetext(o, ang, 0, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
688     }
689     draw_endBoldFont();
690 }
691
692 void WaypointSprite_Load_Frames(string ext)
693 {
694     int dh = search_begin(strcat("models/sprites/*_frame*", ext), false, false);
695     if (dh < 0) return;
696     int ext_len = strlen(ext);
697     int n = search_getsize(dh);
698     for (int i = 0; i < n; ++i)
699     {
700         string s = search_getfilename(dh, i);
701         s = substring(s, 15, strlen(s) - 15 - ext_len); // strip models/sprites/ and extension
702
703         int o = strstrofs(s, "_frame", 0);
704         string sname = strcat("/spriteframes/", substring(s, 0, o));
705         string sframes = substring(s, o + 6, strlen(s) - o - 6);
706         int f = stof(sframes) + 1;
707         db_put(tempdb, sname, ftos(max(f, stof(db_get(tempdb, sname)))));
708     }
709     search_end(dh);
710 }
711
712 void WaypointSprite_Load();
713 STATIC_INIT(WaypointSprite_Load) {
714     WaypointSprite_Load();
715     WaypointSprite_Load_Frames(".tga");
716     WaypointSprite_Load_Frames(".jpg");
717 }
718 void WaypointSprite_Load()
719 {
720     waypointsprite_fadedistance = vlen(mi_scale);
721     waypointsprite_normdistance = autocvar_g_waypointsprite_normdistance;
722     waypointsprite_minscale = autocvar_g_waypointsprite_minscale;
723     waypointsprite_minalpha = autocvar_g_waypointsprite_minalpha;
724     waypointsprite_distancealphaexponent = autocvar_g_waypointsprite_distancealphaexponent;
725     waypointsprite_timealphaexponent = autocvar_g_waypointsprite_timealphaexponent;
726     waypointsprite_scale = autocvar_g_waypointsprite_scale;
727     waypointsprite_fontsize = autocvar_g_waypointsprite_fontsize;
728     waypointsprite_edgefadealpha = autocvar_g_waypointsprite_edgefadealpha;
729     waypointsprite_edgefadescale = autocvar_g_waypointsprite_edgefadescale;
730     waypointsprite_edgefadedistance = autocvar_g_waypointsprite_edgefadedistance;
731     waypointsprite_edgeoffset_bottom = autocvar_g_waypointsprite_edgeoffset_bottom;
732     waypointsprite_edgeoffset_left = autocvar_g_waypointsprite_edgeoffset_left;
733     waypointsprite_edgeoffset_right = autocvar_g_waypointsprite_edgeoffset_right;
734     waypointsprite_edgeoffset_top = autocvar_g_waypointsprite_edgeoffset_top;
735     waypointsprite_crosshairfadealpha = autocvar_g_waypointsprite_crosshairfadealpha;
736     waypointsprite_crosshairfadescale = autocvar_g_waypointsprite_crosshairfadescale;
737     waypointsprite_crosshairfadedistance = autocvar_g_waypointsprite_crosshairfadedistance;
738     waypointsprite_distancefadealpha = autocvar_g_waypointsprite_distancefadealpha;
739     waypointsprite_distancefadescale = autocvar_g_waypointsprite_distancefadescale;
740     waypointsprite_distancefadedistance = waypointsprite_fadedistance * autocvar_g_waypointsprite_distancefadedistancemultiplier;
741     waypointsprite_alpha = autocvar_g_waypointsprite_alpha * (1 - autocvar__menu_alpha);
742
743     waypointsprite_count = waypointsprite_newcount;
744     waypointsprite_newcount = 0;
745 }
746 #endif
747
748 #ifdef SVQC
749 void WaypointSprite_UpdateSprites(entity e, entity _m1, entity _m2, entity _m3)
750 {
751     string m1 = _m1.netname;
752     string m2 = _m2.netname;
753     string m3 = _m3.netname;
754     if (m1 != e.model1)
755     {
756         e.model1 = m1;
757         e.SendFlags |= 2;
758     }
759     if (m2 != e.model2)
760     {
761         e.model2 = m2;
762         e.SendFlags |= 4;
763     }
764     if (m3 != e.model3)
765     {
766         e.model3 = m3;
767         e.SendFlags |= 8;
768     }
769 }
770
771 void WaypointSprite_UpdateHealth(entity e, float f)
772 {
773     f = bound(0, f, e.max_health);
774     if (f != e.health || e.pain_finished)
775     {
776         e.health = f;
777         e.pain_finished = 0;
778         e.SendFlags |= 0x80;
779     }
780 }
781
782 void WaypointSprite_UpdateMaxHealth(entity e, float f)
783 {
784     if (f != e.max_health || e.pain_finished)
785     {
786         e.max_health = f;
787         e.pain_finished = 0;
788         e.SendFlags |= 0x80;
789     }
790 }
791
792 void WaypointSprite_UpdateBuildFinished(entity e, float f)
793 {
794     if (f != e.pain_finished || e.max_health)
795     {
796         e.max_health = 0;
797         e.pain_finished = f;
798         e.SendFlags |= 0x80;
799     }
800 }
801
802 void WaypointSprite_UpdateOrigin(entity e, vector o)
803 {
804     if (o != e.origin)
805     {
806         setorigin(e, o);
807         e.SendFlags |= 64;
808     }
809 }
810
811 void WaypointSprite_UpdateRule(entity e, float t, float r)
812 {
813     // no check, as this is never called without doing an actual change (usually only once)
814     e.rule = r;
815     e.team = t;
816     e.SendFlags |= 1;
817 }
818
819 void WaypointSprite_UpdateTeamRadar(entity e, entity icon, vector col)
820 {
821     // no check, as this is never called without doing an actual change (usually only once)
822     int i = icon.m_id;
823     e.cnt = (e.cnt & BIT(7)) | (i & BITS(7));
824     e.colormod = col;
825     e.SendFlags |= 32;
826 }
827
828 void WaypointSprite_Ping(entity e)
829 {
830     // anti spam
831     if (time < e.waypointsprite_pingtime) return;
832     e.waypointsprite_pingtime = time + 0.3;
833     // ALWAYS sends (this causes a radar circle), thus no check
834     e.cnt |= BIT(7);
835     e.SendFlags |= 32;
836 }
837
838 void WaypointSprite_HelpMePing(entity e)
839 {
840     WaypointSprite_Ping(e);
841     e.waypointsprite_helpmetime = time + waypointsprite_deployed_lifetime;
842     e.SendFlags |= 32;
843 }
844
845 void WaypointSprite_FadeOutIn(entity e, float t)
846 {
847     if (!e.fade_time)
848     {
849         e.fade_time = t;
850         e.teleport_time = time + t;
851     }
852     else if (t < (e.teleport_time - time))
853     {
854         // accelerate the waypoint's dying
855         // ensure:
856         //   (e.teleport_time - time) / wp.fade_time stays
857         //   e.teleport_time = time + fadetime
858         float current_fadetime;
859         current_fadetime = e.teleport_time - time;
860         e.teleport_time = time + t;
861         e.fade_time = e.fade_time * t / current_fadetime;
862     }
863
864     e.SendFlags |= 16;
865 }
866
867 void WaypointSprite_Init()
868 {
869     waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
870     waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
871     waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
872 }
873
874 void WaypointSprite_InitClient(entity e)
875 {
876 }
877
878 void WaypointSprite_Kill(entity wp)
879 {
880     if (!wp) return;
881     if (wp.owner) wp.owner.(wp.owned_by_field) = world;
882     remove(wp);
883 }
884
885 void WaypointSprite_Disown(entity wp, float fadetime)
886 {
887     if (!wp) return;
888     if (wp.classname != "sprite_waypoint")
889     {
890         backtrace("Trying to disown a non-waypointsprite");
891         return;
892     }
893     if (wp.owner)
894     {
895         if (wp.exteriormodeltoclient == wp.owner)
896             wp.exteriormodeltoclient = world;
897         wp.owner.(wp.owned_by_field) = world;
898         wp.owner = world;
899
900         WaypointSprite_FadeOutIn(wp, fadetime);
901     }
902 }
903
904 void WaypointSprite_Think()
905 {SELFPARAM();
906     bool doremove = false;
907
908     if (self.fade_time && time >= self.teleport_time)
909     {
910         doremove = true;
911     }
912
913     if (self.exteriormodeltoclient)
914         WaypointSprite_UpdateOrigin(self, self.exteriormodeltoclient.origin + self.view_ofs);
915
916     if (doremove)
917         WaypointSprite_Kill(self);
918     else
919         self.nextthink = time; // WHY?!?
920 }
921
922 float WaypointSprite_visible_for_player(entity e)
923 {SELFPARAM();
924     // personal waypoints
925     if (self.enemy && self.enemy != e)
926         return false;
927
928     // team waypoints
929     if (self.rule == SPRITERULE_SPECTATOR)
930     {
931         if (!autocvar_sv_itemstime)
932             return false;
933         if (!warmup_stage && IS_PLAYER(e))
934             return false;
935     }
936     else if (self.team && self.rule == SPRITERULE_DEFAULT)
937     {
938         if (self.team != e.team)
939             return false;
940         if (!IS_PLAYER(e))
941             return false;
942     }
943
944     return true;
945 }
946
947 entity WaypointSprite_getviewentity(entity e)
948 {
949     if (IS_SPEC(e)) e = e.enemy;
950     /* TODO idea (check this breaks nothing)
951     else if (e.classname == "observer")
952         e = world;
953     */
954     return e;
955 }
956
957 float WaypointSprite_isteammate(entity e, entity e2)
958 {
959     if (teamplay)
960         return e2.team == e.team;
961     return e2 == e;
962 }
963
964 float WaypointSprite_Customize()
965 {SELFPARAM();
966     // this is not in SendEntity because it shall run every frame, not just every update
967
968     // make spectators see what the player would see
969     entity e = WaypointSprite_getviewentity(other);
970
971     if (MUTATOR_CALLHOOK(CustomizeWaypoint, self, other))
972         return false;
973
974     return self.waypointsprite_visible_for_player(e);
975 }
976
977 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
978
979 void WaypointSprite_Reset()
980 {SELFPARAM();
981     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
982
983     if (self.fade_time) // was there before: || g_keyhunt, do we really need this?
984         WaypointSprite_Kill(self);
985 }
986
987 entity WaypointSprite_Spawn(
988     entity spr, // sprite
989     float _lifetime, float maxdistance, // lifetime, max distance
990     entity ref, vector ofs, // position
991     entity showto, float t, // show to whom? Use a flag to indicate a team
992     entity own, .entity ownfield, // remove when own gets killed
993     float hideable, // true when it should be controlled by cl_hidewaypoints
994     entity icon // initial icon
995 )
996 {
997     entity wp = new(sprite_waypoint);
998     wp.teleport_time = time + _lifetime;
999     wp.fade_time = _lifetime;
1000     wp.exteriormodeltoclient = ref;
1001     if (ref)
1002     {
1003         wp.view_ofs = ofs;
1004         setorigin(wp, ref.origin + ofs);
1005     }
1006     else
1007         setorigin(wp, ofs);
1008     wp.enemy = showto;
1009     wp.team = t;
1010     wp.owner = own;
1011     wp.currentammo = hideable;
1012     if (own)
1013     {
1014         if (own.(ownfield))
1015             remove(own.(ownfield));
1016         own.(ownfield) = wp;
1017         wp.owned_by_field = ownfield;
1018     }
1019     wp.fade_rate = maxdistance;
1020     wp.think = WaypointSprite_Think;
1021     wp.nextthink = time;
1022     wp.model1 = spr.netname;
1023     wp.customizeentityforclient = WaypointSprite_Customize;
1024     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1025     wp.reset2 = WaypointSprite_Reset;
1026     wp.cnt = icon.m_id;
1027     wp.colormod = spr.m_color;
1028     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1029     return wp;
1030 }
1031
1032 entity WaypointSprite_SpawnFixed(
1033     entity spr,
1034     vector ofs,
1035     entity own,
1036     .entity ownfield,
1037     entity icon // initial icon
1038 )
1039 {
1040     return WaypointSprite_Spawn(spr, 0, 0, world, ofs, world, 0, own, ownfield, true, icon);
1041 }
1042
1043 entity WaypointSprite_DeployFixed(
1044     entity spr,
1045     float limited_range,
1046     vector ofs,
1047     entity icon // initial icon
1048 )
1049 {SELFPARAM();
1050     float t;
1051     if (teamplay)
1052         t = self.team;
1053     else
1054         t = 0;
1055     float maxdistance;
1056     if (limited_range)
1057         maxdistance = waypointsprite_limitedrange;
1058     else
1059         maxdistance = 0;
1060     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, world, ofs, world, t, self, waypointsprite_deployed_fixed, false, icon);
1061 }
1062
1063 entity WaypointSprite_DeployPersonal(
1064     entity spr,
1065     vector ofs,
1066     entity icon // initial icon
1067 )
1068 {SELFPARAM();
1069     return WaypointSprite_Spawn(spr, 0, 0, world, ofs, world, 0, self, waypointsprite_deployed_personal, false, icon);
1070 }
1071
1072 entity WaypointSprite_Attach(
1073     entity spr,
1074     float limited_range,
1075     entity icon // initial icon
1076 )
1077 {SELFPARAM();
1078     float t;
1079     if (self.waypointsprite_attachedforcarrier)
1080         return world; // can't attach to FC
1081     if (teamplay)
1082         t = self.team;
1083     else
1084         t = 0;
1085     float maxdistance;
1086     if (limited_range)
1087         maxdistance = waypointsprite_limitedrange;
1088     else
1089         maxdistance = 0;
1090     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, self, '0 0 64', world, t, self, waypointsprite_attached, false, icon);
1091 }
1092
1093 entity WaypointSprite_AttachCarrier(
1094     entity spr,
1095     entity carrier,
1096     entity icon // initial icon and color
1097 )
1098 {
1099     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1100     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', world, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1101     if (e)
1102     {
1103         WaypointSprite_UpdateMaxHealth(e, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
1104         WaypointSprite_UpdateHealth(e, '1 0 0' * healtharmor_maxdamage(carrier.health, carrier.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
1105     }
1106     return e;
1107 }
1108
1109 void WaypointSprite_DetachCarrier(entity carrier)
1110 {
1111     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1112 }
1113
1114 void WaypointSprite_ClearPersonal()
1115 {SELFPARAM();
1116     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1117 }
1118
1119 void WaypointSprite_ClearOwned()
1120 {SELFPARAM();
1121     WaypointSprite_Kill(self.waypointsprite_deployed_fixed);
1122     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1123     WaypointSprite_Kill(self.waypointsprite_attached);
1124 }
1125
1126 void WaypointSprite_PlayerDead()
1127 {SELFPARAM();
1128     WaypointSprite_Disown(self.waypointsprite_attached, waypointsprite_deadlifetime);
1129     WaypointSprite_DetachCarrier(self);
1130 }
1131
1132 void WaypointSprite_PlayerGone()
1133 {SELFPARAM();
1134     WaypointSprite_Disown(self.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1135     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1136     WaypointSprite_Disown(self.waypointsprite_attached, waypointsprite_deadlifetime);
1137     WaypointSprite_DetachCarrier(self);
1138 }
1139 #endif
1140 #endif