9adf1a95bac36b74e3a5dbd046ae630b820c77db
[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 & 0x7F);
214         if (f & 0x80)
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(string s, vector def)
246 {SELFPARAM();
247     if (s == WP_Weapon.netname) return get_weaponinfo(self.wp_extra).wpcolor;
248     if (s == WP_Item.netname) return Items[self.wp_extra].m_color;
249     if (s == WP_Buff.netname) return Buffs[self.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(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, float icon, vector col)
831 {
832     // no check, as this is never called without doing an actual change (usually only once)
833     e.cnt = (icon & 0x7F) | (e.cnt & 0x80);
834     e.colormod = col;
835     e.SendFlags |= 32;
836 }
837
838 void WaypointSprite_Ping(entity e)
839 {
840     // anti spam
841     if (time < e.waypointsprite_pingtime) return;
842     e.waypointsprite_pingtime = time + 0.3;
843     // ALWAYS sends (this causes a radar circle), thus no check
844     e.cnt |= 0x80;
845     e.SendFlags |= 32;
846 }
847
848 void WaypointSprite_HelpMePing(entity e)
849 {
850     WaypointSprite_Ping(e);
851     e.waypointsprite_helpmetime = time + waypointsprite_deployed_lifetime;
852     e.SendFlags |= 32;
853 }
854
855 void WaypointSprite_FadeOutIn(entity e, float t)
856 {
857     if (!e.fade_time)
858     {
859         e.fade_time = t;
860         e.teleport_time = time + t;
861     }
862     else if (t < (e.teleport_time - time))
863     {
864         // accelerate the waypoint's dying
865         // ensure:
866         //   (e.teleport_time - time) / wp.fade_time stays
867         //   e.teleport_time = time + fadetime
868         float current_fadetime;
869         current_fadetime = e.teleport_time - time;
870         e.teleport_time = time + t;
871         e.fade_time = e.fade_time * t / current_fadetime;
872     }
873
874     e.SendFlags |= 16;
875 }
876
877 void WaypointSprite_Init()
878 {
879     waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
880     waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
881     waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
882 }
883
884 void WaypointSprite_InitClient(entity e)
885 {
886 }
887
888 void WaypointSprite_Kill(entity wp)
889 {
890     if (!wp) return;
891     if (wp.owner) wp.owner.(wp.owned_by_field) = world;
892     remove(wp);
893 }
894
895 void WaypointSprite_Disown(entity wp, float fadetime)
896 {
897     if (!wp) return;
898     if (wp.classname != "sprite_waypoint")
899     {
900         backtrace("Trying to disown a non-waypointsprite");
901         return;
902     }
903     if (wp.owner)
904     {
905         if (wp.exteriormodeltoclient == wp.owner)
906             wp.exteriormodeltoclient = world;
907         wp.owner.(wp.owned_by_field) = world;
908         wp.owner = world;
909
910         WaypointSprite_FadeOutIn(wp, fadetime);
911     }
912 }
913
914 void WaypointSprite_Think()
915 {SELFPARAM();
916     bool doremove = false;
917
918     if (self.fade_time && time >= self.teleport_time)
919     {
920         doremove = true;
921     }
922
923     if (self.exteriormodeltoclient)
924         WaypointSprite_UpdateOrigin(self, self.exteriormodeltoclient.origin + self.view_ofs);
925
926     if (doremove)
927         WaypointSprite_Kill(self);
928     else
929         self.nextthink = time; // WHY?!?
930 }
931
932 float WaypointSprite_visible_for_player(entity e)
933 {SELFPARAM();
934     // personal waypoints
935     if (self.enemy && self.enemy != e)
936         return false;
937
938     // team waypoints
939     if (self.rule == SPRITERULE_SPECTATOR)
940     {
941         if (!autocvar_sv_itemstime)
942             return false;
943         if (!warmup_stage && IS_PLAYER(e))
944             return false;
945     }
946     else if (self.team && self.rule == SPRITERULE_DEFAULT)
947     {
948         if (self.team != e.team)
949             return false;
950         if (!IS_PLAYER(e))
951             return false;
952     }
953
954     return true;
955 }
956
957 entity WaypointSprite_getviewentity(entity e)
958 {
959     if (IS_SPEC(e)) e = e.enemy;
960     /* TODO idea (check this breaks nothing)
961     else if (e.classname == "observer")
962         e = world;
963     */
964     return e;
965 }
966
967 float WaypointSprite_isteammate(entity e, entity e2)
968 {
969     if (teamplay)
970         return e2.team == e.team;
971     return e2 == e;
972 }
973
974 float WaypointSprite_Customize()
975 {SELFPARAM();
976     // this is not in SendEntity because it shall run every frame, not just every update
977
978     // make spectators see what the player would see
979     entity e = WaypointSprite_getviewentity(other);
980
981     if (MUTATOR_CALLHOOK(CustomizeWaypoint, self, other))
982         return false;
983
984     return self.waypointsprite_visible_for_player(e);
985 }
986
987 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
988
989 void WaypointSprite_Reset()
990 {SELFPARAM();
991     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
992
993     if (self.fade_time) // there was there before: || g_keyhunt, do we really need this?
994         WaypointSprite_Kill(self);
995 }
996
997 entity WaypointSprite_Spawn(
998     entity spr, // sprite
999     float _lifetime, float maxdistance, // lifetime, max distance
1000     entity ref, vector ofs, // position
1001     entity showto, float t, // show to whom? Use a flag to indicate a team
1002     entity own, .entity ownfield, // remove when own gets killed
1003     float hideable, // true when it should be controlled by cl_hidewaypoints
1004     float icon // initial icon
1005 )
1006 {
1007     entity wp = new(sprite_waypoint);
1008     wp.teleport_time = time + _lifetime;
1009     wp.fade_time = _lifetime;
1010     wp.exteriormodeltoclient = ref;
1011     if (ref)
1012     {
1013         wp.view_ofs = ofs;
1014         setorigin(wp, ref.origin + ofs);
1015     }
1016     else
1017         setorigin(wp, ofs);
1018     wp.enemy = showto;
1019     wp.team = t;
1020     wp.owner = own;
1021     wp.currentammo = hideable;
1022     if (own)
1023     {
1024         if (own.(ownfield))
1025             remove(own.(ownfield));
1026         own.(ownfield) = wp;
1027         wp.owned_by_field = ownfield;
1028     }
1029     wp.fade_rate = maxdistance;
1030     wp.think = WaypointSprite_Think;
1031     wp.nextthink = time;
1032     wp.model1 = spr.netname;
1033     wp.customizeentityforclient = WaypointSprite_Customize;
1034     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1035     wp.reset2 = WaypointSprite_Reset;
1036     wp.cnt = icon;
1037     wp.colormod = spr.m_color;
1038     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1039     return wp;
1040 }
1041
1042 entity WaypointSprite_SpawnFixed(
1043     entity spr,
1044     vector ofs,
1045     entity own,
1046     .entity ownfield,
1047     float icon // initial icon
1048 )
1049 {
1050     return WaypointSprite_Spawn(spr, 0, 0, world, ofs, world, 0, own, ownfield, true, icon);
1051 }
1052
1053 entity WaypointSprite_DeployFixed(
1054     entity spr,
1055     float limited_range,
1056     vector ofs,
1057     float icon // initial icon
1058 )
1059 {SELFPARAM();
1060     float t;
1061     if (teamplay)
1062         t = self.team;
1063     else
1064         t = 0;
1065     float maxdistance;
1066     if (limited_range)
1067         maxdistance = waypointsprite_limitedrange;
1068     else
1069         maxdistance = 0;
1070     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, world, ofs, world, t, self, waypointsprite_deployed_fixed, false, icon);
1071 }
1072
1073 entity WaypointSprite_DeployPersonal(
1074     entity spr,
1075     vector ofs,
1076     float icon // initial icon
1077 )
1078 {SELFPARAM();
1079     return WaypointSprite_Spawn(spr, 0, 0, world, ofs, world, 0, self, waypointsprite_deployed_personal, false, icon);
1080 }
1081
1082 entity WaypointSprite_Attach(
1083     entity spr,
1084     float limited_range,
1085     float icon // initial icon
1086 )
1087 {SELFPARAM();
1088     float t;
1089     if (self.waypointsprite_attachedforcarrier)
1090         return world; // can't attach to FC
1091     if (teamplay)
1092         t = self.team;
1093     else
1094         t = 0;
1095     float maxdistance;
1096     if (limited_range)
1097         maxdistance = waypointsprite_limitedrange;
1098     else
1099         maxdistance = 0;
1100     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, self, '0 0 64', world, t, self, waypointsprite_attached, false, icon);
1101 }
1102
1103 entity WaypointSprite_AttachCarrier(
1104     entity spr,
1105     entity carrier,
1106     float icon // initial icon and color
1107 )
1108 {
1109     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1110     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', world, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1111     if (e)
1112     {
1113         WaypointSprite_UpdateMaxHealth(e, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON) * 2);
1114         WaypointSprite_UpdateHealth(e, '1 0 0' * healtharmor_maxdamage(carrier.health, carrier.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON));
1115     }
1116     return e;
1117 }
1118
1119 void WaypointSprite_DetachCarrier(entity carrier)
1120 {
1121     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1122 }
1123
1124 void WaypointSprite_ClearPersonal()
1125 {SELFPARAM();
1126     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1127 }
1128
1129 void WaypointSprite_ClearOwned()
1130 {SELFPARAM();
1131     WaypointSprite_Kill(self.waypointsprite_deployed_fixed);
1132     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1133     WaypointSprite_Kill(self.waypointsprite_attached);
1134 }
1135
1136 void WaypointSprite_PlayerDead()
1137 {SELFPARAM();
1138     WaypointSprite_Disown(self.waypointsprite_attached, waypointsprite_deadlifetime);
1139     WaypointSprite_DetachCarrier(self);
1140 }
1141
1142 void WaypointSprite_PlayerGone()
1143 {SELFPARAM();
1144     WaypointSprite_Disown(self.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1145     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1146     WaypointSprite_Disown(self.waypointsprite_attached, waypointsprite_deadlifetime);
1147     WaypointSprite_DetachCarrier(self);
1148 }
1149 #endif