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