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