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