]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/waypoints/waypointsprites.qc
Small cleanup
[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 REGISTER_NET_LINKED(waypointsprites)
8
9 #ifdef SVQC
10 /** flags origin [team displayrule] [spritename] [spritename2] [spritename3] [lifetime maxdistance hideable] */
11 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags)
12 {
13     WriteHeader(MSG_ENTITY, waypointsprites);
14
15     sendflags = sendflags & 0x7F;
16
17     if (this.max_health || (this.pain_finished && (time < this.pain_finished + 0.25)))
18         sendflags |= 0x80;
19
20     int f = 0;
21     if(this.currentammo)
22         f |= 1; // hideable
23     if(this.exteriormodeltoclient == to)
24         f |= 2; // my own
25
26     MUTATOR_CALLHOOK(SendWaypoint, this, to, sendflags, f);
27     sendflags = M_ARGV(2, int);
28     f = M_ARGV(3, int);
29
30     WriteByte(MSG_ENTITY, sendflags);
31     WriteByte(MSG_ENTITY, this.wp_extra);
32
33     if (sendflags & 0x80)
34     {
35         if (this.max_health)
36         {
37             WriteByte(MSG_ENTITY, (this.health / this.max_health) * 191.0);
38         }
39         else
40         {
41             float dt = this.pain_finished - time;
42             dt = bound(0, dt * 32, 16383);
43             WriteByte(MSG_ENTITY, (dt & 0xFF00) / 256 + 192);
44             WriteByte(MSG_ENTITY, (dt & 0x00FF));
45         }
46     }
47
48     if (sendflags & 64)
49     {
50         WriteCoord(MSG_ENTITY, this.origin.x);
51         WriteCoord(MSG_ENTITY, this.origin.y);
52         WriteCoord(MSG_ENTITY, this.origin.z);
53     }
54
55     if (sendflags & 1)
56     {
57         WriteByte(MSG_ENTITY, this.team);
58         WriteByte(MSG_ENTITY, this.rule);
59     }
60
61     if (sendflags & 2)
62         WriteString(MSG_ENTITY, this.model1);
63
64     if (sendflags & 4)
65         WriteString(MSG_ENTITY, this.model2);
66
67     if (sendflags & 8)
68         WriteString(MSG_ENTITY, this.model3);
69
70     if (sendflags & 16)
71     {
72         WriteCoord(MSG_ENTITY, this.fade_time);
73         WriteCoord(MSG_ENTITY, this.teleport_time);
74         WriteShort(MSG_ENTITY, this.fade_rate); // maxdist
75         WriteByte(MSG_ENTITY, f);
76     }
77
78     if (sendflags & 32)
79     {
80         WriteByte(MSG_ENTITY, this.cnt); // icon on radar
81         WriteByte(MSG_ENTITY, this.colormod.x * 255.0);
82         WriteByte(MSG_ENTITY, this.colormod.y * 255.0);
83         WriteByte(MSG_ENTITY, this.colormod.z * 255.0);
84
85         if (WaypointSprite_isteammate(this.owner, WaypointSprite_getviewentity(to)))
86         {
87             float dt = (this.waypointsprite_helpmetime - time) / 0.1;
88             if (dt < 0)
89                 dt = 0;
90             if (dt > 255)
91                 dt = 255;
92             WriteByte(MSG_ENTITY, dt);
93         }
94         else
95             WriteByte(MSG_ENTITY, 0);
96     }
97
98     return true;
99 }
100 #endif
101
102 #ifdef CSQC
103 void Ent_WaypointSprite(entity this, bool isnew);
104 NET_HANDLE(waypointsprites, bool isnew) {
105     Ent_WaypointSprite(this, isnew);
106     return true;
107 }
108
109 void Ent_RemoveWaypointSprite(entity this)
110 {
111     if (this.netname) strunzone(this.netname);
112     if (this.netname2) strunzone(this.netname2);
113     if (this.netname3) strunzone(this.netname3);
114 }
115
116 /** flags origin [team displayrule] [spritename] [spritename2] [spritename3] [lifetime maxdistance hideable] */
117 void Ent_WaypointSprite(entity this, bool isnew)
118 {
119     int sendflags = ReadByte();
120     this.wp_extra = ReadByte();
121
122     if (!this.spawntime)
123         this.spawntime = time;
124
125     this.draw2d = Draw_WaypointSprite;
126     if (isnew) {
127                 IL_PUSH(g_drawables_2d, this);
128                 IL_PUSH(g_radaricons, this);
129     }
130
131     InterpolateOrigin_Undo(this);
132     this.iflags |= IFLAG_ORIGIN;
133
134     if (sendflags & 0x80)
135     {
136         int t = ReadByte();
137         if (t < 192)
138         {
139             this.health = t / 191.0;
140             this.build_finished = 0;
141         }
142         else
143         {
144             t = (t - 192) * 256 + ReadByte();
145             this.build_started = servertime;
146             if (this.build_finished)
147                 this.build_starthealth = bound(0, this.health, 1);
148             else
149                 this.build_starthealth = 0;
150             this.build_finished = servertime + t / 32;
151         }
152     }
153     else
154     {
155         this.health = -1;
156         this.build_finished = 0;
157     }
158
159     if (sendflags & 64)
160     {
161         // unfortunately, this needs to be exact (for the 3D display)
162         this.origin_x = ReadCoord();
163         this.origin_y = ReadCoord();
164         this.origin_z = ReadCoord();
165         setorigin(this, this.origin);
166     }
167
168     if (sendflags & 1)
169     {
170         this.team = ReadByte();
171         this.rule = ReadByte();
172     }
173
174     if (sendflags & 2)
175     {
176         if (this.netname)
177             strunzone(this.netname);
178         this.netname = strzone(ReadString());
179     }
180
181     if (sendflags & 4)
182     {
183         if (this.netname2)
184             strunzone(this.netname2);
185         this.netname2 = strzone(ReadString());
186     }
187
188     if (sendflags & 8)
189     {
190         if (this.netname3)
191             strunzone(this.netname3);
192         this.netname3 = strzone(ReadString());
193     }
194
195     if (sendflags & 16)
196     {
197         this.lifetime = ReadCoord();
198         this.fadetime = ReadCoord();
199         this.maxdistance = ReadShort();
200         this.hideflags = ReadByte();
201     }
202
203     if (sendflags & 32)
204     {
205         int f = ReadByte();
206         this.teamradar_icon = f & BITS(7);
207         if (f & BIT(7))
208         {
209             this.(teamradar_times[this.teamradar_time_index]) = time;
210             this.teamradar_time_index = (this.teamradar_time_index + 1) % MAX_TEAMRADAR_TIMES;
211         }
212         this.teamradar_color_x = ReadByte() / 255.0;
213         this.teamradar_color_y = ReadByte() / 255.0;
214         this.teamradar_color_z = ReadByte() / 255.0;
215         this.helpme = ReadByte() * 0.1;
216         if (this.helpme > 0)
217             this.helpme += servertime;
218     }
219
220     InterpolateOrigin_Note(this);
221
222     this.entremove = Ent_RemoveWaypointSprite;
223 }
224 #endif
225
226 #ifdef CSQC
227 float spritelookupblinkvalue(entity this, string s)
228 {
229     if (s == WP_Weapon.netname) {
230         if (Weapons_from(this.wp_extra).spawnflags & WEP_FLAG_SUPERWEAPON)
231             return 2;
232     }
233     if (s == WP_Item.netname) return Items_from(this.wp_extra).m_waypointblink;
234     if(s == WP_FlagReturn.netname) return 2;
235
236     return 1;
237 }
238
239 vector spritelookupcolor(entity this, string s, vector def)
240 {
241     if (s == WP_Weapon.netname  || s == RADARICON_Weapon.netname) return Weapons_from(this.wp_extra).wpcolor;
242     if (s == WP_Item.netname    || s == RADARICON_Item.netname) return Items_from(this.wp_extra).m_color;
243     if (MUTATOR_CALLHOOK(WP_Format, this, s))
244     {
245         return M_ARGV(2, vector);
246     }
247     return def;
248 }
249
250 string spritelookuptext(entity this, string s)
251 {
252     if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
253     if (s == WP_Weapon.netname) return Weapons_from(this.wp_extra).m_name;
254     if (s == WP_Item.netname) return Items_from(this.wp_extra).m_waypoint;
255     if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).monster_name;
256     if (MUTATOR_CALLHOOK(WP_Format, this, s))
257     {
258         return M_ARGV(3, string);
259     }
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         float f = fabs(sa);
388         algny = 0.5 - 0.5 * (f ? (ca / f) : 0);
389     }
390     else
391     {
392         float f = fabs(ca);
393         algnx = 0.5 - 0.5 * (f ? (sa / f) : 0);
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 (this.lifetime > 0)
463         this.alpha = pow(bound(0, (this.fadetime - time) / this.lifetime, 1), waypointsprite_timealphaexponent);
464     else if (this.lifetime < 0)
465         this.alpha = (time < this.fadetime) ? 1 : 0; // no fading out effect
466     else
467         this.alpha = 1;
468
469     if (this.hideflags & 2)
470         return; // radar only
471
472     if (autocvar_cl_hidewaypoints >= 2)
473         return;
474
475     if (this.hideflags & 1 && autocvar_cl_hidewaypoints)
476         return; // fixed waypoint
477
478     InterpolateOrigin_Do(this);
479
480     float t = entcs_GetTeam(player_localnum) + 1;
481     string spriteimage = "";
482
483     // choose the sprite
484     switch (this.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 || STAT(ITEMSTIME) == 2))
490                 ))
491                 return;
492             spriteimage = this.netname;
493             break;
494         case SPRITERULE_DEFAULT:
495             if (this.team)
496             {
497                 if (this.team == t)
498                     spriteimage = this.netname;
499                 else
500                     spriteimage = "";
501             }
502             else
503                 spriteimage = this.netname;
504             break;
505         case SPRITERULE_TEAMPLAY:
506             if (t == NUM_SPECTATOR + 1)
507                 spriteimage = this.netname3;
508             else if (this.team == t)
509                 spriteimage = this.netname2;
510             else
511                 spriteimage = this.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 = vlen(this.origin - view_origin);
524     float a = this.alpha * autocvar_hud_panel_fg_alpha;
525
526     if (this.maxdistance > waypointsprite_normdistance)
527         a *= pow(bound(0, (this.maxdistance - dist) / (this.maxdistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent);
528     else if (this.maxdistance > 0)
529         a *= pow(bound(0, (waypointsprite_fadedistance - dist) / (waypointsprite_fadedistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent) * (1 - waypointsprite_minalpha) + waypointsprite_minalpha;
530
531     vector rgb = spritelookupcolor(this, spriteimage, this.teamradar_color);
532     if (rgb == '0 0 0')
533     {
534         this.teamradar_color = '1 0 1';
535         LOG_INFOF("WARNING: sprite of name %s has no color, using pink so you notice it\n", spriteimage);
536     }
537
538     if (time - floor(time) > 0.5)
539     {
540         if (this.helpme && time < this.helpme)
541             a *= SPRITE_HELPME_BLINK;
542         else if (this.lifetime > 0) // fading out waypoints don't blink
543             a *= spritelookupblinkvalue(this, spriteimage);
544     }
545
546     if (a > 1)
547     {
548         rgb *= a;
549         a = 1;
550     }
551
552     if (a <= 0.003)
553         return;
554
555     rgb = fixrgbexcess(rgb);
556
557     vector o;
558     float ang;
559
560     o = project_3d_to_2d(this.origin);
561     if (o.z < 0
562     || o.x < (vid_conwidth * waypointsprite_edgeoffset_left)
563     || o.y < (vid_conheight * waypointsprite_edgeoffset_top)
564     || o.x > (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right))
565     || o.y > (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)))
566     {
567         // scale it to be just in view
568         vector d;
569         float f1, f2;
570
571         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
572         ang = atan2(-d.x, -d.y);
573         if (o.z < 0)
574             ang += M_PI;
575
576         f1 = d.x / vid_conwidth;
577         f2 = d.y / vid_conheight;
578
579         if (max(f1, -f1) > max(f2, -f2)) {
580             if (d.z * f1 > 0) {
581                 // RIGHT edge
582                 d = d * ((0.5 - waypointsprite_edgeoffset_right) / f1);
583             } else {
584                 // LEFT edge
585                 d = d * (-(0.5 - waypointsprite_edgeoffset_left) / f1);
586             }
587         } else {
588             if (d.z * f2 > 0) {
589                 // BOTTOM edge
590                 d = d * ((0.5 - waypointsprite_edgeoffset_bottom) / f2);
591             } else {
592                 // TOP edge
593                 d = d * (-(0.5 - waypointsprite_edgeoffset_top) / f2);
594             }
595         }
596
597         o = d + '0.5 0 0' * vid_conwidth + '0 0.5 0' * vid_conheight;
598     }
599     else
600     {
601 #if 1
602         ang = M_PI;
603 #else
604         vector d;
605         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
606         ang = atan2(-d.x, -d.y);
607 #endif
608     }
609     o.z = 0;
610
611     float edgedistance_min = min((o.y - (vid_conheight * waypointsprite_edgeoffset_top)),
612     (o.x - (vid_conwidth * waypointsprite_edgeoffset_left)),
613     (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right)) - o.x,
614     (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)) - o.y);
615
616     float vidscale = max(vid_conwidth / vid_width, vid_conheight / vid_height);
617
618     float crosshairdistance = sqrt( pow(o.x - vid_conwidth/2, 2) + pow(o.y - vid_conheight/2, 2) );
619
620     t = waypointsprite_scale * vidscale;
621     a *= waypointsprite_alpha;
622
623     {
624         a = a * (1 - (1 - waypointsprite_distancefadealpha) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
625         t = t * (1 - (1 - waypointsprite_distancefadescale) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
626     }
627     if (edgedistance_min < waypointsprite_edgefadedistance) {
628         a = a * (1 - (1 - waypointsprite_edgefadealpha) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
629         t = t * (1 - (1 - waypointsprite_edgefadescale) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
630     }
631     if (crosshairdistance < waypointsprite_crosshairfadedistance) {
632         a = a * (1 - (1 - waypointsprite_crosshairfadealpha) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
633         t = t * (1 - (1 - waypointsprite_crosshairfadescale) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
634     }
635
636     if (this.build_finished)
637     {
638         if (time < this.build_finished + 0.25)
639         {
640             if (time < this.build_started)
641                 this.health = this.build_starthealth;
642             else if (time < this.build_finished)
643                 this.health = (time - this.build_started) / (this.build_finished - this.build_started) * (1 - this.build_starthealth) + this.build_starthealth;
644             else
645                 this.health = 1;
646         }
647         else
648             this.health = -1;
649     }
650
651     o = drawspritearrow(o, ang, rgb, a, SPRITE_ARROW_SCALE * t);
652
653     string txt;
654     if (autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
655         txt = _("Spam");
656     else
657         txt = spritelookuptext(this, spriteimage);
658     if (this.helpme && time < this.helpme)
659         txt = sprintf(_("%s needing help!"), txt);
660     if (autocvar_g_waypointsprite_uppercase)
661         txt = strtoupper(txt);
662
663     draw_beginBoldFont();
664     if (this.health >= 0)
665     {
666         o = drawspritetext(o, ang, (SPRITE_HEALTHBAR_WIDTH + 2 * SPRITE_HEALTHBAR_BORDER) * t, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
667
668         float align, marg;
669         if (this.build_finished)
670             align = 0.5;
671         else
672             align = 0;
673         if (cos(ang) > 0)
674             marg = -(SPRITE_HEALTHBAR_MARGIN + SPRITE_HEALTHBAR_HEIGHT + 2 * SPRITE_HEALTHBAR_BORDER) * t - 0.5 * waypointsprite_fontsize;
675         else
676             marg = SPRITE_HEALTHBAR_MARGIN * t + 0.5 * waypointsprite_fontsize;
677         drawhealthbar(
678                 o,
679                 0,
680                 this.health,
681                 '0 0 0',
682                 '0 0 0',
683                 SPRITE_HEALTHBAR_WIDTH * t,
684                 SPRITE_HEALTHBAR_HEIGHT * t,
685                 marg,
686                 SPRITE_HEALTHBAR_BORDER * t,
687                 align,
688                 rgb,
689                 a * SPRITE_HEALTHBAR_BORDERALPHA,
690                 rgb,
691                 a * SPRITE_HEALTHBAR_HEALTHALPHA,
692                 DRAWFLAG_NORMAL
693                  );
694     }
695     else
696     {
697         o = drawspritetext(o, ang, 0, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
698     }
699     draw_endBoldFont();
700 }
701
702 void WaypointSprite_Load_Frames(string ext)
703 {
704     int dh = search_begin(strcat("models/sprites/*_frame*", ext), false, false);
705     if (dh < 0) return;
706     int ext_len = strlen(ext);
707     int n = search_getsize(dh);
708     for (int i = 0; i < n; ++i)
709     {
710         string s = search_getfilename(dh, i);
711         s = substring(s, 15, strlen(s) - 15 - ext_len); // strip models/sprites/ and extension
712
713         int o = strstrofs(s, "_frame", 0);
714         string sname = strcat("/spriteframes/", substring(s, 0, o));
715         string sframes = substring(s, o + 6, strlen(s) - o - 6);
716         int f = stof(sframes) + 1;
717         db_put(tempdb, sname, ftos(max(f, stof(db_get(tempdb, sname)))));
718     }
719     search_end(dh);
720 }
721
722 void WaypointSprite_Load();
723 STATIC_INIT(WaypointSprite_Load) {
724     WaypointSprite_Load();
725     WaypointSprite_Load_Frames(".tga");
726     WaypointSprite_Load_Frames(".jpg");
727 }
728 void WaypointSprite_Load()
729 {
730     waypointsprite_fadedistance = vlen(mi_scale);
731     waypointsprite_normdistance = autocvar_g_waypointsprite_normdistance;
732     waypointsprite_minscale = autocvar_g_waypointsprite_minscale;
733     waypointsprite_minalpha = autocvar_g_waypointsprite_minalpha;
734     waypointsprite_distancealphaexponent = autocvar_g_waypointsprite_distancealphaexponent;
735     waypointsprite_timealphaexponent = autocvar_g_waypointsprite_timealphaexponent;
736     waypointsprite_scale = autocvar_g_waypointsprite_scale;
737     waypointsprite_fontsize = autocvar_g_waypointsprite_fontsize;
738     waypointsprite_edgefadealpha = autocvar_g_waypointsprite_edgefadealpha;
739     waypointsprite_edgefadescale = autocvar_g_waypointsprite_edgefadescale;
740     waypointsprite_edgefadedistance = autocvar_g_waypointsprite_edgefadedistance;
741     waypointsprite_edgeoffset_bottom = autocvar_g_waypointsprite_edgeoffset_bottom;
742     waypointsprite_edgeoffset_left = autocvar_g_waypointsprite_edgeoffset_left;
743     waypointsprite_edgeoffset_right = autocvar_g_waypointsprite_edgeoffset_right;
744     waypointsprite_edgeoffset_top = autocvar_g_waypointsprite_edgeoffset_top;
745     waypointsprite_crosshairfadealpha = autocvar_g_waypointsprite_crosshairfadealpha;
746     waypointsprite_crosshairfadescale = autocvar_g_waypointsprite_crosshairfadescale;
747     waypointsprite_crosshairfadedistance = autocvar_g_waypointsprite_crosshairfadedistance;
748     waypointsprite_distancefadealpha = autocvar_g_waypointsprite_distancefadealpha;
749     waypointsprite_distancefadescale = autocvar_g_waypointsprite_distancefadescale;
750     waypointsprite_distancefadedistance = waypointsprite_fadedistance * autocvar_g_waypointsprite_distancefadedistancemultiplier;
751     waypointsprite_alpha = autocvar_g_waypointsprite_alpha * (1 - autocvar__menu_alpha);
752
753     waypointsprite_count = waypointsprite_newcount;
754     waypointsprite_newcount = 0;
755 }
756 #endif
757
758 #ifdef SVQC
759 void WaypointSprite_UpdateSprites(entity e, entity _m1, entity _m2, entity _m3)
760 {
761     string m1 = _m1.netname;
762     string m2 = _m2.netname;
763     string m3 = _m3.netname;
764     if (m1 != e.model1)
765     {
766         e.model1 = m1;
767         e.SendFlags |= 2;
768     }
769     if (m2 != e.model2)
770     {
771         e.model2 = m2;
772         e.SendFlags |= 4;
773     }
774     if (m3 != e.model3)
775     {
776         e.model3 = m3;
777         e.SendFlags |= 8;
778     }
779 }
780
781 void WaypointSprite_UpdateHealth(entity e, float f)
782 {
783     f = bound(0, f, e.max_health);
784     if (f != e.health || e.pain_finished)
785     {
786         e.health = f;
787         e.pain_finished = 0;
788         e.SendFlags |= 0x80;
789     }
790 }
791
792 void WaypointSprite_UpdateMaxHealth(entity e, float f)
793 {
794     if (f != e.max_health || e.pain_finished)
795     {
796         e.max_health = f;
797         e.pain_finished = 0;
798         e.SendFlags |= 0x80;
799     }
800 }
801
802 void WaypointSprite_UpdateBuildFinished(entity e, float f)
803 {
804     if (f != e.pain_finished || e.max_health)
805     {
806         e.max_health = 0;
807         e.pain_finished = f;
808         e.SendFlags |= 0x80;
809     }
810 }
811
812 void WaypointSprite_UpdateOrigin(entity e, vector o)
813 {
814     if (o != e.origin)
815     {
816         setorigin(e, o);
817         e.SendFlags |= 64;
818     }
819 }
820
821 void WaypointSprite_UpdateRule(entity e, float t, float r)
822 {
823     // no check, as this is never called without doing an actual change (usually only once)
824     e.rule = r;
825     e.team = t;
826     e.SendFlags |= 1;
827 }
828
829 void WaypointSprite_UpdateTeamRadar(entity e, entity icon, vector col)
830 {
831     // no check, as this is never called without doing an actual change (usually only once)
832     int i = icon.m_id;
833     e.cnt = (e.cnt & BIT(7)) | (i & BITS(7));
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 |= BIT(7);
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 = e.teleport_time - time;
869         e.teleport_time = time + t;
870         if (e.fade_time < 0)
871                 e.fade_time = -e.fade_time;
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_Kill(entity wp)
886 {
887     if (!wp) return;
888     if (wp.owner) wp.owner.(wp.owned_by_field) = NULL;
889     delete(wp);
890 }
891
892 void WaypointSprite_Disown(entity wp, float fadetime)
893 {
894     if (!wp) return;
895     if (wp.classname != "sprite_waypoint")
896     {
897         backtrace("Trying to disown a non-waypointsprite");
898         return;
899     }
900     if (wp.owner)
901     {
902         if (wp.exteriormodeltoclient == wp.owner)
903             wp.exteriormodeltoclient = NULL;
904         wp.owner.(wp.owned_by_field) = NULL;
905         wp.owner = NULL;
906
907         WaypointSprite_FadeOutIn(wp, fadetime);
908     }
909 }
910
911 void WaypointSprite_Think(entity this)
912 {
913     bool doremove = false;
914
915     if (this.fade_time && time >= this.teleport_time)
916     {
917         doremove = true;
918     }
919
920     if (this.exteriormodeltoclient)
921         WaypointSprite_UpdateOrigin(this, this.exteriormodeltoclient.origin + this.view_ofs);
922
923     if (doremove)
924         WaypointSprite_Kill(this);
925     else
926         this.nextthink = time; // WHY?!?
927 }
928
929 bool WaypointSprite_visible_for_player(entity this, entity player, entity view)
930 {
931     // personal waypoints
932     if (this.enemy && this.enemy != view)
933         return false;
934
935     // team waypoints
936     if (this.rule == SPRITERULE_SPECTATOR)
937     {
938         if (!autocvar_sv_itemstime)
939             return false;
940         if (!warmup_stage && IS_PLAYER(view) && autocvar_sv_itemstime != 2)
941             return false;
942     }
943     else if (this.team && this.rule == SPRITERULE_DEFAULT)
944     {
945         if (this.team != view.team)
946             return false;
947         if (!IS_PLAYER(view))
948             return false;
949     }
950
951     return true;
952 }
953
954 entity WaypointSprite_getviewentity(entity e)
955 {
956     if (IS_SPEC(e)) e = e.enemy;
957     /* TODO idea (check this breaks nothing)
958     else if (e.classname == "observer")
959         e = NULL;
960     */
961     return e;
962 }
963
964 float WaypointSprite_isteammate(entity e, entity e2)
965 {
966     if (teamplay)
967         return e2.team == e.team;
968     return e2 == e;
969 }
970
971 bool WaypointSprite_Customize(entity this, entity client)
972 {
973     // this is not in SendEntity because it shall run every frame, not just every update
974
975     // make spectators see what the player would see
976     entity e = WaypointSprite_getviewentity(client);
977
978     if (MUTATOR_CALLHOOK(CustomizeWaypoint, this, client))
979         return false;
980
981     return this.waypointsprite_visible_for_player(this, client, e);
982 }
983
984 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
985
986 void WaypointSprite_Reset(entity this)
987 {
988     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
989
990     if (this.fade_time)
991         WaypointSprite_Kill(this);
992 }
993
994 entity WaypointSprite_Spawn(
995     entity spr, // sprite
996     float _lifetime, float maxdistance, // lifetime, max distance
997     entity ref, vector ofs, // position
998     entity showto, float t, // show to whom? Use a flag to indicate a team
999     entity own, .entity ownfield, // remove when own gets killed
1000     float hideable, // true when it should be controlled by cl_hidewaypoints
1001     entity icon // initial icon
1002 )
1003 {
1004     entity wp = new(sprite_waypoint);
1005     wp.fade_time = _lifetime; // if negative tells client not to fade it out
1006     if(_lifetime < 0)
1007         _lifetime = -_lifetime;
1008     wp.teleport_time = time + _lifetime;
1009     wp.exteriormodeltoclient = ref;
1010     if (ref)
1011     {
1012         wp.view_ofs = ofs;
1013         setorigin(wp, ref.origin + ofs);
1014     }
1015     else
1016         setorigin(wp, ofs);
1017     wp.enemy = showto;
1018     wp.team = t;
1019     wp.owner = own;
1020     wp.currentammo = hideable;
1021     if (own)
1022     {
1023         if (own.(ownfield))
1024             delete(own.(ownfield));
1025         own.(ownfield) = wp;
1026         wp.owned_by_field = ownfield;
1027     }
1028     wp.fade_rate = maxdistance;
1029     setthink(wp, WaypointSprite_Think);
1030     wp.nextthink = time;
1031     wp.model1 = spr.netname;
1032     setcefc(wp, WaypointSprite_Customize);
1033     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1034     wp.reset2 = WaypointSprite_Reset;
1035     wp.cnt = icon.m_id;
1036     wp.colormod = spr.m_color;
1037     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1038     return wp;
1039 }
1040
1041 entity WaypointSprite_SpawnFixed(
1042     entity spr,
1043     vector ofs,
1044     entity own,
1045     .entity ownfield,
1046     entity icon // initial icon
1047 )
1048 {
1049     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, own, ownfield, true, icon);
1050 }
1051
1052 entity WaypointSprite_DeployFixed(
1053     entity spr,
1054     float limited_range,
1055     entity player,
1056     vector ofs,
1057     entity icon // initial icon
1058 )
1059 {
1060     float t;
1061     if (teamplay)
1062         t = player.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, NULL, ofs, NULL, t, player, waypointsprite_deployed_fixed, false, icon);
1071 }
1072
1073 entity WaypointSprite_DeployPersonal(
1074     entity spr,
1075     entity player,
1076     vector ofs,
1077     entity icon // initial icon
1078 )
1079 {
1080     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, player, waypointsprite_deployed_personal, false, icon);
1081 }
1082
1083 entity WaypointSprite_Attach(
1084     entity spr,
1085     entity player,
1086     float limited_range,
1087     entity icon // initial icon
1088 )
1089 {
1090     float t;
1091     if (player.waypointsprite_attachedforcarrier)
1092         return NULL; // can't attach to FC
1093     if (teamplay)
1094         t = player.team;
1095     else
1096         t = 0;
1097     float maxdistance;
1098     if (limited_range)
1099         maxdistance = waypointsprite_limitedrange;
1100     else
1101         maxdistance = 0;
1102     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, player, '0 0 64', NULL, t, player, waypointsprite_attached, false, icon);
1103 }
1104
1105 entity WaypointSprite_AttachCarrier(
1106     entity spr,
1107     entity carrier,
1108     entity icon // initial icon and color
1109 )
1110 {
1111     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1112     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', NULL, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1113     if (carrier.health)
1114     {
1115         WaypointSprite_UpdateMaxHealth(e, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
1116         WaypointSprite_UpdateHealth(e, '1 0 0' * healtharmor_maxdamage(carrier.health, carrier.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
1117     }
1118     return e;
1119 }
1120
1121 void WaypointSprite_DetachCarrier(entity carrier)
1122 {
1123     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1124 }
1125
1126 void WaypointSprite_ClearPersonal(entity this)
1127 {
1128     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1129 }
1130
1131 void WaypointSprite_ClearOwned(entity this)
1132 {
1133     WaypointSprite_Kill(this.waypointsprite_deployed_fixed);
1134     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1135     WaypointSprite_Kill(this.waypointsprite_attached);
1136 }
1137
1138 void WaypointSprite_PlayerDead(entity this)
1139 {
1140     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1141     WaypointSprite_DetachCarrier(this);
1142 }
1143
1144 void WaypointSprite_PlayerGone(entity this)
1145 {
1146     WaypointSprite_Disown(this.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1147     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1148     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1149     WaypointSprite_DetachCarrier(this);
1150 }
1151 #endif
1152 #endif