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