]> git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/waypoints/waypointsprites.qc
Merge branch 'bones_was_here/sv_legacy_bbox_expand_4' 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 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags)
9 {
10     WriteHeader(MSG_ENTITY, waypointsprites);
11
12     sendflags = sendflags & 0x7F;
13
14     if (this.max_health || (this.pain_finished && (time < this.pain_finished + 0.25)))
15         sendflags |= 0x80;
16
17     int hide_flags = 0;
18     if(this.currentammo == 1) hide_flags |= 1; // hideable
19     else if(this.currentammo == 2) hide_flags |= 2; // radar only
20     if(this.exteriormodeltoclient == to) hide_flags |= 2; // my own
21
22     MUTATOR_CALLHOOK(SendWaypoint, this, to, sendflags, hide_flags);
23     sendflags = M_ARGV(2, int);
24     hide_flags = M_ARGV(3, int);
25
26     WriteByte(MSG_ENTITY, sendflags);
27     WriteByte(MSG_ENTITY, this.wp_extra);
28
29     if (sendflags & 0x80)
30     {
31         if (this.max_health)
32         {
33             WriteByte(MSG_ENTITY, (GetResource(this, RES_HEALTH) / this.max_health) * 191.0);
34         }
35         else
36         {
37             float dt = this.pain_finished - time;
38             dt = bound(0, dt * 32, 16383);
39             WriteByte(MSG_ENTITY, (dt & 0xFF00) / 256 + 192);
40             WriteByte(MSG_ENTITY, (dt & 0x00FF));
41         }
42     }
43
44     if (sendflags & 64)
45     {
46         WriteVector(MSG_ENTITY, this.origin);
47     }
48
49     if (sendflags & 1)
50     {
51         WriteByte(MSG_ENTITY, this.team);
52         WriteByte(MSG_ENTITY, this.rule);
53     }
54
55     if (sendflags & 2)
56         WriteString(MSG_ENTITY, this.model1);
57
58     if (sendflags & 4)
59         WriteString(MSG_ENTITY, this.model2);
60
61     if (sendflags & 8)
62         WriteString(MSG_ENTITY, this.model3);
63
64     if (sendflags & 16)
65     {
66         WriteCoord(MSG_ENTITY, this.fade_time);
67         WriteCoord(MSG_ENTITY, this.teleport_time);
68         WriteShort(MSG_ENTITY, bound(0, this.fade_rate, 32767)); // maxdist
69         WriteByte(MSG_ENTITY, hide_flags);
70     }
71
72     if (sendflags & 32)
73     {
74         WriteByte(MSG_ENTITY, this.cnt); // icon on radar
75         WriteByte(MSG_ENTITY, this.colormod.x * 255.0);
76         WriteByte(MSG_ENTITY, this.colormod.y * 255.0);
77         WriteByte(MSG_ENTITY, this.colormod.z * 255.0);
78
79         if (WaypointSprite_isteammate(this.owner, WaypointSprite_getviewentity(to)))
80         {
81             float dt = bound(0, (this.waypointsprite_helpmetime - time) / 0.1, 255);
82             WriteByte(MSG_ENTITY, dt);
83         }
84         else
85             WriteByte(MSG_ENTITY, 0);
86     }
87
88     return true;
89 }
90 #endif
91
92 #ifdef CSQC
93 void Ent_WaypointSprite(entity this, bool isnew);
94 NET_HANDLE(waypointsprites, bool isnew) {
95     Ent_WaypointSprite(this, isnew);
96     return true;
97 }
98
99 void Ent_RemoveWaypointSprite(entity this)
100 {
101     strfree(this.netname);
102     strfree(this.netname2);
103     strfree(this.netname3);
104 }
105
106 void Ent_WaypointSprite(entity this, bool isnew)
107 {
108     int sendflags = ReadByte();
109     this.wp_extra = ReadByte();
110
111     if (!this.spawntime)
112         this.spawntime = time;
113
114     this.draw2d = Draw_WaypointSprite;
115     if (isnew) {
116                 IL_PUSH(g_drawables_2d, this);
117                 IL_PUSH(g_radaricons, this);
118     }
119
120     InterpolateOrigin_Undo(this);
121     this.iflags |= IFLAG_ORIGIN;
122
123     if (sendflags & 0x80)
124     {
125         int t = ReadByte();
126         if (t < 192)
127         {
128             SetResourceExplicit(this, RES_HEALTH, t / 191.0);
129             this.build_finished = 0;
130         }
131         else
132         {
133             t = (t - 192) * 256 + ReadByte();
134             this.build_started = servertime;
135             if (this.build_finished)
136                 this.build_starthealth = bound(0, GetResource(this, RES_HEALTH), 1);
137             else
138                 this.build_starthealth = 0;
139             this.build_finished = servertime + t / 32;
140         }
141     }
142     else
143     {
144         SetResourceExplicit(this, RES_HEALTH, -1);
145         this.build_finished = 0;
146     }
147
148     if (sendflags & 64)
149     {
150         // unfortunately, this needs to be exact (for the 3D display)
151         this.origin = ReadVector();
152         setorigin(this, this.origin);
153     }
154
155     if (sendflags & 1)
156     {
157         this.team = ReadByte();
158         this.rule = ReadByte();
159     }
160
161     if (sendflags & 2)
162     {
163         strcpy(this.netname, ReadString());
164     }
165
166     if (sendflags & 4)
167     {
168         strcpy(this.netname2, ReadString());
169     }
170
171     if (sendflags & 8)
172     {
173         strcpy(this.netname3, ReadString());
174     }
175
176     if (sendflags & 16)
177     {
178         this.lifetime = ReadCoord();
179         this.fadetime = ReadCoord();
180         this.maxdistance = ReadShort();
181         this.hideflags = ReadByte();
182     }
183
184     if (sendflags & 32)
185     {
186         int f = ReadByte();
187         this.teamradar_icon = f & BITS(7);
188         if (f & BIT(7))
189         {
190             this.(teamradar_times[this.teamradar_time_index]) = time;
191             this.teamradar_time_index = (this.teamradar_time_index + 1) % MAX_TEAMRADAR_TIMES;
192         }
193         this.teamradar_color_x = ReadByte() / 255.0;
194         this.teamradar_color_y = ReadByte() / 255.0;
195         this.teamradar_color_z = ReadByte() / 255.0;
196         this.helpme = ReadByte() * 0.1;
197         if (this.helpme > 0)
198             this.helpme += servertime;
199     }
200
201     InterpolateOrigin_Note(this);
202
203     this.entremove = Ent_RemoveWaypointSprite;
204 }
205 #endif
206
207 #ifdef CSQC
208 float spritelookupblinkvalue(entity this, string s)
209 {
210     if (s == WP_Weapon.netname) {
211         if (REGISTRY_GET(Weapons, this.wp_extra).spawnflags & WEP_FLAG_SUPERWEAPON)
212             return 2;
213     }
214     if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_waypointblink;
215     if(s == WP_FlagReturn.netname) return 2;
216
217     return 1;
218 }
219
220 vector spritelookupcolor(entity this, string s, vector def)
221 {
222     if (s == WP_Weapon.netname  || s == RADARICON_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).wpcolor;
223     if (s == WP_Item.netname    || s == RADARICON_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_color;
224     if (MUTATOR_CALLHOOK(WP_Format, this, s))
225     {
226         return M_ARGV(2, vector);
227     }
228     return def;
229 }
230
231 string spritelookuptext(entity this, string s)
232 {
233         if(autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
234                 return "Spam"; // no need to translate this debug string
235     if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
236     if (s == WP_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).m_name;
237     if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_waypoint;
238     if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).monster_name;
239     if (MUTATOR_CALLHOOK(WP_Format, this, s))
240     {
241         return M_ARGV(3, string);
242     }
243
244     // need to loop, as our netname could be one of three
245     FOREACH(Waypoints, it.netname == s, {
246         return it.m_name;
247     });
248
249     return s;
250 }
251
252 string spritelookupicon(entity this, string s)
253 {
254     // TODO: needs icons! //if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
255     if (s == WP_Weapon.netname) return REGISTRY_GET(Weapons, this.wp_extra).model2;
256     if (s == WP_Item.netname) return REGISTRY_GET(Items, this.wp_extra).m_icon;
257     if (s == WP_Vehicle.netname) return REGISTRY_GET(Vehicles, this.wp_extra).m_icon;
258     //if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).m_icon;
259     if (MUTATOR_CALLHOOK(WP_Format, this, s))
260     {
261         return M_ARGV(4, string);
262     }
263
264     // need to loop, as our netname could be one of three
265     FOREACH(Waypoints, it.netname == s, {
266         return it.m_icon;
267     });
268
269     return s;
270 }
271 #endif
272
273 #ifdef CSQC
274 void drawrotpic(vector org, float rot, string pic, vector sz, vector hotspot, vector rgb, float a, float f)
275 {
276     vector v1, v2, v3, v4;
277
278     hotspot = -1 * hotspot;
279
280     // hotspot-relative coordinates of the corners
281     v1 = hotspot;
282     v2 = hotspot + '1 0 0' * sz.x;
283     v3 = hotspot + '1 0 0' * sz.x + '0 1 0' * sz.y;
284     v4 = hotspot                  + '0 1 0' * sz.y;
285
286     // rotate them, and make them absolute
287     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
288     v1 = Rotate(v1, rot) + org;
289     v2 = Rotate(v2, rot) + org;
290     v3 = Rotate(v3, rot) + org;
291     v4 = Rotate(v4, rot) + org;
292
293     // draw them
294     R_BeginPolygon(pic, f, true);
295     R_PolygonVertex(v1, '0 0 0', rgb, a);
296     R_PolygonVertex(v2, '1 0 0', rgb, a);
297     R_PolygonVertex(v3, '1 1 0', rgb, a);
298     R_PolygonVertex(v4, '0 1 0', rgb, a);
299     R_EndPolygon();
300 }
301
302 void drawquad(vector o, vector ri, vector up, string pic, vector rgb, float a, float f)
303 {
304     R_BeginPolygon(pic, f, true);
305     R_PolygonVertex(o, '0 0 0', rgb, a);
306     R_PolygonVertex(o + ri, '1 0 0', rgb, a);
307     R_PolygonVertex(o + up + ri, '1 1 0', rgb, a);
308     R_PolygonVertex(o + up, '0 1 0', rgb, a);
309     R_EndPolygon();
310 }
311
312 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)
313 {
314     vector o, ri, up;
315     float owidth; // outer width
316
317     hotspot = -1 * hotspot;
318
319     // hotspot-relative coordinates of the healthbar corners
320     o = hotspot;
321     ri = '1 0 0';
322     up = '0 1 0';
323
324     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
325     o = Rotate(o, rot) + org;
326     ri = Rotate(ri, rot);
327     up = Rotate(up, rot);
328
329     owidth = width + 2 * border;
330     o = o - up * (margin + border + theheight) + ri * (sz.x - owidth) * 0.5;
331
332     drawquad(o - up * border,                               ri * owidth,    up * border,    "", rgb,  a,  f);
333     drawquad(o + up * theheight,                            ri * owidth,    up * border,    "", rgb,  a,  f);
334     drawquad(o,                                             ri * border,    up * theheight, "", rgb,  a,  f);
335     drawquad(o + ri * (owidth - border),                    ri * border,    up * theheight, "", rgb,  a,  f);
336     drawquad(o + ri * (border + align * ((1 - h) * width)), ri * width * h, up * theheight, "", hrgb, ha, f);
337 }
338
339 // returns location of sprite text
340 vector drawspritearrow(vector o, float ang, vector rgb, float a, float t)
341 {
342     float size   = 9.0 * t;
343     float border = 1.5 * t;
344     float margin = 4.0 * t;
345
346     float borderDiag = border * M_SQRT2;
347     vector arrowX  = eX * size;
348     vector arrowY  = eY * (size+borderDiag);
349     vector borderX = eX * (size+borderDiag);
350     vector borderY = eY * (size+borderDiag+border);
351
352     R_BeginPolygon("", DRAWFLAG_NORMAL, true);
353     R_PolygonVertex(o,                                  '0 0 0', '0 0 0', a);
354     R_PolygonVertex(o + Rotate(arrowY  - borderX, ang), '0 0 0', '0 0 0', a);
355     R_PolygonVertex(o + Rotate(borderY - borderX, ang), '0 0 0', '0 0 0', a);
356     R_PolygonVertex(o + Rotate(borderY + borderX, ang), '0 0 0', '0 0 0', a);
357     R_PolygonVertex(o + Rotate(arrowY  + borderX, ang), '0 0 0', '0 0 0', a);
358     R_EndPolygon();
359
360     R_BeginPolygon("", DRAWFLAG_ADDITIVE, true);
361     R_PolygonVertex(o + Rotate(eY * borderDiag, ang), '0 0 0', rgb, a);
362     R_PolygonVertex(o + Rotate(arrowY - arrowX, ang), '0 0 0', rgb, a);
363     R_PolygonVertex(o + Rotate(arrowY + arrowX, ang), '0 0 0', rgb, a);
364     R_EndPolygon();
365
366     return o + Rotate(eY * (borderDiag+size+margin), ang);
367 }
368
369 // returns location of sprite healthbar
370 vector drawsprite_TextOrIcon(bool is_text, vector o, float ang, float minwidth, vector rgb, float a, vector sz, string str)
371 {
372     float algnx, algny;
373     float sw, w, h;
374     float aspect, sa, ca;
375
376     if (is_text)
377         sw = stringwidth(str, false, sz);
378     else
379         sw = sz.x;
380
381     if (sw > minwidth)
382         w = sw;
383     else
384         w = minwidth;
385     h = sz.y;
386
387     // how do corners work?
388     aspect = vid_conwidth / vid_conheight;
389     sa = sin(ang);
390     ca = cos(ang) * aspect;
391     if (fabs(sa) > fabs(ca))
392     {
393         algnx = (sa < 0);
394         float f = fabs(sa);
395         algny = 0.5 - 0.5 * (f ? (ca / f) : 0);
396     }
397     else
398     {
399         float f = fabs(ca);
400         algnx = 0.5 - 0.5 * (f ? (sa / f) : 0);
401         algny = (ca < 0);
402     }
403
404     // align
405     o.x -= w * algnx;
406     o.y -= h * algny;
407
408     // we want to be onscreen
409     if (o.x < 0)
410         o.x = 0;
411     if (o.y < 0)
412         o.y = 0;
413     if (o.x > vid_conwidth - w)
414         o.x = vid_conwidth - w;
415     if (o.y > vid_conheight - h)
416         o.y = vid_conheight - h;
417
418     o.x += 0.5 * (w - sw);
419
420     if (is_text)
421         drawstring(o, str, sz, rgb, a, DRAWFLAG_NORMAL);
422     else
423         drawpic(o, str, sz, rgb, a, DRAWFLAG_NORMAL);
424
425     o.x += 0.5 * sw;
426     o.y += 0.5 * h;
427
428     return o;
429 }
430
431 vector fixrgbexcess_move(vector rgb, vector src, vector dst)
432 {
433     vector yvec = '0.299 0.587 0.114';
434     return rgb + dst * ((src * yvec) / (dst * yvec)) * ((rgb - '1 1 1') * src);
435 }
436
437 vector fixrgbexcess(vector rgb)
438 {
439     if (rgb.x > 1) {
440         rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 1');
441         if (rgb.y > 1) {
442             rgb = fixrgbexcess_move(rgb, '0 1 0', '0 0 1');
443             if (rgb.z > 1) rgb.z = 1;
444         } else if (rgb.z > 1) {
445             rgb = fixrgbexcess_move(rgb, '0 0 1', '0 1 0');
446             if (rgb.y > 1) rgb.y = 1;
447         }
448     } else if (rgb.y > 1) {
449         rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 1');
450         if (rgb.x > 1) {
451             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 0 1');
452             if (rgb.z > 1) rgb.z = 1;
453         } else if (rgb.z > 1) {
454             rgb = fixrgbexcess_move(rgb, '0 0 1', '1 0 0');
455             if (rgb.x > 1) rgb.x = 1;
456         }
457     } else if (rgb.z > 1) {
458         rgb = fixrgbexcess_move(rgb, '0 0 1', '1 1 0');
459         if (rgb.x > 1) {
460             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 0');
461             if (rgb.y > 1) rgb.y = 1;
462         } else if (rgb.y > 1) {
463             rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 0');
464             if (rgb.x > 1) rgb.x = 1;
465         }
466     }
467     return rgb;
468 }
469
470 void Draw_WaypointSprite(entity this)
471 {
472     if (this.lifetime > 0)
473         this.alpha = (bound(0, (this.fadetime - time) / this.lifetime, 1) ** waypointsprite_timealphaexponent);
474     else
475         this.alpha = 1;
476
477     if (this.hideflags & 2)
478         return; // radar only
479
480     if (autocvar_cl_hidewaypoints >= 2)
481         return;
482
483     if ((this.hideflags & 1) && autocvar_cl_hidewaypoints)
484         return; // fixed waypoint
485
486     InterpolateOrigin_Do(this);
487
488     float t = entcs_GetTeam(player_localnum) + 1;
489     string spriteimage = "";
490
491     // choose the sprite
492     switch (this.rule)
493     {
494         case SPRITERULE_SPECTATOR:
495             if (!(
496                 (autocvar_g_waypointsprite_itemstime == 1 && t == NUM_SPECTATOR + 1)
497             ||  (autocvar_g_waypointsprite_itemstime == 2 && (t == NUM_SPECTATOR + 1 || warmup_stage || STAT(ITEMSTIME) == 2))
498                 ))
499                 return;
500             spriteimage = this.netname;
501             break;
502         case SPRITERULE_DEFAULT:
503             if (this.team)
504             {
505                 if (this.team == t)
506                     spriteimage = this.netname;
507                 else
508                     spriteimage = "";
509             }
510             else
511                 spriteimage = this.netname;
512             break;
513         case SPRITERULE_TEAMPLAY:
514             if (t == NUM_SPECTATOR + 1)
515                 spriteimage = this.netname3;
516             else if (this.team == t)
517                 spriteimage = this.netname2;
518             else
519                 spriteimage = this.netname;
520             break;
521         default:
522             error("Invalid waypointsprite rule!");
523             break;
524     }
525
526     if (spriteimage == "")
527         return;
528
529     ++waypointsprite_newcount;
530
531     float dist = vlen(this.origin - view_origin);
532     float a = this.alpha * autocvar_hud_panel_fg_alpha;
533
534     if(this.maxdistance > 0)
535     {
536         // restrict maximum normal distance to the waypoint's maximum distance to prevent exploiting cvars
537         float maxnormdistance = bound(0, waypointsprite_normdistance, this.maxdistance - 1);
538         a *= (bound(0, (this.maxdistance - dist) / (this.maxdistance - maxnormdistance), 1) ** waypointsprite_distancealphaexponent);
539     }
540
541     vector rgb = spritelookupcolor(this, spriteimage, this.teamradar_color);
542     if (rgb == '0 0 0')
543     {
544         this.teamradar_color = '1 0 1';
545         LOG_INFOF("WARNING: sprite of name %s has no color, using pink so you notice it", spriteimage);
546     }
547
548     float health_val = GetResource(this, RES_HEALTH);
549     float blink_time = (health_val >= 0) ? (health_val * 10) : time;
550     if (blink_time - floor(blink_time) > 0.5)
551     {
552         if (this.helpme && time < this.helpme)
553             a *= SPRITE_HELPME_BLINK;
554         else if (!this.lifetime) // fading out waypoints don't blink
555             a *= spritelookupblinkvalue(this, spriteimage);
556     }
557
558     if (a > 1)
559     {
560         rgb *= a;
561         a = 1;
562     }
563
564     if (a <= 0.003)
565         return;
566
567     rgb = fixrgbexcess(rgb);
568
569     vector o;
570     float ang;
571
572     o = project_3d_to_2d(this.origin);
573     if (o.z < 0
574     || o.x < (vid_conwidth * waypointsprite_edgeoffset_left)
575     || o.y < (vid_conheight * waypointsprite_edgeoffset_top)
576     || o.x > (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right))
577     || o.y > (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)))
578     {
579         // scale it to be just in view
580         vector d;
581
582         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
583         ang = atan2(-d.x, -d.y);
584         if (o.z < 0)
585             ang += M_PI;
586
587                 float f1 = d.x / vid_conwidth;
588                 float f2 = d.y / vid_conheight;
589                 if (f1 == 0) { f1 = 0.000001; }
590                 if (f2 == 0) { f2 = 0.000001; }
591
592         if (max(f1, -f1) > max(f2, -f2)) {
593             if (d.z * f1 > 0) {
594                 // RIGHT edge
595                 d = d * ((0.5 - waypointsprite_edgeoffset_right) / f1);
596             } else {
597                 // LEFT edge
598                 d = d * (-(0.5 - waypointsprite_edgeoffset_left) / f1);
599             }
600         } else {
601             if (d.z * f2 > 0) {
602                 // BOTTOM edge
603                 d = d * ((0.5 - waypointsprite_edgeoffset_bottom) / f2);
604             } else {
605                 // TOP edge
606                 d = d * (-(0.5 - waypointsprite_edgeoffset_top) / f2);
607             }
608         }
609
610         o = d + '0.5 0 0' * vid_conwidth + '0 0.5 0' * vid_conheight;
611     }
612     else
613     {
614 #if 1
615         ang = M_PI;
616 #else
617         vector d;
618         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
619         ang = atan2(-d.x, -d.y);
620 #endif
621     }
622     o.z = 0;
623
624     float edgedistance_min = min((o.y - (vid_conheight * waypointsprite_edgeoffset_top)),
625     (o.x - (vid_conwidth * waypointsprite_edgeoffset_left)),
626     (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right)) - o.x,
627     (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)) - o.y);
628
629     float crosshairdistance = sqrt( ((o.x - vid_conwidth/2) ** 2) + ((o.y - vid_conheight/2) ** 2) );
630
631     t = waypointsprite_scale;
632     a *= waypointsprite_alpha;
633
634     {
635         a = a * (1 - (1 - waypointsprite_distancefadealpha) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
636         t = t * (1 - (1 - waypointsprite_distancefadescale) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
637     }
638     if (edgedistance_min < waypointsprite_edgefadedistance) {
639         a = a * (1 - (1 - waypointsprite_edgefadealpha) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
640         t = t * (1 - (1 - waypointsprite_edgefadescale) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
641     }
642     if (crosshairdistance < waypointsprite_crosshairfadedistance) {
643         a = a * (1 - (1 - waypointsprite_crosshairfadealpha) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
644         t = t * (1 - (1 - waypointsprite_crosshairfadescale) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
645     }
646
647     if (this.build_finished)
648     {
649         if (time < this.build_finished + 0.25)
650         {
651             if (time < this.build_started)
652                 SetResourceExplicit(this, RES_HEALTH, this.build_starthealth);
653             else if (time < this.build_finished)
654                 SetResourceExplicit(this, RES_HEALTH, (time - this.build_started) / (this.build_finished - this.build_started) * (1 - this.build_starthealth) + this.build_starthealth);
655             else
656                 SetResourceExplicit(this, RES_HEALTH, 1);
657         }
658         else
659             SetResourceExplicit(this, RES_HEALTH, -1);
660     }
661
662     o = drawspritearrow(o, ang, rgb, a, SPRITE_ARROW_SCALE * t);
663
664         string pic = "";
665         bool is_text = true;
666         if (!autocvar_g_waypointsprite_text)
667         {
668                 string spr_icon = spritelookupicon(this, spriteimage);
669                 pic = spr_icon;
670                 bool icon_found = !(!spr_icon || spr_icon == "");
671                 if (icon_found) // it's valid, but let's make sure it exists!
672                 {
673                         pic = strcat(hud_skin_path, "/", spr_icon);
674                         if(precache_pic(pic) == "")
675                         {
676                                 pic = strcat("gfx/hud/default/", spr_icon);
677                                 if(!precache_pic(pic))
678                                         icon_found = false;
679                         }
680                 }
681                 if (icon_found)
682                         is_text = false;
683         }
684
685         vector sz;
686         vector col = rgb;
687     string txt = string_null; // it will contain either the text or the icon path
688     if (is_text)
689     {
690         txt = spritelookuptext(this, spriteimage);
691         if (this.helpme && time < this.helpme)
692             txt = sprintf(_("%s needing help!"), txt);
693         if (autocvar_g_waypointsprite_uppercase)
694             txt = strtoupper(txt);
695         sz = waypointsprite_fontsize * '1 1 0';
696     }
697     else
698     {
699         txt = pic; // icon path
700         if (autocvar_g_waypointsprite_iconcolor == 0)
701                 col = '1 1 1';
702         else if (autocvar_g_waypointsprite_iconcolor > 0 && autocvar_g_waypointsprite_iconcolor != 1)
703         {
704             col = rgb_to_hsv(col);
705             col.y *= autocvar_g_waypointsprite_iconcolor; // scale saturation
706             col = hsv_to_rgb(col);
707         }
708         sz = autocvar_g_waypointsprite_iconsize * '1 1 0';
709     }
710
711     draw_beginBoldFont();
712     if (GetResource(this, RES_HEALTH) >= 0)
713     {
714         float align = 0, marg;
715         if (this.build_finished)
716             align = 0.5;
717         else
718             align = 0;
719         if (cos(ang) > 0)
720             marg = -(SPRITE_HEALTHBAR_MARGIN + SPRITE_HEALTHBAR_HEIGHT + 2 * SPRITE_HEALTHBAR_BORDER) * t - 0.5 * sz.y;
721         else
722             marg = SPRITE_HEALTHBAR_MARGIN * t + 0.5 * sz.y;
723
724         float minwidth = (SPRITE_HEALTHBAR_WIDTH + 2 * SPRITE_HEALTHBAR_BORDER) * t;
725         o = drawsprite_TextOrIcon(is_text, o, ang, minwidth, col, a, sz, txt);
726         drawhealthbar(
727                 o,
728                 0,
729                 GetResource(this, RES_HEALTH),
730                 '0 0 0',
731                 '0 0 0',
732                 SPRITE_HEALTHBAR_WIDTH * t,
733                 SPRITE_HEALTHBAR_HEIGHT * t,
734                 marg,
735                 SPRITE_HEALTHBAR_BORDER * t,
736                 align,
737                 rgb,
738                 a * SPRITE_HEALTHBAR_BORDERALPHA,
739                 rgb,
740                 a * SPRITE_HEALTHBAR_HEALTHALPHA,
741                 DRAWFLAG_NORMAL
742                  );
743     }
744     else
745     {
746         drawsprite_TextOrIcon(is_text, o, ang, 0, col, a, sz, txt);
747     }
748
749     draw_endBoldFont();
750 }
751
752 void WaypointSprite_Load_Frames(string ext)
753 {
754     int dh = search_begin(strcat("models/sprites/*_frame*", ext), false, false);
755     if (dh < 0) return;
756     int ext_len = strlen(ext);
757     int n = search_getsize(dh);
758     for (int i = 0; i < n; ++i)
759     {
760         string s = search_getfilename(dh, i);
761         s = substring(s, 15, strlen(s) - 15 - ext_len); // strip models/sprites/ and extension
762
763         int o = strstrofs(s, "_frame", 0);
764         string sname = strcat("/spriteframes/", substring(s, 0, o));
765         string sframes = substring(s, o + 6, strlen(s) - o - 6);
766         int f = stof(sframes) + 1;
767         db_put(tempdb, sname, ftos(max(f, stof(db_get(tempdb, sname)))));
768     }
769     search_end(dh);
770 }
771
772 void WaypointSprite_Load();
773 STATIC_INIT(WaypointSprite_Load) {
774     WaypointSprite_Load();
775     WaypointSprite_Load_Frames(".tga");
776     WaypointSprite_Load_Frames(".jpg");
777 }
778 void WaypointSprite_Load()
779 {
780     waypointsprite_fadedistance = vlen(mi_scale);
781     waypointsprite_normdistance = autocvar_g_waypointsprite_normdistance;
782     waypointsprite_minscale = autocvar_g_waypointsprite_minscale;
783     waypointsprite_minalpha = autocvar_g_waypointsprite_minalpha;
784     waypointsprite_distancealphaexponent = autocvar_g_waypointsprite_distancealphaexponent;
785     waypointsprite_timealphaexponent = autocvar_g_waypointsprite_timealphaexponent;
786     waypointsprite_scale = autocvar_g_waypointsprite_scale;
787     waypointsprite_fontsize = autocvar_g_waypointsprite_fontsize;
788     waypointsprite_edgefadealpha = autocvar_g_waypointsprite_edgefadealpha;
789     waypointsprite_edgefadescale = autocvar_g_waypointsprite_edgefadescale;
790     waypointsprite_edgefadedistance = autocvar_g_waypointsprite_edgefadedistance;
791     waypointsprite_edgeoffset_bottom = autocvar_g_waypointsprite_edgeoffset_bottom;
792     waypointsprite_edgeoffset_left = autocvar_g_waypointsprite_edgeoffset_left;
793     waypointsprite_edgeoffset_right = autocvar_g_waypointsprite_edgeoffset_right;
794     waypointsprite_edgeoffset_top = autocvar_g_waypointsprite_edgeoffset_top;
795     waypointsprite_crosshairfadealpha = autocvar_g_waypointsprite_crosshairfadealpha;
796     waypointsprite_crosshairfadescale = autocvar_g_waypointsprite_crosshairfadescale;
797     waypointsprite_crosshairfadedistance = autocvar_g_waypointsprite_crosshairfadedistance;
798     waypointsprite_distancefadealpha = autocvar_g_waypointsprite_distancefadealpha;
799     waypointsprite_distancefadescale = autocvar_g_waypointsprite_distancefadescale;
800     waypointsprite_distancefadedistance = waypointsprite_fadedistance * autocvar_g_waypointsprite_distancefadedistancemultiplier;
801     waypointsprite_alpha = autocvar_g_waypointsprite_alpha * (1 - autocvar__menu_alpha);
802
803     waypointsprite_count = waypointsprite_newcount;
804     waypointsprite_newcount = 0;
805 }
806 #endif
807
808 #ifdef SVQC
809 void WaypointSprite_UpdateSprites(entity e, entity _m1, entity _m2, entity _m3)
810 {
811     string m1 = _m1.netname;
812     string m2 = _m2.netname;
813     string m3 = _m3.netname;
814     if (m1 != e.model1)
815     {
816         e.model1 = m1;
817         e.SendFlags |= 2;
818     }
819     if (m2 != e.model2)
820     {
821         e.model2 = m2;
822         e.SendFlags |= 4;
823     }
824     if (m3 != e.model3)
825     {
826         e.model3 = m3;
827         e.SendFlags |= 8;
828     }
829 }
830
831 void WaypointSprite_UpdateHealth(entity e, float f)
832 {
833     f = bound(0, f, e.max_health);
834     float step = e.max_health / 40;
835     if ((floor(f / step) != floor(GetResource(e, RES_HEALTH) / step)) || e.pain_finished)
836     {
837         SetResourceExplicit(e, RES_HEALTH, f);
838         e.pain_finished = 0;
839         e.SendFlags |= 0x80;
840     }
841 }
842
843 void WaypointSprite_UpdateMaxHealth(entity e, float f)
844 {
845     if (f != e.max_health || e.pain_finished)
846     {
847         e.max_health = f;
848         e.pain_finished = 0;
849         e.SendFlags |= 0x80;
850     }
851 }
852
853 void WaypointSprite_UpdateBuildFinished(entity e, float f)
854 {
855     if (f != e.pain_finished || e.max_health)
856     {
857         e.max_health = 0;
858         e.pain_finished = f;
859         e.SendFlags |= 0x80;
860     }
861 }
862
863 void WaypointSprite_UpdateOrigin(entity e, vector o)
864 {
865     if (o != e.origin)
866     {
867         setorigin(e, o);
868         e.SendFlags |= 64;
869     }
870 }
871
872 void WaypointSprite_UpdateRule(entity e, float t, float r)
873 {
874     // no check, as this is never called without doing an actual change (usually only once)
875     e.rule = r;
876     e.team = t;
877     e.SendFlags |= 1;
878 }
879
880 void WaypointSprite_UpdateTeamRadar(entity e, entity icon, vector col)
881 {
882     // no check, as this is never called without doing an actual change (usually only once)
883     int i = icon.m_id;
884     int new_cnt = (e.cnt & BIT(7)) | (i & BITS(7));
885     if (new_cnt != e.cnt || col != e.colormod)
886     {
887         e.cnt = new_cnt;
888         e.colormod = col;
889         e.SendFlags |= 32;
890     }
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     bool 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     bool 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 (GetResource(carrier, RES_HEALTH))
1169     {
1170         WaypointSprite_UpdateMaxHealth(e, 2 * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
1171         WaypointSprite_UpdateHealth(e, healtharmor_maxdamage(GetResource(carrier, RES_HEALTH), GetResource(carrier, RES_ARMOR), autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id).x);
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