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