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