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