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