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