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