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