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