]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/waypoints/waypointsprites.qc
d3a9806e0f05a808f8addb5384fe08b4e4ec7528
[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, (GetResource(this, RES_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 = bound(0, (this.waypointsprite_helpmetime - time) / 0.1, 255);
86             WriteByte(MSG_ENTITY, dt);
87         }
88         else
89             WriteByte(MSG_ENTITY, 0);
90     }
91
92     return true;
93 }
94 #endif
95
96 #ifdef CSQC
97 void Ent_WaypointSprite(entity this, bool isnew);
98 NET_HANDLE(waypointsprites, bool isnew) {
99     Ent_WaypointSprite(this, isnew);
100     return true;
101 }
102
103 void Ent_RemoveWaypointSprite(entity this)
104 {
105     strfree(this.netname);
106     strfree(this.netname2);
107     strfree(this.netname3);
108 }
109
110 /** flags origin [team displayrule] [spritename] [spritename2] [spritename3] [lifetime maxdistance hideable] */
111 void Ent_WaypointSprite(entity this, bool isnew)
112 {
113     int sendflags = ReadByte();
114     this.wp_extra = ReadByte();
115
116     if (!this.spawntime)
117         this.spawntime = time;
118
119     this.draw2d = Draw_WaypointSprite;
120     if (isnew) {
121                 IL_PUSH(g_drawables_2d, this);
122                 IL_PUSH(g_radaricons, this);
123     }
124
125     InterpolateOrigin_Undo(this);
126     this.iflags |= IFLAG_ORIGIN;
127
128     if (sendflags & 0x80)
129     {
130         int t = ReadByte();
131         if (t < 192)
132         {
133             SetResourceExplicit(this, RES_HEALTH, t / 191.0);
134             this.build_finished = 0;
135         }
136         else
137         {
138             t = (t - 192) * 256 + ReadByte();
139             this.build_started = servertime;
140             if (this.build_finished)
141                 this.build_starthealth = bound(0, GetResource(this, RES_HEALTH), 1);
142             else
143                 this.build_starthealth = 0;
144             this.build_finished = servertime + t / 32;
145         }
146     }
147     else
148     {
149         SetResourceExplicit(this, RES_HEALTH, -1);
150         this.build_finished = 0;
151     }
152
153     if (sendflags & 64)
154     {
155         // unfortunately, this needs to be exact (for the 3D display)
156         this.origin = ReadVector();
157         setorigin(this, this.origin);
158     }
159
160     if (sendflags & 1)
161     {
162         this.team = ReadByte();
163         this.rule = ReadByte();
164     }
165
166     if (sendflags & 2)
167     {
168         strcpy(this.netname, ReadString());
169     }
170
171     if (sendflags & 4)
172     {
173         strcpy(this.netname2, ReadString());
174     }
175
176     if (sendflags & 8)
177     {
178         strcpy(this.netname3, ReadString());
179     }
180
181     if (sendflags & 16)
182     {
183         this.lifetime = ReadCoord();
184         this.fadetime = ReadCoord();
185         this.maxdistance = ReadShort();
186         this.hideflags = ReadByte();
187     }
188
189     if (sendflags & 32)
190     {
191         int f = ReadByte();
192         this.teamradar_icon = f & BITS(7);
193         if (f & BIT(7))
194         {
195             this.(teamradar_times[this.teamradar_time_index]) = time;
196             this.teamradar_time_index = (this.teamradar_time_index + 1) % MAX_TEAMRADAR_TIMES;
197         }
198         this.teamradar_color_x = ReadByte() / 255.0;
199         this.teamradar_color_y = ReadByte() / 255.0;
200         this.teamradar_color_z = ReadByte() / 255.0;
201         this.helpme = ReadByte() * 0.1;
202         if (this.helpme > 0)
203             this.helpme += servertime;
204     }
205
206     InterpolateOrigin_Note(this);
207
208     this.entremove = Ent_RemoveWaypointSprite;
209 }
210 #endif
211
212 #ifdef CSQC
213 float spritelookupblinkvalue(entity this, string s)
214 {
215     if (s == WP_Weapon.netname) {
216         if (REGISTRY_GET(Weapons, this.wp_extra).spawnflags & WEP_FLAG_SUPERWEAPON)
217             return 2;
218     }
219     if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_waypointblink;
220     if(s == WP_FlagReturn.netname) return 2;
221
222     return 1;
223 }
224
225 vector spritelookupcolor(entity this, string s, vector def)
226 {
227     if (s == WP_Weapon.netname  || s == RADARICON_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).wpcolor;
228     if (s == WP_Item.netname    || s == RADARICON_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_color;
229     if (MUTATOR_CALLHOOK(WP_Format, this, s))
230     {
231         return M_ARGV(2, vector);
232     }
233     return def;
234 }
235
236 string spritelookuptext(entity this, string s)
237 {
238         if(autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
239                 return "Spam"; // no need to translate this debug string
240     if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
241     if (s == WP_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).m_name;
242     if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_waypoint;
243     if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).monster_name;
244     if (MUTATOR_CALLHOOK(WP_Format, this, s))
245     {
246         return M_ARGV(3, string);
247     }
248
249     // need to loop, as our netname could be one of three
250     FOREACH(Waypoints, it.netname == s, {
251         return it.m_name;
252     });
253
254     return s;
255 }
256
257 string spritelookupicon(entity this, string s)
258 {
259     // TODO: needs icons! //if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
260     if (s == WP_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).model2;
261     if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_icon;
262     if (s == WP_Vehicle.netname) return REGISTRY_GET(Vehicles, this.wp_extra).m_icon;
263     //if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).m_icon;
264     if (MUTATOR_CALLHOOK(WP_Format, this, s))
265     {
266         return M_ARGV(4, string);
267     }
268
269     // need to loop, as our netname could be one of three
270     FOREACH(Waypoints, it.netname == s, {
271         return it.m_icon;
272     });
273
274     return s;
275 }
276 #endif
277
278 #ifdef CSQC
279 void drawrotpic(vector org, float rot, string pic, vector sz, vector hotspot, vector rgb, float a, float f)
280 {
281     vector v1, v2, v3, v4;
282
283     hotspot = -1 * hotspot;
284
285     // hotspot-relative coordinates of the corners
286     v1 = hotspot;
287     v2 = hotspot + '1 0 0' * sz.x;
288     v3 = hotspot + '1 0 0' * sz.x + '0 1 0' * sz.y;
289     v4 = hotspot                  + '0 1 0' * sz.y;
290
291     // rotate them, and make them absolute
292     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
293     v1 = Rotate(v1, rot) + org;
294     v2 = Rotate(v2, rot) + org;
295     v3 = Rotate(v3, rot) + org;
296     v4 = Rotate(v4, rot) + org;
297
298     // draw them
299     R_BeginPolygon(pic, f, true);
300     R_PolygonVertex(v1, '0 0 0', rgb, a);
301     R_PolygonVertex(v2, '1 0 0', rgb, a);
302     R_PolygonVertex(v3, '1 1 0', rgb, a);
303     R_PolygonVertex(v4, '0 1 0', rgb, a);
304     R_EndPolygon();
305 }
306
307 void drawquad(vector o, vector ri, vector up, string pic, vector rgb, float a, float f)
308 {
309     R_BeginPolygon(pic, f, true);
310     R_PolygonVertex(o, '0 0 0', rgb, a);
311     R_PolygonVertex(o + ri, '1 0 0', rgb, a);
312     R_PolygonVertex(o + up + ri, '1 1 0', rgb, a);
313     R_PolygonVertex(o + up, '0 1 0', rgb, a);
314     R_EndPolygon();
315 }
316
317 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)
318 {
319     vector o, ri, up;
320     float owidth; // outer width
321
322     hotspot = -1 * hotspot;
323
324     // hotspot-relative coordinates of the healthbar corners
325     o = hotspot;
326     ri = '1 0 0';
327     up = '0 1 0';
328
329     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
330     o = Rotate(o, rot) + org;
331     ri = Rotate(ri, rot);
332     up = Rotate(up, rot);
333
334     owidth = width + 2 * border;
335     o = o - up * (margin + border + theheight) + ri * (sz.x - owidth) * 0.5;
336
337     drawquad(o - up * border,                               ri * owidth,    up * border,    "", rgb,  a,  f);
338     drawquad(o + up * theheight,                            ri * owidth,    up * border,    "", rgb,  a,  f);
339     drawquad(o,                                             ri * border,    up * theheight, "", rgb,  a,  f);
340     drawquad(o + ri * (owidth - border),                    ri * border,    up * theheight, "", rgb,  a,  f);
341     drawquad(o + ri * (border + align * ((1 - h) * width)), ri * width * h, up * theheight, "", hrgb, ha, f);
342 }
343
344 // returns location of sprite text
345 vector drawspritearrow(vector o, float ang, vector rgb, float a, float t)
346 {
347     float size   = 9.0 * t;
348     float border = 1.5 * t;
349     float margin = 4.0 * t;
350
351     float borderDiag = border * M_SQRT2;
352     vector arrowX  = eX * size;
353     vector arrowY  = eY * (size+borderDiag);
354     vector borderX = eX * (size+borderDiag);
355     vector borderY = eY * (size+borderDiag+border);
356
357     R_BeginPolygon("", DRAWFLAG_NORMAL, true);
358     R_PolygonVertex(o,                                  '0 0 0', '0 0 0', a);
359     R_PolygonVertex(o + Rotate(arrowY  - borderX, ang), '0 0 0', '0 0 0', a);
360     R_PolygonVertex(o + Rotate(borderY - borderX, ang), '0 0 0', '0 0 0', a);
361     R_PolygonVertex(o + Rotate(borderY + borderX, ang), '0 0 0', '0 0 0', a);
362     R_PolygonVertex(o + Rotate(arrowY  + borderX, ang), '0 0 0', '0 0 0', a);
363     R_EndPolygon();
364
365     R_BeginPolygon("", DRAWFLAG_ADDITIVE, true);
366     R_PolygonVertex(o + Rotate(eY * borderDiag, ang), '0 0 0', rgb, a);
367     R_PolygonVertex(o + Rotate(arrowY - arrowX, ang), '0 0 0', rgb, a);
368     R_PolygonVertex(o + Rotate(arrowY + arrowX, ang), '0 0 0', rgb, a);
369     R_EndPolygon();
370
371     return o + Rotate(eY * (borderDiag+size+margin), ang);
372 }
373
374 // returns location of sprite healthbar
375 vector drawsprite_TextOrIcon(bool is_text, vector o, float ang, float minwidth, vector rgb, float a, vector sz, string str)
376 {
377     float algnx, algny;
378     float sw, w, h;
379     float aspect, sa, ca;
380
381     if (is_text)
382         sw = stringwidth(str, false, sz);
383     else
384         sw = sz.x;
385
386     if (sw > minwidth)
387         w = sw;
388     else
389         w = minwidth;
390     h = sz.y;
391
392     // how do corners work?
393     aspect = vid_conwidth / vid_conheight;
394     sa = sin(ang);
395     ca = cos(ang) * aspect;
396     if (fabs(sa) > fabs(ca))
397     {
398         algnx = (sa < 0);
399         float f = fabs(sa);
400         algny = 0.5 - 0.5 * (f ? (ca / f) : 0);
401     }
402     else
403     {
404         float f = fabs(ca);
405         algnx = 0.5 - 0.5 * (f ? (sa / f) : 0);
406         algny = (ca < 0);
407     }
408
409     // align
410     o.x -= w * algnx;
411     o.y -= h * algny;
412
413     // we want to be onscreen
414     if (o.x < 0)
415         o.x = 0;
416     if (o.y < 0)
417         o.y = 0;
418     if (o.x > vid_conwidth - w)
419         o.x = vid_conwidth - w;
420     if (o.y > vid_conheight - h)
421         o.y = vid_conheight - h;
422
423     o.x += 0.5 * (w - sw);
424
425     if (is_text)
426         drawstring(o, str, sz, rgb, a, DRAWFLAG_NORMAL);
427     else
428         drawpic(o, str, sz, rgb, a, DRAWFLAG_NORMAL);
429
430     o.x += 0.5 * sw;
431     o.y += 0.5 * h;
432
433     return o;
434 }
435
436 vector fixrgbexcess_move(vector rgb, vector src, vector dst)
437 {
438     vector yvec = '0.299 0.587 0.114';
439     return rgb + dst * ((src * yvec) / (dst * yvec)) * ((rgb - '1 1 1') * src);
440 }
441
442 vector fixrgbexcess(vector rgb)
443 {
444     if (rgb.x > 1) {
445         rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 1');
446         if (rgb.y > 1) {
447             rgb = fixrgbexcess_move(rgb, '0 1 0', '0 0 1');
448             if (rgb.z > 1) rgb.z = 1;
449         } else if (rgb.z > 1) {
450             rgb = fixrgbexcess_move(rgb, '0 0 1', '0 1 0');
451             if (rgb.y > 1) rgb.y = 1;
452         }
453     } else if (rgb.y > 1) {
454         rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 1');
455         if (rgb.x > 1) {
456             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 0 1');
457             if (rgb.z > 1) rgb.z = 1;
458         } else if (rgb.z > 1) {
459             rgb = fixrgbexcess_move(rgb, '0 0 1', '1 0 0');
460             if (rgb.x > 1) rgb.x = 1;
461         }
462     } else if (rgb.z > 1) {
463         rgb = fixrgbexcess_move(rgb, '0 0 1', '1 1 0');
464         if (rgb.x > 1) {
465             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 0');
466             if (rgb.y > 1) rgb.y = 1;
467         } else if (rgb.y > 1) {
468             rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 0');
469             if (rgb.x > 1) rgb.x = 1;
470         }
471     }
472     return rgb;
473 }
474
475 void Draw_WaypointSprite(entity this)
476 {
477     if (this.lifetime > 0)
478         this.alpha = (bound(0, (this.fadetime - time) / this.lifetime, 1) ** waypointsprite_timealphaexponent);
479     else
480         this.alpha = 1;
481
482     if (this.hideflags & 2)
483         return; // radar only
484
485     if (autocvar_cl_hidewaypoints >= 2)
486         return;
487
488     if ((this.hideflags & 1) && autocvar_cl_hidewaypoints)
489         return; // fixed waypoint
490
491     InterpolateOrigin_Do(this);
492
493     float t = entcs_GetTeam(player_localnum) + 1;
494     string spriteimage = "";
495
496     // choose the sprite
497     switch (this.rule)
498     {
499         case SPRITERULE_SPECTATOR:
500             if (!(
501                 (autocvar_g_waypointsprite_itemstime == 1 && t == NUM_SPECTATOR + 1)
502             ||  (autocvar_g_waypointsprite_itemstime == 2 && (t == NUM_SPECTATOR + 1 || warmup_stage || STAT(ITEMSTIME) == 2))
503                 ))
504                 return;
505             spriteimage = this.netname;
506             break;
507         case SPRITERULE_DEFAULT:
508             if (this.team)
509             {
510                 if (this.team == t)
511                     spriteimage = this.netname;
512                 else
513                     spriteimage = "";
514             }
515             else
516                 spriteimage = this.netname;
517             break;
518         case SPRITERULE_TEAMPLAY:
519             if (t == NUM_SPECTATOR + 1)
520                 spriteimage = this.netname3;
521             else if (this.team == t)
522                 spriteimage = this.netname2;
523             else
524                 spriteimage = this.netname;
525             break;
526         default:
527             error("Invalid waypointsprite rule!");
528             break;
529     }
530
531     if (spriteimage == "")
532         return;
533
534     ++waypointsprite_newcount;
535
536     float dist = vlen(this.origin - view_origin);
537     float a = this.alpha * autocvar_hud_panel_fg_alpha;
538
539     if(this.maxdistance > 0)
540     {
541         // restrict maximum normal distance to the waypoint's maximum distance to prevent exploiting cvars
542         float maxnormdistance = bound(0, waypointsprite_normdistance, this.maxdistance - 1);
543         a *= (bound(0, (this.maxdistance - dist) / (this.maxdistance - maxnormdistance), 1) ** waypointsprite_distancealphaexponent);
544     }
545
546     vector rgb = spritelookupcolor(this, spriteimage, this.teamradar_color);
547     if (rgb == '0 0 0')
548     {
549         this.teamradar_color = '1 0 1';
550         LOG_INFOF("WARNING: sprite of name %s has no color, using pink so you notice it", spriteimage);
551     }
552
553     float health_val = GetResource(this, RES_HEALTH);
554     float blink_time = (health_val >= 0) ? (health_val * 10) : time;
555     if (blink_time - floor(blink_time) > 0.5)
556     {
557         if (this.helpme && time < this.helpme)
558             a *= SPRITE_HELPME_BLINK;
559         else if (!this.lifetime) // 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                 SetResourceExplicit(this, RES_HEALTH, this.build_starthealth);
658             else if (time < this.build_finished)
659                 SetResourceExplicit(this, RES_HEALTH, (time - this.build_started) / (this.build_finished - this.build_started) * (1 - this.build_starthealth) + this.build_starthealth);
660             else
661                 SetResourceExplicit(this, RES_HEALTH, 1);
662         }
663         else
664             SetResourceExplicit(this, RES_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 (GetResource(this, RES_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                 GetResource(this, RES_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     float step = e.max_health / 40;
835     if ((floor(f / step) != floor(GetResource(e, RES_HEALTH) / step)) || e.pain_finished)
836     {
837         SetResourceExplicit(e, RES_HEALTH, f);
838         e.pain_finished = 0;
839         e.SendFlags |= 0x80;
840     }
841 }
842
843 void WaypointSprite_UpdateMaxHealth(entity e, float f)
844 {
845     if (f != e.max_health || e.pain_finished)
846     {
847         e.max_health = f;
848         e.pain_finished = 0;
849         e.SendFlags |= 0x80;
850     }
851 }
852
853 void WaypointSprite_UpdateBuildFinished(entity e, float f)
854 {
855     if (f != e.pain_finished || e.max_health)
856     {
857         e.max_health = 0;
858         e.pain_finished = f;
859         e.SendFlags |= 0x80;
860     }
861 }
862
863 void WaypointSprite_UpdateOrigin(entity e, vector o)
864 {
865     if (o != e.origin)
866     {
867         setorigin(e, o);
868         e.SendFlags |= 64;
869     }
870 }
871
872 void WaypointSprite_UpdateRule(entity e, float t, float r)
873 {
874     // no check, as this is never called without doing an actual change (usually only once)
875     e.rule = r;
876     e.team = t;
877     e.SendFlags |= 1;
878 }
879
880 void WaypointSprite_UpdateTeamRadar(entity e, entity icon, vector col)
881 {
882     // no check, as this is never called without doing an actual change (usually only once)
883     int i = icon.m_id;
884     int new_cnt = (e.cnt & BIT(7)) | (i & BITS(7));
885     if (new_cnt != e.cnt || col != e.colormod)
886     {
887         e.cnt = new_cnt;
888         e.colormod = col;
889         e.SendFlags |= 32;
890     }
891 }
892
893 void WaypointSprite_Ping(entity e)
894 {
895     // anti spam
896     if (time < e.waypointsprite_pingtime) return;
897     e.waypointsprite_pingtime = time + 0.3;
898     // ALWAYS sends (this causes a radar circle), thus no check
899     e.cnt |= BIT(7);
900     e.SendFlags |= 32;
901 }
902
903 void WaypointSprite_HelpMePing(entity e)
904 {
905     WaypointSprite_Ping(e);
906     e.waypointsprite_helpmetime = time + waypointsprite_deployed_lifetime;
907     e.SendFlags |= 32;
908 }
909
910 void WaypointSprite_FadeOutIn(entity e, float t)
911 {
912     if (!e.fade_time)
913     {
914         e.fade_time = t;
915         e.teleport_time = time + t;
916     }
917     else if (t < (e.teleport_time - time))
918     {
919         // accelerate the waypoint's dying
920         // ensure:
921         //   (e.teleport_time - time) / wp.fade_time stays
922         //   e.teleport_time = time + fadetime
923         float current_fadetime = e.teleport_time - time;
924         e.teleport_time = time + t;
925         if (e.fade_time < 0)
926                 e.fade_time = -e.fade_time;
927         e.fade_time = e.fade_time * t / current_fadetime;
928     }
929
930     e.SendFlags |= 16;
931 }
932
933 void WaypointSprite_Init()
934 {
935     waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
936     waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
937     waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
938 }
939
940 void WaypointSprite_Kill(entity wp)
941 {
942     if (!wp) return;
943     if (wp.owner) wp.owner.(wp.owned_by_field) = NULL;
944     delete(wp);
945 }
946
947 void WaypointSprite_Disown(entity wp, float fadetime)
948 {
949     if (!wp) return;
950     if (wp.classname != "sprite_waypoint")
951     {
952         backtrace("Trying to disown a non-waypointsprite");
953         return;
954     }
955     if (wp.owner)
956     {
957         if (wp.exteriormodeltoclient == wp.owner)
958             wp.exteriormodeltoclient = NULL;
959         wp.owner.(wp.owned_by_field) = NULL;
960         wp.owner = NULL;
961
962         WaypointSprite_FadeOutIn(wp, fadetime);
963     }
964 }
965
966 void WaypointSprite_Think(entity this)
967 {
968     bool doremove = false;
969
970     if (this.fade_time && time >= this.teleport_time)
971     {
972         doremove = true;
973     }
974
975     if (this.exteriormodeltoclient)
976         WaypointSprite_UpdateOrigin(this, this.exteriormodeltoclient.origin + this.view_ofs);
977
978     if (doremove)
979         WaypointSprite_Kill(this);
980     else
981         this.nextthink = time; // WHY?!?
982 }
983
984 bool WaypointSprite_visible_for_player(entity this, entity player, entity view)
985 {
986     // personal waypoints
987     if (this.enemy && this.enemy != view)
988         return false;
989
990     // team waypoints
991     if (this.rule == SPRITERULE_SPECTATOR)
992     {
993         if (!autocvar_sv_itemstime)
994             return false;
995         if (!warmup_stage && IS_PLAYER(view) && autocvar_sv_itemstime != 2)
996             return false;
997     }
998     else if (this.team && this.rule == SPRITERULE_DEFAULT)
999     {
1000         if (this.team != view.team)
1001             return false;
1002         if (!IS_PLAYER(view))
1003             return false;
1004     }
1005
1006     return true;
1007 }
1008
1009 entity WaypointSprite_getviewentity(entity e)
1010 {
1011     if (IS_SPEC(e)) e = e.enemy;
1012     /* TODO idea (check this breaks nothing)
1013     else if (e.classname == "observer")
1014         e = NULL;
1015     */
1016     return e;
1017 }
1018
1019 float WaypointSprite_isteammate(entity e, entity e2)
1020 {
1021     if (teamplay)
1022         return e2.team == e.team;
1023     return e2 == e;
1024 }
1025
1026 bool WaypointSprite_Customize(entity this, entity client)
1027 {
1028     // this is not in SendEntity because it shall run every frame, not just every update
1029
1030     // make spectators see what the player would see
1031     entity e = WaypointSprite_getviewentity(client);
1032
1033     if (MUTATOR_CALLHOOK(CustomizeWaypoint, this, client))
1034         return false;
1035
1036     return this.waypointsprite_visible_for_player(this, client, e);
1037 }
1038
1039 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
1040
1041 void WaypointSprite_Reset(entity this)
1042 {
1043     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
1044
1045     if (this.fade_time)
1046         WaypointSprite_Kill(this);
1047 }
1048
1049 entity WaypointSprite_Spawn(
1050     entity spr, // sprite
1051     float _lifetime, float maxdistance, // lifetime, max distance
1052     entity ref, vector ofs, // position
1053     entity showto, float t, // show to whom? Use a flag to indicate a team
1054     entity own, .entity ownfield, // remove when own gets killed
1055     float hideable, // true when it should be controlled by cl_hidewaypoints
1056     entity icon // initial icon
1057 )
1058 {
1059     entity wp = new(sprite_waypoint);
1060     wp.fade_time = _lifetime; // if negative tells client not to fade it out
1061     if(_lifetime < 0)
1062         _lifetime = -_lifetime;
1063     wp.teleport_time = time + _lifetime;
1064     wp.exteriormodeltoclient = ref;
1065     if (ref)
1066     {
1067         wp.view_ofs = ofs;
1068         setorigin(wp, ref.origin + ofs);
1069     }
1070     else
1071         setorigin(wp, ofs);
1072     wp.enemy = showto;
1073     wp.team = t;
1074     wp.owner = own;
1075     wp.currentammo = hideable;
1076     if (own)
1077     {
1078         if (own.(ownfield))
1079             delete(own.(ownfield));
1080         own.(ownfield) = wp;
1081         wp.owned_by_field = ownfield;
1082     }
1083     wp.fade_rate = maxdistance;
1084     setthink(wp, WaypointSprite_Think);
1085     wp.nextthink = time;
1086     wp.model1 = spr.netname;
1087     setcefc(wp, WaypointSprite_Customize);
1088     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1089     wp.reset2 = WaypointSprite_Reset;
1090     wp.cnt = icon.m_id;
1091     wp.colormod = spr.m_color;
1092     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1093     return wp;
1094 }
1095
1096 entity WaypointSprite_SpawnFixed(
1097     entity spr,
1098     vector ofs,
1099     entity own,
1100     .entity ownfield,
1101     entity icon // initial icon
1102 )
1103 {
1104     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, own, ownfield, true, icon);
1105 }
1106
1107 entity WaypointSprite_DeployFixed(
1108     entity spr,
1109     bool limited_range,
1110     entity player,
1111     vector ofs,
1112     entity icon // initial icon
1113 )
1114 {
1115     float t;
1116     if (teamplay)
1117         t = player.team;
1118     else
1119         t = 0;
1120     float maxdistance;
1121     if (limited_range)
1122         maxdistance = waypointsprite_limitedrange;
1123     else
1124         maxdistance = 0;
1125     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, NULL, ofs, NULL, t, player, waypointsprite_deployed_fixed, false, icon);
1126 }
1127
1128 entity WaypointSprite_DeployPersonal(
1129     entity spr,
1130     entity player,
1131     vector ofs,
1132     entity icon // initial icon
1133 )
1134 {
1135     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, player, waypointsprite_deployed_personal, false, icon);
1136 }
1137
1138 entity WaypointSprite_Attach(
1139     entity spr,
1140     entity player,
1141     bool limited_range,
1142     entity icon // initial icon
1143 )
1144 {
1145     float t;
1146     if (player.waypointsprite_attachedforcarrier)
1147         return NULL; // can't attach to FC
1148     if (teamplay)
1149         t = player.team;
1150     else
1151         t = 0;
1152     float maxdistance;
1153     if (limited_range)
1154         maxdistance = waypointsprite_limitedrange;
1155     else
1156         maxdistance = 0;
1157     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, player, '0 0 64', NULL, t, player, waypointsprite_attached, false, icon);
1158 }
1159
1160 entity WaypointSprite_AttachCarrier(
1161     entity spr,
1162     entity carrier,
1163     entity icon // initial icon and color
1164 )
1165 {
1166     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1167     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', NULL, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1168     if (GetResource(carrier, RES_HEALTH))
1169     {
1170         WaypointSprite_UpdateMaxHealth(e, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
1171         WaypointSprite_UpdateHealth(e, healtharmor_maxdamage(GetResource(carrier, RES_HEALTH), GetResource(carrier, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
1172     }
1173     return e;
1174 }
1175
1176 void WaypointSprite_DetachCarrier(entity carrier)
1177 {
1178     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1179 }
1180
1181 void WaypointSprite_ClearPersonal(entity this)
1182 {
1183     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1184 }
1185
1186 void WaypointSprite_ClearOwned(entity this)
1187 {
1188     WaypointSprite_Kill(this.waypointsprite_deployed_fixed);
1189     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1190     WaypointSprite_Kill(this.waypointsprite_attached);
1191 }
1192
1193 void WaypointSprite_PlayerDead(entity this)
1194 {
1195     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1196     WaypointSprite_DetachCarrier(this);
1197 }
1198
1199 void WaypointSprite_PlayerGone(entity this)
1200 {
1201     WaypointSprite_Disown(this.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1202     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1203     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1204     WaypointSprite_DetachCarrier(this);
1205 }
1206 #endif