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