]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/waypoints/waypointsprites.qc
Merge branch 'TimePath/remove_remove'
[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)
22         f |= 1; // hideable
23     if(this.exteriormodeltoclient == to)
24         f |= 2; // my own
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, LAMBDA(
263         return it.m_name;
264     ));
265
266     return s;
267 }
268 #endif
269
270 #ifdef CSQC
271 void drawrotpic(vector org, float rot, string pic, vector sz, vector hotspot, vector rgb, float a, float f)
272 {
273     vector v1, v2, v3, v4;
274
275     hotspot = -1 * hotspot;
276
277     // hotspot-relative coordinates of the corners
278     v1 = hotspot;
279     v2 = hotspot + '1 0 0' * sz.x;
280     v3 = hotspot + '1 0 0' * sz.x + '0 1 0' * sz.y;
281     v4 = hotspot                  + '0 1 0' * sz.y;
282
283     // rotate them, and make them absolute
284     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
285     v1 = rotate(v1, rot) + org;
286     v2 = rotate(v2, rot) + org;
287     v3 = rotate(v3, rot) + org;
288     v4 = rotate(v4, rot) + org;
289
290     // draw them
291     R_BeginPolygon(pic, f);
292     R_PolygonVertex(v1, '0 0 0', rgb, a);
293     R_PolygonVertex(v2, '1 0 0', rgb, a);
294     R_PolygonVertex(v3, '1 1 0', rgb, a);
295     R_PolygonVertex(v4, '0 1 0', rgb, a);
296     R_EndPolygon();
297 }
298
299 void drawquad(vector o, vector ri, vector up, string pic, vector rgb, float a, float f)
300 {
301     R_BeginPolygon(pic, f);
302     R_PolygonVertex(o, '0 0 0', rgb, a);
303     R_PolygonVertex(o + ri, '1 0 0', rgb, a);
304     R_PolygonVertex(o + up + ri, '1 1 0', rgb, a);
305     R_PolygonVertex(o + up, '0 1 0', rgb, a);
306     R_EndPolygon();
307 }
308
309 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)
310 {
311     vector o, ri, up;
312     float owidth; // outer width
313
314     hotspot = -1 * hotspot;
315
316     // hotspot-relative coordinates of the healthbar corners
317     o = hotspot;
318     ri = '1 0 0';
319     up = '0 1 0';
320
321     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
322     o = rotate(o, rot) + org;
323     ri = rotate(ri, rot);
324     up = rotate(up, rot);
325
326     owidth = width + 2 * border;
327     o = o - up * (margin + border + theheight) + ri * (sz.x - owidth) * 0.5;
328
329     drawquad(o - up * border,                               ri * owidth,    up * border,    "", rgb,  a,  f);
330     drawquad(o + up * theheight,                            ri * owidth,    up * border,    "", rgb,  a,  f);
331     drawquad(o,                                             ri * border,    up * theheight, "", rgb,  a,  f);
332     drawquad(o + ri * (owidth - border),                    ri * border,    up * theheight, "", rgb,  a,  f);
333     drawquad(o + ri * (border + align * ((1 - h) * width)), ri * width * h, up * theheight, "", hrgb, ha, f);
334 }
335
336 // returns location of sprite text
337 vector drawspritearrow(vector o, float ang, vector rgb, float a, float t)
338 {
339     float size   = 9.0 * t;
340     float border = 1.5 * t;
341     float margin = 4.0 * t;
342
343     float borderDiag = border * 1.414;
344     vector arrowX  = eX * size;
345     vector arrowY  = eY * (size+borderDiag);
346     vector borderX = eX * (size+borderDiag);
347     vector borderY = eY * (size+borderDiag+border);
348
349     R_BeginPolygon("", DRAWFLAG_NORMAL);
350     R_PolygonVertex(o,                                  '0 0 0', '0 0 0', a);
351     R_PolygonVertex(o + rotate(arrowY  - borderX, ang), '0 0 0', '0 0 0', a);
352     R_PolygonVertex(o + rotate(borderY - borderX, ang), '0 0 0', '0 0 0', a);
353     R_PolygonVertex(o + rotate(borderY + borderX, ang), '0 0 0', '0 0 0', a);
354     R_PolygonVertex(o + rotate(arrowY  + borderX, ang), '0 0 0', '0 0 0', a);
355     R_EndPolygon();
356
357     R_BeginPolygon("", DRAWFLAG_ADDITIVE);
358     R_PolygonVertex(o + rotate(eY * borderDiag, ang), '0 0 0', rgb, a);
359     R_PolygonVertex(o + rotate(arrowY - arrowX, ang), '0 0 0', rgb, a);
360     R_PolygonVertex(o + rotate(arrowY + arrowX, ang), '0 0 0', rgb, a);
361     R_EndPolygon();
362
363     return o + rotate(eY * (borderDiag+size+margin), ang);
364 }
365
366 // returns location of sprite healthbar
367 vector drawspritetext(vector o, float ang, float minwidth, vector rgb, float a, vector fontsize, string s)
368 {
369     float algnx, algny;
370     float sw, w, h;
371     float aspect, sa, ca;
372
373     sw = stringwidth(s, false, fontsize);
374     if (sw > minwidth)
375         w = sw;
376     else
377         w = minwidth;
378     h = fontsize.y;
379
380     // how do corners work?
381     aspect = vid_conwidth / vid_conheight;
382     sa = sin(ang);
383     ca = cos(ang) * aspect;
384     if (fabs(sa) > fabs(ca))
385     {
386         algnx = (sa < 0);
387         float f = fabs(sa);
388         algny = 0.5 - 0.5 * (f ? (ca / f) : 0);
389     }
390     else
391     {
392         float f = fabs(ca);
393         algnx = 0.5 - 0.5 * (f ? (sa / f) : 0);
394         algny = (ca < 0);
395     }
396
397     // align
398     o.x -= w * algnx;
399     o.y -= h * algny;
400
401     // we want to be onscreen
402     if (o.x < 0)
403         o.x = 0;
404     if (o.y < 0)
405         o.y = 0;
406     if (o.x > vid_conwidth - w)
407         o.x = vid_conwidth - w;
408     if (o.y > vid_conheight - h)
409         o.x = vid_conheight - h;
410
411     o.x += 0.5 * (w - sw);
412
413     drawstring(o, s, fontsize, rgb, a, DRAWFLAG_NORMAL);
414
415     o.x += 0.5 * sw;
416     o.y += 0.5 * h;
417
418     return o;
419 }
420
421 vector fixrgbexcess_move(vector rgb, vector src, vector dst)
422 {
423     vector yvec = '0.299 0.587 0.114';
424     return rgb + dst * ((src * yvec) / (dst * yvec)) * ((rgb - '1 1 1') * src);
425 }
426
427 vector fixrgbexcess(vector rgb)
428 {
429     if (rgb.x > 1) {
430         rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 1');
431         if (rgb.y > 1) {
432             rgb = fixrgbexcess_move(rgb, '0 1 0', '0 0 1');
433             if (rgb.z > 1) rgb.z = 1;
434         } else if (rgb.z > 1) {
435             rgb = fixrgbexcess_move(rgb, '0 0 1', '0 1 0');
436             if (rgb.y > 1) rgb.y = 1;
437         }
438     } else if (rgb.y > 1) {
439         rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 1');
440         if (rgb.x > 1) {
441             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 0 1');
442             if (rgb.z > 1) rgb.z = 1;
443         } else if (rgb.z > 1) {
444             rgb = fixrgbexcess_move(rgb, '0 0 1', '1 0 0');
445             if (rgb.x > 1) rgb.x = 1;
446         }
447     } else if (rgb.z > 1) {
448         rgb = fixrgbexcess_move(rgb, '0 0 1', '1 1 0');
449         if (rgb.x > 1) {
450             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 0');
451             if (rgb.y > 1) rgb.y = 1;
452         } else if (rgb.y > 1) {
453             rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 0');
454             if (rgb.x > 1) rgb.x = 1;
455         }
456     }
457     return rgb;
458 }
459
460 void Draw_WaypointSprite(entity this)
461 {
462     if (this.lifetime)
463         this.alpha = pow(bound(0, (this.fadetime - time) / this.lifetime, 1), waypointsprite_timealphaexponent);
464     else
465         this.alpha = 1;
466
467     if (this.hideflags & 2)
468         return; // radar only
469
470     if (autocvar_cl_hidewaypoints >= 2)
471         return;
472
473     if (this.hideflags & 1)
474         if (autocvar_cl_hidewaypoints)
475             return; // fixed waypoint
476
477     InterpolateOrigin_Do(this);
478
479     float t = entcs_GetTeam(player_localnum) + 1;
480
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;
524     dist = vlen(this.origin - view_origin);
525
526     float a;
527     a = this.alpha * autocvar_hud_panel_fg_alpha;
528
529     if (this.maxdistance > waypointsprite_normdistance)
530         a *= pow(bound(0, (this.maxdistance - dist) / (this.maxdistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent);
531     else if (this.maxdistance > 0)
532         a *= pow(bound(0, (waypointsprite_fadedistance - dist) / (waypointsprite_fadedistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent) * (1 - waypointsprite_minalpha) + waypointsprite_minalpha;
533
534     vector rgb = spritelookupcolor(this, spriteimage, this.teamradar_color);
535     if (rgb == '0 0 0')
536     {
537         this.teamradar_color = '1 0 1';
538         LOG_INFOF("WARNING: sprite of name %s has no color, using pink so you notice it\n", spriteimage);
539     }
540
541     if (time - floor(time) > 0.5)
542     {
543         if (this.helpme && time < this.helpme)
544             a *= SPRITE_HELPME_BLINK;
545         else if (!this.lifetime) // fading out waypoints don't blink
546             a *= spritelookupblinkvalue(this, spriteimage);
547     }
548
549     if (a > 1)
550     {
551         rgb *= a;
552         a = 1;
553     }
554
555     if (a <= 0.003)
556         return;
557
558     rgb = fixrgbexcess(rgb);
559
560     vector o;
561     float ang;
562
563     o = project_3d_to_2d(this.origin);
564     if (o.z < 0
565     || o.x < (vid_conwidth * waypointsprite_edgeoffset_left)
566     || o.y < (vid_conheight * waypointsprite_edgeoffset_top)
567     || o.x > (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right))
568     || o.y > (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)))
569     {
570         // scale it to be just in view
571         vector d;
572         float f1, f2;
573
574         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
575         ang = atan2(-d.x, -d.y);
576         if (o.z < 0)
577             ang += M_PI;
578
579         f1 = d.x / vid_conwidth;
580         f2 = d.y / vid_conheight;
581
582         if (max(f1, -f1) > max(f2, -f2)) {
583             if (d.z * f1 > 0) {
584                 // RIGHT edge
585                 d = d * ((0.5 - waypointsprite_edgeoffset_right) / f1);
586             } else {
587                 // LEFT edge
588                 d = d * (-(0.5 - waypointsprite_edgeoffset_left) / f1);
589             }
590         } else {
591             if (d.z * f2 > 0) {
592                 // BOTTOM edge
593                 d = d * ((0.5 - waypointsprite_edgeoffset_bottom) / f2);
594             } else {
595                 // TOP edge
596                 d = d * (-(0.5 - waypointsprite_edgeoffset_top) / f2);
597             }
598         }
599
600         o = d + '0.5 0 0' * vid_conwidth + '0 0.5 0' * vid_conheight;
601     }
602     else
603     {
604 #if 1
605         ang = M_PI;
606 #else
607         vector d;
608         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
609         ang = atan2(-d.x, -d.y);
610 #endif
611     }
612     o.z = 0;
613
614     float edgedistance_min = min((o.y - (vid_conheight * waypointsprite_edgeoffset_top)),
615     (o.x - (vid_conwidth * waypointsprite_edgeoffset_left)),
616     (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right)) - o.x,
617     (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)) - o.y);
618
619     float vidscale = max(vid_conwidth / vid_width, vid_conheight / vid_height);
620
621     float crosshairdistance = sqrt( pow(o.x - vid_conwidth/2, 2) + pow(o.y - vid_conheight/2, 2) );
622
623     t = waypointsprite_scale * vidscale;
624     a *= waypointsprite_alpha;
625
626     {
627         a = a * (1 - (1 - waypointsprite_distancefadealpha) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
628         t = t * (1 - (1 - waypointsprite_distancefadescale) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
629     }
630     if (edgedistance_min < waypointsprite_edgefadedistance) {
631         a = a * (1 - (1 - waypointsprite_edgefadealpha) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
632         t = t * (1 - (1 - waypointsprite_edgefadescale) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
633     }
634     if (crosshairdistance < waypointsprite_crosshairfadedistance) {
635         a = a * (1 - (1 - waypointsprite_crosshairfadealpha) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
636         t = t * (1 - (1 - waypointsprite_crosshairfadescale) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
637     }
638
639     if (this.build_finished)
640     {
641         if (time < this.build_finished + 0.25)
642         {
643             if (time < this.build_started)
644                 this.health = this.build_starthealth;
645             else if (time < this.build_finished)
646                 this.health = (time - this.build_started) / (this.build_finished - this.build_started) * (1 - this.build_starthealth) + this.build_starthealth;
647             else
648                 this.health = 1;
649         }
650         else
651             this.health = -1;
652     }
653
654     o = drawspritearrow(o, ang, rgb, a, SPRITE_ARROW_SCALE * t);
655
656     string txt;
657     if (autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
658         txt = _("Spam");
659     else
660         txt = spritelookuptext(this, spriteimage);
661     if (this.helpme && time < this.helpme)
662         txt = sprintf(_("%s needing help!"), txt);
663     if (autocvar_g_waypointsprite_uppercase)
664         txt = strtoupper(txt);
665
666     draw_beginBoldFont();
667     if (this.health >= 0)
668     {
669         o = drawspritetext(o, ang, (SPRITE_HEALTHBAR_WIDTH + 2 * SPRITE_HEALTHBAR_BORDER) * t, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
670
671         float align, marg;
672         if (this.build_finished)
673             align = 0.5;
674         else
675             align = 0;
676         if (cos(ang) > 0)
677             marg = -(SPRITE_HEALTHBAR_MARGIN + SPRITE_HEALTHBAR_HEIGHT + 2 * SPRITE_HEALTHBAR_BORDER) * t - 0.5 * waypointsprite_fontsize;
678         else
679             marg = SPRITE_HEALTHBAR_MARGIN * t + 0.5 * waypointsprite_fontsize;
680         drawhealthbar(
681                 o,
682                 0,
683                 this.health,
684                 '0 0 0',
685                 '0 0 0',
686                 SPRITE_HEALTHBAR_WIDTH * t,
687                 SPRITE_HEALTHBAR_HEIGHT * t,
688                 marg,
689                 SPRITE_HEALTHBAR_BORDER * t,
690                 align,
691                 rgb,
692                 a * SPRITE_HEALTHBAR_BORDERALPHA,
693                 rgb,
694                 a * SPRITE_HEALTHBAR_HEALTHALPHA,
695                 DRAWFLAG_NORMAL
696                  );
697     }
698     else
699     {
700         o = drawspritetext(o, ang, 0, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
701     }
702     draw_endBoldFont();
703 }
704
705 void WaypointSprite_Load_Frames(string ext)
706 {
707     int dh = search_begin(strcat("models/sprites/*_frame*", ext), false, false);
708     if (dh < 0) return;
709     int ext_len = strlen(ext);
710     int n = search_getsize(dh);
711     for (int i = 0; i < n; ++i)
712     {
713         string s = search_getfilename(dh, i);
714         s = substring(s, 15, strlen(s) - 15 - ext_len); // strip models/sprites/ and extension
715
716         int o = strstrofs(s, "_frame", 0);
717         string sname = strcat("/spriteframes/", substring(s, 0, o));
718         string sframes = substring(s, o + 6, strlen(s) - o - 6);
719         int f = stof(sframes) + 1;
720         db_put(tempdb, sname, ftos(max(f, stof(db_get(tempdb, sname)))));
721     }
722     search_end(dh);
723 }
724
725 void WaypointSprite_Load();
726 STATIC_INIT(WaypointSprite_Load) {
727     WaypointSprite_Load();
728     WaypointSprite_Load_Frames(".tga");
729     WaypointSprite_Load_Frames(".jpg");
730 }
731 void WaypointSprite_Load()
732 {
733     waypointsprite_fadedistance = vlen(mi_scale);
734     waypointsprite_normdistance = autocvar_g_waypointsprite_normdistance;
735     waypointsprite_minscale = autocvar_g_waypointsprite_minscale;
736     waypointsprite_minalpha = autocvar_g_waypointsprite_minalpha;
737     waypointsprite_distancealphaexponent = autocvar_g_waypointsprite_distancealphaexponent;
738     waypointsprite_timealphaexponent = autocvar_g_waypointsprite_timealphaexponent;
739     waypointsprite_scale = autocvar_g_waypointsprite_scale;
740     waypointsprite_fontsize = autocvar_g_waypointsprite_fontsize;
741     waypointsprite_edgefadealpha = autocvar_g_waypointsprite_edgefadealpha;
742     waypointsprite_edgefadescale = autocvar_g_waypointsprite_edgefadescale;
743     waypointsprite_edgefadedistance = autocvar_g_waypointsprite_edgefadedistance;
744     waypointsprite_edgeoffset_bottom = autocvar_g_waypointsprite_edgeoffset_bottom;
745     waypointsprite_edgeoffset_left = autocvar_g_waypointsprite_edgeoffset_left;
746     waypointsprite_edgeoffset_right = autocvar_g_waypointsprite_edgeoffset_right;
747     waypointsprite_edgeoffset_top = autocvar_g_waypointsprite_edgeoffset_top;
748     waypointsprite_crosshairfadealpha = autocvar_g_waypointsprite_crosshairfadealpha;
749     waypointsprite_crosshairfadescale = autocvar_g_waypointsprite_crosshairfadescale;
750     waypointsprite_crosshairfadedistance = autocvar_g_waypointsprite_crosshairfadedistance;
751     waypointsprite_distancefadealpha = autocvar_g_waypointsprite_distancefadealpha;
752     waypointsprite_distancefadescale = autocvar_g_waypointsprite_distancefadescale;
753     waypointsprite_distancefadedistance = waypointsprite_fadedistance * autocvar_g_waypointsprite_distancefadedistancemultiplier;
754     waypointsprite_alpha = autocvar_g_waypointsprite_alpha * (1 - autocvar__menu_alpha);
755
756     waypointsprite_count = waypointsprite_newcount;
757     waypointsprite_newcount = 0;
758 }
759 #endif
760
761 #ifdef SVQC
762 void WaypointSprite_UpdateSprites(entity e, entity _m1, entity _m2, entity _m3)
763 {
764     string m1 = _m1.netname;
765     string m2 = _m2.netname;
766     string m3 = _m3.netname;
767     if (m1 != e.model1)
768     {
769         e.model1 = m1;
770         e.SendFlags |= 2;
771     }
772     if (m2 != e.model2)
773     {
774         e.model2 = m2;
775         e.SendFlags |= 4;
776     }
777     if (m3 != e.model3)
778     {
779         e.model3 = m3;
780         e.SendFlags |= 8;
781     }
782 }
783
784 void WaypointSprite_UpdateHealth(entity e, float f)
785 {
786     f = bound(0, f, e.max_health);
787     if (f != e.health || e.pain_finished)
788     {
789         e.health = f;
790         e.pain_finished = 0;
791         e.SendFlags |= 0x80;
792     }
793 }
794
795 void WaypointSprite_UpdateMaxHealth(entity e, float f)
796 {
797     if (f != e.max_health || e.pain_finished)
798     {
799         e.max_health = f;
800         e.pain_finished = 0;
801         e.SendFlags |= 0x80;
802     }
803 }
804
805 void WaypointSprite_UpdateBuildFinished(entity e, float f)
806 {
807     if (f != e.pain_finished || e.max_health)
808     {
809         e.max_health = 0;
810         e.pain_finished = f;
811         e.SendFlags |= 0x80;
812     }
813 }
814
815 void WaypointSprite_UpdateOrigin(entity e, vector o)
816 {
817     if (o != e.origin)
818     {
819         setorigin(e, o);
820         e.SendFlags |= 64;
821     }
822 }
823
824 void WaypointSprite_UpdateRule(entity e, float t, float r)
825 {
826     // no check, as this is never called without doing an actual change (usually only once)
827     e.rule = r;
828     e.team = t;
829     e.SendFlags |= 1;
830 }
831
832 void WaypointSprite_UpdateTeamRadar(entity e, entity icon, vector col)
833 {
834     // no check, as this is never called without doing an actual change (usually only once)
835     int i = icon.m_id;
836     e.cnt = (e.cnt & BIT(7)) | (i & BITS(7));
837     e.colormod = col;
838     e.SendFlags |= 32;
839 }
840
841 void WaypointSprite_Ping(entity e)
842 {
843     // anti spam
844     if (time < e.waypointsprite_pingtime) return;
845     e.waypointsprite_pingtime = time + 0.3;
846     // ALWAYS sends (this causes a radar circle), thus no check
847     e.cnt |= BIT(7);
848     e.SendFlags |= 32;
849 }
850
851 void WaypointSprite_HelpMePing(entity e)
852 {
853     WaypointSprite_Ping(e);
854     e.waypointsprite_helpmetime = time + waypointsprite_deployed_lifetime;
855     e.SendFlags |= 32;
856 }
857
858 void WaypointSprite_FadeOutIn(entity e, float t)
859 {
860     if (!e.fade_time)
861     {
862         e.fade_time = t;
863         e.teleport_time = time + t;
864     }
865     else if (t < (e.teleport_time - time))
866     {
867         // accelerate the waypoint's dying
868         // ensure:
869         //   (e.teleport_time - time) / wp.fade_time stays
870         //   e.teleport_time = time + fadetime
871         float current_fadetime;
872         current_fadetime = e.teleport_time - time;
873         e.teleport_time = time + t;
874         e.fade_time = e.fade_time * t / current_fadetime;
875     }
876
877     e.SendFlags |= 16;
878 }
879
880 void WaypointSprite_Init()
881 {
882     waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
883     waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
884     waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
885 }
886
887 void WaypointSprite_Kill(entity wp)
888 {
889     if (!wp) return;
890     if (wp.owner) wp.owner.(wp.owned_by_field) = NULL;
891     delete(wp);
892 }
893
894 void WaypointSprite_Disown(entity wp, float fadetime)
895 {
896     if (!wp) return;
897     if (wp.classname != "sprite_waypoint")
898     {
899         backtrace("Trying to disown a non-waypointsprite");
900         return;
901     }
902     if (wp.owner)
903     {
904         if (wp.exteriormodeltoclient == wp.owner)
905             wp.exteriormodeltoclient = NULL;
906         wp.owner.(wp.owned_by_field) = NULL;
907         wp.owner = NULL;
908
909         WaypointSprite_FadeOutIn(wp, fadetime);
910     }
911 }
912
913 void WaypointSprite_Think(entity this)
914 {
915     bool doremove = false;
916
917     if (this.fade_time && time >= this.teleport_time)
918     {
919         doremove = true;
920     }
921
922     if (this.exteriormodeltoclient)
923         WaypointSprite_UpdateOrigin(this, this.exteriormodeltoclient.origin + this.view_ofs);
924
925     if (doremove)
926         WaypointSprite_Kill(this);
927     else
928         this.nextthink = time; // WHY?!?
929 }
930
931 bool WaypointSprite_visible_for_player(entity this, entity player, entity view)
932 {
933     // personal waypoints
934     if (this.enemy && this.enemy != view)
935         return false;
936
937     // team waypoints
938     if (this.rule == SPRITERULE_SPECTATOR)
939     {
940         if (!autocvar_sv_itemstime)
941             return false;
942         if (!warmup_stage && IS_PLAYER(view) && autocvar_sv_itemstime != 2)
943             return false;
944     }
945     else if (this.team && this.rule == SPRITERULE_DEFAULT)
946     {
947         if (this.team != view.team)
948             return false;
949         if (!IS_PLAYER(view))
950             return false;
951     }
952
953     return true;
954 }
955
956 entity WaypointSprite_getviewentity(entity e)
957 {
958     if (IS_SPEC(e)) e = e.enemy;
959     /* TODO idea (check this breaks nothing)
960     else if (e.classname == "observer")
961         e = NULL;
962     */
963     return e;
964 }
965
966 float WaypointSprite_isteammate(entity e, entity e2)
967 {
968     if (teamplay)
969         return e2.team == e.team;
970     return e2 == e;
971 }
972
973 bool WaypointSprite_Customize(entity this, entity client)
974 {
975     // this is not in SendEntity because it shall run every frame, not just every update
976
977     // make spectators see what the player would see
978     entity e = WaypointSprite_getviewentity(client);
979
980     if (MUTATOR_CALLHOOK(CustomizeWaypoint, this, client))
981         return false;
982
983     return this.waypointsprite_visible_for_player(this, client, e);
984 }
985
986 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
987
988 void WaypointSprite_Reset(entity this)
989 {
990     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
991
992     if (this.fade_time)
993         WaypointSprite_Kill(this);
994 }
995
996 entity WaypointSprite_Spawn(
997     entity spr, // sprite
998     float _lifetime, float maxdistance, // lifetime, max distance
999     entity ref, vector ofs, // position
1000     entity showto, float t, // show to whom? Use a flag to indicate a team
1001     entity own, .entity ownfield, // remove when own gets killed
1002     float hideable, // true when it should be controlled by cl_hidewaypoints
1003     entity icon // initial icon
1004 )
1005 {
1006     entity wp = new(sprite_waypoint);
1007     wp.teleport_time = time + _lifetime;
1008     wp.fade_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