Merge branch 'TimePath/modules'
[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, 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 > 0)
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 && autocvar_cl_hidewaypoints)
474         return; // fixed waypoint
475
476     InterpolateOrigin_Do(this);
477
478     float t = entcs_GetTeam(player_localnum) + 1;
479     string spriteimage = "";
480
481     // choose the sprite
482     switch (this.rule)
483     {
484         case SPRITERULE_SPECTATOR:
485             if (!(
486                 (autocvar_g_waypointsprite_itemstime == 1 && t == NUM_SPECTATOR + 1)
487             ||  (autocvar_g_waypointsprite_itemstime == 2 && (t == NUM_SPECTATOR + 1 || warmup_stage || STAT(ITEMSTIME) == 2))
488                 ))
489                 return;
490             spriteimage = this.netname;
491             break;
492         case SPRITERULE_DEFAULT:
493             if (this.team)
494             {
495                 if (this.team == t)
496                     spriteimage = this.netname;
497                 else
498                     spriteimage = "";
499             }
500             else
501                 spriteimage = this.netname;
502             break;
503         case SPRITERULE_TEAMPLAY:
504             if (t == NUM_SPECTATOR + 1)
505                 spriteimage = this.netname3;
506             else if (this.team == t)
507                 spriteimage = this.netname2;
508             else
509                 spriteimage = this.netname;
510             break;
511         default:
512             error("Invalid waypointsprite rule!");
513             break;
514     }
515
516     if (spriteimage == "")
517         return;
518
519     ++waypointsprite_newcount;
520
521     float dist = vlen(this.origin - view_origin);
522     float a = this.alpha * autocvar_hud_panel_fg_alpha;
523
524     if (this.maxdistance > waypointsprite_normdistance)
525         a *= pow(bound(0, (this.maxdistance - dist) / (this.maxdistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent);
526     else if (this.maxdistance > 0)
527         a *= pow(bound(0, (waypointsprite_fadedistance - dist) / (waypointsprite_fadedistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent) * (1 - waypointsprite_minalpha) + waypointsprite_minalpha;
528
529     vector rgb = spritelookupcolor(this, spriteimage, this.teamradar_color);
530     if (rgb == '0 0 0')
531     {
532         this.teamradar_color = '1 0 1';
533         LOG_INFOF("WARNING: sprite of name %s has no color, using pink so you notice it\n", spriteimage);
534     }
535
536     if (time - floor(time) > 0.5)
537     {
538         if (this.helpme && time < this.helpme)
539             a *= SPRITE_HELPME_BLINK;
540         else if (this.lifetime > 0) // fading out waypoints don't blink
541             a *= spritelookupblinkvalue(this, spriteimage);
542     }
543
544     if (a > 1)
545     {
546         rgb *= a;
547         a = 1;
548     }
549
550     if (a <= 0.003)
551         return;
552
553     rgb = fixrgbexcess(rgb);
554
555     vector o;
556     float ang;
557
558     o = project_3d_to_2d(this.origin);
559     if (o.z < 0
560     || o.x < (vid_conwidth * waypointsprite_edgeoffset_left)
561     || o.y < (vid_conheight * waypointsprite_edgeoffset_top)
562     || o.x > (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right))
563     || o.y > (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)))
564     {
565         // scale it to be just in view
566         vector d;
567         float f1, f2;
568
569         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
570         ang = atan2(-d.x, -d.y);
571         if (o.z < 0)
572             ang += M_PI;
573
574         f1 = d.x / vid_conwidth;
575         f2 = d.y / vid_conheight;
576
577         if (max(f1, -f1) > max(f2, -f2)) {
578             if (d.z * f1 > 0) {
579                 // RIGHT edge
580                 d = d * ((0.5 - waypointsprite_edgeoffset_right) / f1);
581             } else {
582                 // LEFT edge
583                 d = d * (-(0.5 - waypointsprite_edgeoffset_left) / f1);
584             }
585         } else {
586             if (d.z * f2 > 0) {
587                 // BOTTOM edge
588                 d = d * ((0.5 - waypointsprite_edgeoffset_bottom) / f2);
589             } else {
590                 // TOP edge
591                 d = d * (-(0.5 - waypointsprite_edgeoffset_top) / f2);
592             }
593         }
594
595         o = d + '0.5 0 0' * vid_conwidth + '0 0.5 0' * vid_conheight;
596     }
597     else
598     {
599 #if 1
600         ang = M_PI;
601 #else
602         vector d;
603         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
604         ang = atan2(-d.x, -d.y);
605 #endif
606     }
607     o.z = 0;
608
609     float edgedistance_min = min((o.y - (vid_conheight * waypointsprite_edgeoffset_top)),
610     (o.x - (vid_conwidth * waypointsprite_edgeoffset_left)),
611     (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right)) - o.x,
612     (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)) - o.y);
613
614     float vidscale = max(vid_conwidth / vid_width, vid_conheight / vid_height);
615
616     float crosshairdistance = sqrt( pow(o.x - vid_conwidth/2, 2) + pow(o.y - vid_conheight/2, 2) );
617
618     t = waypointsprite_scale * vidscale;
619     a *= waypointsprite_alpha;
620
621     {
622         a = a * (1 - (1 - waypointsprite_distancefadealpha) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
623         t = t * (1 - (1 - waypointsprite_distancefadescale) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
624     }
625     if (edgedistance_min < waypointsprite_edgefadedistance) {
626         a = a * (1 - (1 - waypointsprite_edgefadealpha) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
627         t = t * (1 - (1 - waypointsprite_edgefadescale) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
628     }
629     if (crosshairdistance < waypointsprite_crosshairfadedistance) {
630         a = a * (1 - (1 - waypointsprite_crosshairfadealpha) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
631         t = t * (1 - (1 - waypointsprite_crosshairfadescale) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
632     }
633
634     if (this.build_finished)
635     {
636         if (time < this.build_finished + 0.25)
637         {
638             if (time < this.build_started)
639                 this.health = this.build_starthealth;
640             else if (time < this.build_finished)
641                 this.health = (time - this.build_started) / (this.build_finished - this.build_started) * (1 - this.build_starthealth) + this.build_starthealth;
642             else
643                 this.health = 1;
644         }
645         else
646             this.health = -1;
647     }
648
649     o = drawspritearrow(o, ang, rgb, a, SPRITE_ARROW_SCALE * t);
650
651     string txt;
652     if (autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
653         txt = _("Spam");
654     else
655         txt = spritelookuptext(this, spriteimage);
656     if (this.helpme && time < this.helpme)
657         txt = sprintf(_("%s needing help!"), txt);
658     if (autocvar_g_waypointsprite_uppercase)
659         txt = strtoupper(txt);
660
661     draw_beginBoldFont();
662     if (this.health >= 0)
663     {
664         o = drawspritetext(o, ang, (SPRITE_HEALTHBAR_WIDTH + 2 * SPRITE_HEALTHBAR_BORDER) * t, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
665
666         float align, marg;
667         if (this.build_finished)
668             align = 0.5;
669         else
670             align = 0;
671         if (cos(ang) > 0)
672             marg = -(SPRITE_HEALTHBAR_MARGIN + SPRITE_HEALTHBAR_HEIGHT + 2 * SPRITE_HEALTHBAR_BORDER) * t - 0.5 * waypointsprite_fontsize;
673         else
674             marg = SPRITE_HEALTHBAR_MARGIN * t + 0.5 * waypointsprite_fontsize;
675         drawhealthbar(
676                 o,
677                 0,
678                 this.health,
679                 '0 0 0',
680                 '0 0 0',
681                 SPRITE_HEALTHBAR_WIDTH * t,
682                 SPRITE_HEALTHBAR_HEIGHT * t,
683                 marg,
684                 SPRITE_HEALTHBAR_BORDER * t,
685                 align,
686                 rgb,
687                 a * SPRITE_HEALTHBAR_BORDERALPHA,
688                 rgb,
689                 a * SPRITE_HEALTHBAR_HEALTHALPHA,
690                 DRAWFLAG_NORMAL
691                  );
692     }
693     else
694     {
695         o = drawspritetext(o, ang, 0, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
696     }
697     draw_endBoldFont();
698 }
699
700 void WaypointSprite_Load_Frames(string ext)
701 {
702     int dh = search_begin(strcat("models/sprites/*_frame*", ext), false, false);
703     if (dh < 0) return;
704     int ext_len = strlen(ext);
705     int n = search_getsize(dh);
706     for (int i = 0; i < n; ++i)
707     {
708         string s = search_getfilename(dh, i);
709         s = substring(s, 15, strlen(s) - 15 - ext_len); // strip models/sprites/ and extension
710
711         int o = strstrofs(s, "_frame", 0);
712         string sname = strcat("/spriteframes/", substring(s, 0, o));
713         string sframes = substring(s, o + 6, strlen(s) - o - 6);
714         int f = stof(sframes) + 1;
715         db_put(tempdb, sname, ftos(max(f, stof(db_get(tempdb, sname)))));
716     }
717     search_end(dh);
718 }
719
720 void WaypointSprite_Load();
721 STATIC_INIT(WaypointSprite_Load) {
722     WaypointSprite_Load();
723     WaypointSprite_Load_Frames(".tga");
724     WaypointSprite_Load_Frames(".jpg");
725 }
726 void WaypointSprite_Load()
727 {
728     waypointsprite_fadedistance = vlen(mi_scale);
729     waypointsprite_normdistance = autocvar_g_waypointsprite_normdistance;
730     waypointsprite_minscale = autocvar_g_waypointsprite_minscale;
731     waypointsprite_minalpha = autocvar_g_waypointsprite_minalpha;
732     waypointsprite_distancealphaexponent = autocvar_g_waypointsprite_distancealphaexponent;
733     waypointsprite_timealphaexponent = autocvar_g_waypointsprite_timealphaexponent;
734     waypointsprite_scale = autocvar_g_waypointsprite_scale;
735     waypointsprite_fontsize = autocvar_g_waypointsprite_fontsize;
736     waypointsprite_edgefadealpha = autocvar_g_waypointsprite_edgefadealpha;
737     waypointsprite_edgefadescale = autocvar_g_waypointsprite_edgefadescale;
738     waypointsprite_edgefadedistance = autocvar_g_waypointsprite_edgefadedistance;
739     waypointsprite_edgeoffset_bottom = autocvar_g_waypointsprite_edgeoffset_bottom;
740     waypointsprite_edgeoffset_left = autocvar_g_waypointsprite_edgeoffset_left;
741     waypointsprite_edgeoffset_right = autocvar_g_waypointsprite_edgeoffset_right;
742     waypointsprite_edgeoffset_top = autocvar_g_waypointsprite_edgeoffset_top;
743     waypointsprite_crosshairfadealpha = autocvar_g_waypointsprite_crosshairfadealpha;
744     waypointsprite_crosshairfadescale = autocvar_g_waypointsprite_crosshairfadescale;
745     waypointsprite_crosshairfadedistance = autocvar_g_waypointsprite_crosshairfadedistance;
746     waypointsprite_distancefadealpha = autocvar_g_waypointsprite_distancefadealpha;
747     waypointsprite_distancefadescale = autocvar_g_waypointsprite_distancefadescale;
748     waypointsprite_distancefadedistance = waypointsprite_fadedistance * autocvar_g_waypointsprite_distancefadedistancemultiplier;
749     waypointsprite_alpha = autocvar_g_waypointsprite_alpha * (1 - autocvar__menu_alpha);
750
751     waypointsprite_count = waypointsprite_newcount;
752     waypointsprite_newcount = 0;
753 }
754 #endif
755
756 #ifdef SVQC
757 void WaypointSprite_UpdateSprites(entity e, entity _m1, entity _m2, entity _m3)
758 {
759     string m1 = _m1.netname;
760     string m2 = _m2.netname;
761     string m3 = _m3.netname;
762     if (m1 != e.model1)
763     {
764         e.model1 = m1;
765         e.SendFlags |= 2;
766     }
767     if (m2 != e.model2)
768     {
769         e.model2 = m2;
770         e.SendFlags |= 4;
771     }
772     if (m3 != e.model3)
773     {
774         e.model3 = m3;
775         e.SendFlags |= 8;
776     }
777 }
778
779 void WaypointSprite_UpdateHealth(entity e, float f)
780 {
781     f = bound(0, f, e.max_health);
782     if (f != e.health || e.pain_finished)
783     {
784         e.health = f;
785         e.pain_finished = 0;
786         e.SendFlags |= 0x80;
787     }
788 }
789
790 void WaypointSprite_UpdateMaxHealth(entity e, float f)
791 {
792     if (f != e.max_health || e.pain_finished)
793     {
794         e.max_health = f;
795         e.pain_finished = 0;
796         e.SendFlags |= 0x80;
797     }
798 }
799
800 void WaypointSprite_UpdateBuildFinished(entity e, float f)
801 {
802     if (f != e.pain_finished || e.max_health)
803     {
804         e.max_health = 0;
805         e.pain_finished = f;
806         e.SendFlags |= 0x80;
807     }
808 }
809
810 void WaypointSprite_UpdateOrigin(entity e, vector o)
811 {
812     if (o != e.origin)
813     {
814         setorigin(e, o);
815         e.SendFlags |= 64;
816     }
817 }
818
819 void WaypointSprite_UpdateRule(entity e, float t, float r)
820 {
821     // no check, as this is never called without doing an actual change (usually only once)
822     e.rule = r;
823     e.team = t;
824     e.SendFlags |= 1;
825 }
826
827 void WaypointSprite_UpdateTeamRadar(entity e, entity icon, vector col)
828 {
829     // no check, as this is never called without doing an actual change (usually only once)
830     int i = icon.m_id;
831     e.cnt = (e.cnt & BIT(7)) | (i & BITS(7));
832     e.colormod = col;
833     e.SendFlags |= 32;
834 }
835
836 void WaypointSprite_Ping(entity e)
837 {
838     // anti spam
839     if (time < e.waypointsprite_pingtime) return;
840     e.waypointsprite_pingtime = time + 0.3;
841     // ALWAYS sends (this causes a radar circle), thus no check
842     e.cnt |= BIT(7);
843     e.SendFlags |= 32;
844 }
845
846 void WaypointSprite_HelpMePing(entity e)
847 {
848     WaypointSprite_Ping(e);
849     e.waypointsprite_helpmetime = time + waypointsprite_deployed_lifetime;
850     e.SendFlags |= 32;
851 }
852
853 void WaypointSprite_FadeOutIn(entity e, float t)
854 {
855     if (!e.fade_time)
856     {
857         e.fade_time = t;
858         e.teleport_time = time + t;
859     }
860     else if (t < (e.teleport_time - time))
861     {
862         // accelerate the waypoint's dying
863         // ensure:
864         //   (e.teleport_time - time) / wp.fade_time stays
865         //   e.teleport_time = time + fadetime
866         float current_fadetime = e.teleport_time - time;
867         e.teleport_time = time + t;
868         if (e.fade_time < 0)
869                 e.fade_time = -e.fade_time;
870         e.fade_time = e.fade_time * t / current_fadetime;
871     }
872
873     e.SendFlags |= 16;
874 }
875
876 void WaypointSprite_Init()
877 {
878     waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
879     waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
880     waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
881 }
882
883 void WaypointSprite_Kill(entity wp)
884 {
885     if (!wp) return;
886     if (wp.owner) wp.owner.(wp.owned_by_field) = NULL;
887     delete(wp);
888 }
889
890 void WaypointSprite_Disown(entity wp, float fadetime)
891 {
892     if (!wp) return;
893     if (wp.classname != "sprite_waypoint")
894     {
895         backtrace("Trying to disown a non-waypointsprite");
896         return;
897     }
898     if (wp.owner)
899     {
900         if (wp.exteriormodeltoclient == wp.owner)
901             wp.exteriormodeltoclient = NULL;
902         wp.owner.(wp.owned_by_field) = NULL;
903         wp.owner = NULL;
904
905         WaypointSprite_FadeOutIn(wp, fadetime);
906     }
907 }
908
909 void WaypointSprite_Think(entity this)
910 {
911     bool doremove = false;
912
913     if (this.fade_time && time >= this.teleport_time)
914     {
915         doremove = true;
916     }
917
918     if (this.exteriormodeltoclient)
919         WaypointSprite_UpdateOrigin(this, this.exteriormodeltoclient.origin + this.view_ofs);
920
921     if (doremove)
922         WaypointSprite_Kill(this);
923     else
924         this.nextthink = time; // WHY?!?
925 }
926
927 bool WaypointSprite_visible_for_player(entity this, entity player, entity view)
928 {
929     // personal waypoints
930     if (this.enemy && this.enemy != view)
931         return false;
932
933     // team waypoints
934     if (this.rule == SPRITERULE_SPECTATOR)
935     {
936         if (!autocvar_sv_itemstime)
937             return false;
938         if (!warmup_stage && IS_PLAYER(view) && autocvar_sv_itemstime != 2)
939             return false;
940     }
941     else if (this.team && this.rule == SPRITERULE_DEFAULT)
942     {
943         if (this.team != view.team)
944             return false;
945         if (!IS_PLAYER(view))
946             return false;
947     }
948
949     return true;
950 }
951
952 entity WaypointSprite_getviewentity(entity e)
953 {
954     if (IS_SPEC(e)) e = e.enemy;
955     /* TODO idea (check this breaks nothing)
956     else if (e.classname == "observer")
957         e = NULL;
958     */
959     return e;
960 }
961
962 float WaypointSprite_isteammate(entity e, entity e2)
963 {
964     if (teamplay)
965         return e2.team == e.team;
966     return e2 == e;
967 }
968
969 bool WaypointSprite_Customize(entity this, entity client)
970 {
971     // this is not in SendEntity because it shall run every frame, not just every update
972
973     // make spectators see what the player would see
974     entity e = WaypointSprite_getviewentity(client);
975
976     if (MUTATOR_CALLHOOK(CustomizeWaypoint, this, client))
977         return false;
978
979     return this.waypointsprite_visible_for_player(this, client, e);
980 }
981
982 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
983
984 void WaypointSprite_Reset(entity this)
985 {
986     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
987
988     if (this.fade_time)
989         WaypointSprite_Kill(this);
990 }
991
992 entity WaypointSprite_Spawn(
993     entity spr, // sprite
994     float _lifetime, float maxdistance, // lifetime, max distance
995     entity ref, vector ofs, // position
996     entity showto, float t, // show to whom? Use a flag to indicate a team
997     entity own, .entity ownfield, // remove when own gets killed
998     float hideable, // true when it should be controlled by cl_hidewaypoints
999     entity icon // initial icon
1000 )
1001 {
1002     entity wp = new(sprite_waypoint);
1003     wp.fade_time = _lifetime; // if negative tells client not to fade it out
1004     if(_lifetime < 0)
1005         _lifetime = -_lifetime;
1006     wp.teleport_time = time + _lifetime;
1007     wp.exteriormodeltoclient = ref;
1008     if (ref)
1009     {
1010         wp.view_ofs = ofs;
1011         setorigin(wp, ref.origin + ofs);
1012     }
1013     else
1014         setorigin(wp, ofs);
1015     wp.enemy = showto;
1016     wp.team = t;
1017     wp.owner = own;
1018     wp.currentammo = hideable;
1019     if (own)
1020     {
1021         if (own.(ownfield))
1022             delete(own.(ownfield));
1023         own.(ownfield) = wp;
1024         wp.owned_by_field = ownfield;
1025     }
1026     wp.fade_rate = maxdistance;
1027     setthink(wp, WaypointSprite_Think);
1028     wp.nextthink = time;
1029     wp.model1 = spr.netname;
1030     setcefc(wp, WaypointSprite_Customize);
1031     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1032     wp.reset2 = WaypointSprite_Reset;
1033     wp.cnt = icon.m_id;
1034     wp.colormod = spr.m_color;
1035     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1036     return wp;
1037 }
1038
1039 entity WaypointSprite_SpawnFixed(
1040     entity spr,
1041     vector ofs,
1042     entity own,
1043     .entity ownfield,
1044     entity icon // initial icon
1045 )
1046 {
1047     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, own, ownfield, true, icon);
1048 }
1049
1050 entity WaypointSprite_DeployFixed(
1051     entity spr,
1052     float limited_range,
1053     entity player,
1054     vector ofs,
1055     entity icon // initial icon
1056 )
1057 {
1058     float t;
1059     if (teamplay)
1060         t = player.team;
1061     else
1062         t = 0;
1063     float maxdistance;
1064     if (limited_range)
1065         maxdistance = waypointsprite_limitedrange;
1066     else
1067         maxdistance = 0;
1068     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, NULL, ofs, NULL, t, player, waypointsprite_deployed_fixed, false, icon);
1069 }
1070
1071 entity WaypointSprite_DeployPersonal(
1072     entity spr,
1073     entity player,
1074     vector ofs,
1075     entity icon // initial icon
1076 )
1077 {
1078     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, player, waypointsprite_deployed_personal, false, icon);
1079 }
1080
1081 entity WaypointSprite_Attach(
1082     entity spr,
1083     entity player,
1084     float limited_range,
1085     entity icon // initial icon
1086 )
1087 {
1088     float t;
1089     if (player.waypointsprite_attachedforcarrier)
1090         return NULL; // can't attach to FC
1091     if (teamplay)
1092         t = player.team;
1093     else
1094         t = 0;
1095     float maxdistance;
1096     if (limited_range)
1097         maxdistance = waypointsprite_limitedrange;
1098     else
1099         maxdistance = 0;
1100     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, player, '0 0 64', NULL, t, player, waypointsprite_attached, false, icon);
1101 }
1102
1103 entity WaypointSprite_AttachCarrier(
1104     entity spr,
1105     entity carrier,
1106     entity icon // initial icon and color
1107 )
1108 {
1109     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1110     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', NULL, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1111     if (carrier.health)
1112     {
1113         WaypointSprite_UpdateMaxHealth(e, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
1114         WaypointSprite_UpdateHealth(e, '1 0 0' * healtharmor_maxdamage(carrier.health, carrier.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
1115     }
1116     return e;
1117 }
1118
1119 void WaypointSprite_DetachCarrier(entity carrier)
1120 {
1121     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1122 }
1123
1124 void WaypointSprite_ClearPersonal(entity this)
1125 {
1126     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1127 }
1128
1129 void WaypointSprite_ClearOwned(entity this)
1130 {
1131     WaypointSprite_Kill(this.waypointsprite_deployed_fixed);
1132     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1133     WaypointSprite_Kill(this.waypointsprite_attached);
1134 }
1135
1136 void WaypointSprite_PlayerDead(entity this)
1137 {
1138     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1139     WaypointSprite_DetachCarrier(this);
1140 }
1141
1142 void WaypointSprite_PlayerGone(entity this)
1143 {
1144     WaypointSprite_Disown(this.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1145     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1146     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1147     WaypointSprite_DetachCarrier(this);
1148 }
1149 #endif