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