]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/waypoints/waypointsprites.qc
Increase time from 1 to 2 of waypoints pointing out weapon spawns and show them at...
[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)
476         if (autocvar_cl_hidewaypoints)
477             return; // fixed waypoint
478
479     InterpolateOrigin_Do(this);
480
481     float t = entcs_GetTeam(player_localnum) + 1;
482
483     string spriteimage = "";
484
485     // choose the sprite
486     switch (this.rule)
487     {
488         case SPRITERULE_SPECTATOR:
489             if (!(
490                 (autocvar_g_waypointsprite_itemstime == 1 && t == NUM_SPECTATOR + 1)
491             ||  (autocvar_g_waypointsprite_itemstime == 2 && (t == NUM_SPECTATOR + 1 || warmup_stage || STAT(ITEMSTIME) == 2))
492                 ))
493                 return;
494             spriteimage = this.netname;
495             break;
496         case SPRITERULE_DEFAULT:
497             if (this.team)
498             {
499                 if (this.team == t)
500                     spriteimage = this.netname;
501                 else
502                     spriteimage = "";
503             }
504             else
505                 spriteimage = this.netname;
506             break;
507         case SPRITERULE_TEAMPLAY:
508             if (t == NUM_SPECTATOR + 1)
509                 spriteimage = this.netname3;
510             else if (this.team == t)
511                 spriteimage = this.netname2;
512             else
513                 spriteimage = this.netname;
514             break;
515         default:
516             error("Invalid waypointsprite rule!");
517             break;
518     }
519
520     if (spriteimage == "")
521         return;
522
523     ++waypointsprite_newcount;
524
525     float dist;
526     dist = vlen(this.origin - view_origin);
527
528     float a;
529     a = this.alpha * autocvar_hud_panel_fg_alpha;
530
531     if (this.maxdistance > waypointsprite_normdistance)
532         a *= pow(bound(0, (this.maxdistance - dist) / (this.maxdistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent);
533     else if (this.maxdistance > 0)
534         a *= pow(bound(0, (waypointsprite_fadedistance - dist) / (waypointsprite_fadedistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent) * (1 - waypointsprite_minalpha) + waypointsprite_minalpha;
535
536     vector rgb = spritelookupcolor(this, spriteimage, this.teamradar_color);
537     if (rgb == '0 0 0')
538     {
539         this.teamradar_color = '1 0 1';
540         LOG_INFOF("WARNING: sprite of name %s has no color, using pink so you notice it\n", spriteimage);
541     }
542
543     if (time - floor(time) > 0.5)
544     {
545         if (this.helpme && time < this.helpme)
546             a *= SPRITE_HELPME_BLINK;
547         else if (this.lifetime > 0) // fading out waypoints don't blink
548             a *= spritelookupblinkvalue(this, spriteimage);
549     }
550
551     if (a > 1)
552     {
553         rgb *= a;
554         a = 1;
555     }
556
557     if (a <= 0.003)
558         return;
559
560     rgb = fixrgbexcess(rgb);
561
562     vector o;
563     float ang;
564
565     o = project_3d_to_2d(this.origin);
566     if (o.z < 0
567     || o.x < (vid_conwidth * waypointsprite_edgeoffset_left)
568     || o.y < (vid_conheight * waypointsprite_edgeoffset_top)
569     || o.x > (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right))
570     || o.y > (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)))
571     {
572         // scale it to be just in view
573         vector d;
574         float f1, f2;
575
576         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
577         ang = atan2(-d.x, -d.y);
578         if (o.z < 0)
579             ang += M_PI;
580
581         f1 = d.x / vid_conwidth;
582         f2 = d.y / vid_conheight;
583
584         if (max(f1, -f1) > max(f2, -f2)) {
585             if (d.z * f1 > 0) {
586                 // RIGHT edge
587                 d = d * ((0.5 - waypointsprite_edgeoffset_right) / f1);
588             } else {
589                 // LEFT edge
590                 d = d * (-(0.5 - waypointsprite_edgeoffset_left) / f1);
591             }
592         } else {
593             if (d.z * f2 > 0) {
594                 // BOTTOM edge
595                 d = d * ((0.5 - waypointsprite_edgeoffset_bottom) / f2);
596             } else {
597                 // TOP edge
598                 d = d * (-(0.5 - waypointsprite_edgeoffset_top) / f2);
599             }
600         }
601
602         o = d + '0.5 0 0' * vid_conwidth + '0 0.5 0' * vid_conheight;
603     }
604     else
605     {
606 #if 1
607         ang = M_PI;
608 #else
609         vector d;
610         d = o - '0.5 0 0' * vid_conwidth - '0 0.5 0' * vid_conheight;
611         ang = atan2(-d.x, -d.y);
612 #endif
613     }
614     o.z = 0;
615
616     float edgedistance_min = min((o.y - (vid_conheight * waypointsprite_edgeoffset_top)),
617     (o.x - (vid_conwidth * waypointsprite_edgeoffset_left)),
618     (vid_conwidth - (vid_conwidth * waypointsprite_edgeoffset_right)) - o.x,
619     (vid_conheight - (vid_conheight * waypointsprite_edgeoffset_bottom)) - o.y);
620
621     float vidscale = max(vid_conwidth / vid_width, vid_conheight / vid_height);
622
623     float crosshairdistance = sqrt( pow(o.x - vid_conwidth/2, 2) + pow(o.y - vid_conheight/2, 2) );
624
625     t = waypointsprite_scale * vidscale;
626     a *= waypointsprite_alpha;
627
628     {
629         a = a * (1 - (1 - waypointsprite_distancefadealpha) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
630         t = t * (1 - (1 - waypointsprite_distancefadescale) * (bound(0, dist/waypointsprite_distancefadedistance, 1)));
631     }
632     if (edgedistance_min < waypointsprite_edgefadedistance) {
633         a = a * (1 - (1 - waypointsprite_edgefadealpha) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
634         t = t * (1 - (1 - waypointsprite_edgefadescale) * (1 - bound(0, edgedistance_min/waypointsprite_edgefadedistance, 1)));
635     }
636     if (crosshairdistance < waypointsprite_crosshairfadedistance) {
637         a = a * (1 - (1 - waypointsprite_crosshairfadealpha) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
638         t = t * (1 - (1 - waypointsprite_crosshairfadescale) * (1 - bound(0, crosshairdistance/waypointsprite_crosshairfadedistance, 1)));
639     }
640
641     if (this.build_finished)
642     {
643         if (time < this.build_finished + 0.25)
644         {
645             if (time < this.build_started)
646                 this.health = this.build_starthealth;
647             else if (time < this.build_finished)
648                 this.health = (time - this.build_started) / (this.build_finished - this.build_started) * (1 - this.build_starthealth) + this.build_starthealth;
649             else
650                 this.health = 1;
651         }
652         else
653             this.health = -1;
654     }
655
656     o = drawspritearrow(o, ang, rgb, a, SPRITE_ARROW_SCALE * t);
657
658     string txt;
659     if (autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
660         txt = _("Spam");
661     else
662         txt = spritelookuptext(this, spriteimage);
663     if (this.helpme && time < this.helpme)
664         txt = sprintf(_("%s needing help!"), txt);
665     if (autocvar_g_waypointsprite_uppercase)
666         txt = strtoupper(txt);
667
668     draw_beginBoldFont();
669     if (this.health >= 0)
670     {
671         o = drawspritetext(o, ang, (SPRITE_HEALTHBAR_WIDTH + 2 * SPRITE_HEALTHBAR_BORDER) * t, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
672
673         float align, marg;
674         if (this.build_finished)
675             align = 0.5;
676         else
677             align = 0;
678         if (cos(ang) > 0)
679             marg = -(SPRITE_HEALTHBAR_MARGIN + SPRITE_HEALTHBAR_HEIGHT + 2 * SPRITE_HEALTHBAR_BORDER) * t - 0.5 * waypointsprite_fontsize;
680         else
681             marg = SPRITE_HEALTHBAR_MARGIN * t + 0.5 * waypointsprite_fontsize;
682         drawhealthbar(
683                 o,
684                 0,
685                 this.health,
686                 '0 0 0',
687                 '0 0 0',
688                 SPRITE_HEALTHBAR_WIDTH * t,
689                 SPRITE_HEALTHBAR_HEIGHT * t,
690                 marg,
691                 SPRITE_HEALTHBAR_BORDER * t,
692                 align,
693                 rgb,
694                 a * SPRITE_HEALTHBAR_BORDERALPHA,
695                 rgb,
696                 a * SPRITE_HEALTHBAR_HEALTHALPHA,
697                 DRAWFLAG_NORMAL
698                  );
699     }
700     else
701     {
702         o = drawspritetext(o, ang, 0, rgb, a, waypointsprite_fontsize * '1 1 0', txt);
703     }
704     draw_endBoldFont();
705 }
706
707 void WaypointSprite_Load_Frames(string ext)
708 {
709     int dh = search_begin(strcat("models/sprites/*_frame*", ext), false, false);
710     if (dh < 0) return;
711     int ext_len = strlen(ext);
712     int n = search_getsize(dh);
713     for (int i = 0; i < n; ++i)
714     {
715         string s = search_getfilename(dh, i);
716         s = substring(s, 15, strlen(s) - 15 - ext_len); // strip models/sprites/ and extension
717
718         int o = strstrofs(s, "_frame", 0);
719         string sname = strcat("/spriteframes/", substring(s, 0, o));
720         string sframes = substring(s, o + 6, strlen(s) - o - 6);
721         int f = stof(sframes) + 1;
722         db_put(tempdb, sname, ftos(max(f, stof(db_get(tempdb, sname)))));
723     }
724     search_end(dh);
725 }
726
727 void WaypointSprite_Load();
728 STATIC_INIT(WaypointSprite_Load) {
729     WaypointSprite_Load();
730     WaypointSprite_Load_Frames(".tga");
731     WaypointSprite_Load_Frames(".jpg");
732 }
733 void WaypointSprite_Load()
734 {
735     waypointsprite_fadedistance = vlen(mi_scale);
736     waypointsprite_normdistance = autocvar_g_waypointsprite_normdistance;
737     waypointsprite_minscale = autocvar_g_waypointsprite_minscale;
738     waypointsprite_minalpha = autocvar_g_waypointsprite_minalpha;
739     waypointsprite_distancealphaexponent = autocvar_g_waypointsprite_distancealphaexponent;
740     waypointsprite_timealphaexponent = autocvar_g_waypointsprite_timealphaexponent;
741     waypointsprite_scale = autocvar_g_waypointsprite_scale;
742     waypointsprite_fontsize = autocvar_g_waypointsprite_fontsize;
743     waypointsprite_edgefadealpha = autocvar_g_waypointsprite_edgefadealpha;
744     waypointsprite_edgefadescale = autocvar_g_waypointsprite_edgefadescale;
745     waypointsprite_edgefadedistance = autocvar_g_waypointsprite_edgefadedistance;
746     waypointsprite_edgeoffset_bottom = autocvar_g_waypointsprite_edgeoffset_bottom;
747     waypointsprite_edgeoffset_left = autocvar_g_waypointsprite_edgeoffset_left;
748     waypointsprite_edgeoffset_right = autocvar_g_waypointsprite_edgeoffset_right;
749     waypointsprite_edgeoffset_top = autocvar_g_waypointsprite_edgeoffset_top;
750     waypointsprite_crosshairfadealpha = autocvar_g_waypointsprite_crosshairfadealpha;
751     waypointsprite_crosshairfadescale = autocvar_g_waypointsprite_crosshairfadescale;
752     waypointsprite_crosshairfadedistance = autocvar_g_waypointsprite_crosshairfadedistance;
753     waypointsprite_distancefadealpha = autocvar_g_waypointsprite_distancefadealpha;
754     waypointsprite_distancefadescale = autocvar_g_waypointsprite_distancefadescale;
755     waypointsprite_distancefadedistance = waypointsprite_fadedistance * autocvar_g_waypointsprite_distancefadedistancemultiplier;
756     waypointsprite_alpha = autocvar_g_waypointsprite_alpha * (1 - autocvar__menu_alpha);
757
758     waypointsprite_count = waypointsprite_newcount;
759     waypointsprite_newcount = 0;
760 }
761 #endif
762
763 #ifdef SVQC
764 void WaypointSprite_UpdateSprites(entity e, entity _m1, entity _m2, entity _m3)
765 {
766     string m1 = _m1.netname;
767     string m2 = _m2.netname;
768     string m3 = _m3.netname;
769     if (m1 != e.model1)
770     {
771         e.model1 = m1;
772         e.SendFlags |= 2;
773     }
774     if (m2 != e.model2)
775     {
776         e.model2 = m2;
777         e.SendFlags |= 4;
778     }
779     if (m3 != e.model3)
780     {
781         e.model3 = m3;
782         e.SendFlags |= 8;
783     }
784 }
785
786 void WaypointSprite_UpdateHealth(entity e, float f)
787 {
788     f = bound(0, f, e.max_health);
789     if (f != e.health || e.pain_finished)
790     {
791         e.health = f;
792         e.pain_finished = 0;
793         e.SendFlags |= 0x80;
794     }
795 }
796
797 void WaypointSprite_UpdateMaxHealth(entity e, float f)
798 {
799     if (f != e.max_health || e.pain_finished)
800     {
801         e.max_health = f;
802         e.pain_finished = 0;
803         e.SendFlags |= 0x80;
804     }
805 }
806
807 void WaypointSprite_UpdateBuildFinished(entity e, float f)
808 {
809     if (f != e.pain_finished || e.max_health)
810     {
811         e.max_health = 0;
812         e.pain_finished = f;
813         e.SendFlags |= 0x80;
814     }
815 }
816
817 void WaypointSprite_UpdateOrigin(entity e, vector o)
818 {
819     if (o != e.origin)
820     {
821         setorigin(e, o);
822         e.SendFlags |= 64;
823     }
824 }
825
826 void WaypointSprite_UpdateRule(entity e, float t, float r)
827 {
828     // no check, as this is never called without doing an actual change (usually only once)
829     e.rule = r;
830     e.team = t;
831     e.SendFlags |= 1;
832 }
833
834 void WaypointSprite_UpdateTeamRadar(entity e, entity icon, vector col)
835 {
836     // no check, as this is never called without doing an actual change (usually only once)
837     int i = icon.m_id;
838     e.cnt = (e.cnt & BIT(7)) | (i & BITS(7));
839     e.colormod = col;
840     e.SendFlags |= 32;
841 }
842
843 void WaypointSprite_Ping(entity e)
844 {
845     // anti spam
846     if (time < e.waypointsprite_pingtime) return;
847     e.waypointsprite_pingtime = time + 0.3;
848     // ALWAYS sends (this causes a radar circle), thus no check
849     e.cnt |= BIT(7);
850     e.SendFlags |= 32;
851 }
852
853 void WaypointSprite_HelpMePing(entity e)
854 {
855     WaypointSprite_Ping(e);
856     e.waypointsprite_helpmetime = time + waypointsprite_deployed_lifetime;
857     e.SendFlags |= 32;
858 }
859
860 void WaypointSprite_FadeOutIn(entity e, float t)
861 {
862     if (!e.fade_time)
863     {
864         e.fade_time = t;
865         e.teleport_time = time + t;
866     }
867     else if (t < (e.teleport_time - time))
868     {
869         // accelerate the waypoint's dying
870         // ensure:
871         //   (e.teleport_time - time) / wp.fade_time stays
872         //   e.teleport_time = time + fadetime
873         float current_fadetime;
874         current_fadetime = e.teleport_time - time;
875         e.teleport_time = time + t;
876         if (e.fade_time < 0)
877                 e.fade_time = -e.fade_time;
878         e.fade_time = e.fade_time * t / current_fadetime;
879     }
880
881     e.SendFlags |= 16;
882 }
883
884 void WaypointSprite_Init()
885 {
886     waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
887     waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
888     waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
889 }
890
891 void WaypointSprite_Kill(entity wp)
892 {
893     if (!wp) return;
894     if (wp.owner) wp.owner.(wp.owned_by_field) = NULL;
895     delete(wp);
896 }
897
898 void WaypointSprite_Disown(entity wp, float fadetime)
899 {
900     if (!wp) return;
901     if (wp.classname != "sprite_waypoint")
902     {
903         backtrace("Trying to disown a non-waypointsprite");
904         return;
905     }
906     if (wp.owner)
907     {
908         if (wp.exteriormodeltoclient == wp.owner)
909             wp.exteriormodeltoclient = NULL;
910         wp.owner.(wp.owned_by_field) = NULL;
911         wp.owner = NULL;
912
913         WaypointSprite_FadeOutIn(wp, fadetime);
914     }
915 }
916
917 void WaypointSprite_Think(entity this)
918 {
919     bool doremove = false;
920
921     if (this.fade_time && time >= this.teleport_time)
922     {
923         doremove = true;
924     }
925
926     if (this.exteriormodeltoclient)
927         WaypointSprite_UpdateOrigin(this, this.exteriormodeltoclient.origin + this.view_ofs);
928
929     if (doremove)
930         WaypointSprite_Kill(this);
931     else
932         this.nextthink = time; // WHY?!?
933 }
934
935 bool WaypointSprite_visible_for_player(entity this, entity player, entity view)
936 {
937     // personal waypoints
938     if (this.enemy && this.enemy != view)
939         return false;
940
941     // team waypoints
942     if (this.rule == SPRITERULE_SPECTATOR)
943     {
944         if (!autocvar_sv_itemstime)
945             return false;
946         if (!warmup_stage && IS_PLAYER(view) && autocvar_sv_itemstime != 2)
947             return false;
948     }
949     else if (this.team && this.rule == SPRITERULE_DEFAULT)
950     {
951         if (this.team != view.team)
952             return false;
953         if (!IS_PLAYER(view))
954             return false;
955     }
956
957     return true;
958 }
959
960 entity WaypointSprite_getviewentity(entity e)
961 {
962     if (IS_SPEC(e)) e = e.enemy;
963     /* TODO idea (check this breaks nothing)
964     else if (e.classname == "observer")
965         e = NULL;
966     */
967     return e;
968 }
969
970 float WaypointSprite_isteammate(entity e, entity e2)
971 {
972     if (teamplay)
973         return e2.team == e.team;
974     return e2 == e;
975 }
976
977 bool WaypointSprite_Customize(entity this, entity client)
978 {
979     // this is not in SendEntity because it shall run every frame, not just every update
980
981     // make spectators see what the player would see
982     entity e = WaypointSprite_getviewentity(client);
983
984     if (MUTATOR_CALLHOOK(CustomizeWaypoint, this, client))
985         return false;
986
987     return this.waypointsprite_visible_for_player(this, client, e);
988 }
989
990 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
991
992 void WaypointSprite_Reset(entity this)
993 {
994     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
995
996     if (this.fade_time)
997         WaypointSprite_Kill(this);
998 }
999
1000 entity WaypointSprite_Spawn(
1001     entity spr, // sprite
1002     float _lifetime, float maxdistance, // lifetime, max distance
1003     entity ref, vector ofs, // position
1004     entity showto, float t, // show to whom? Use a flag to indicate a team
1005     entity own, .entity ownfield, // remove when own gets killed
1006     float hideable, // true when it should be controlled by cl_hidewaypoints
1007     entity icon // initial icon
1008 )
1009 {
1010     entity wp = new(sprite_waypoint);
1011     wp.fade_time = _lifetime; // if negative tells client not to fade it out
1012     if(_lifetime < 0)
1013         _lifetime = -_lifetime;
1014     wp.teleport_time = time + _lifetime;
1015     wp.exteriormodeltoclient = ref;
1016     if (ref)
1017     {
1018         wp.view_ofs = ofs;
1019         setorigin(wp, ref.origin + ofs);
1020     }
1021     else
1022         setorigin(wp, ofs);
1023     wp.enemy = showto;
1024     wp.team = t;
1025     wp.owner = own;
1026     wp.currentammo = hideable;
1027     if (own)
1028     {
1029         if (own.(ownfield))
1030             delete(own.(ownfield));
1031         own.(ownfield) = wp;
1032         wp.owned_by_field = ownfield;
1033     }
1034     wp.fade_rate = maxdistance;
1035     setthink(wp, WaypointSprite_Think);
1036     wp.nextthink = time;
1037     wp.model1 = spr.netname;
1038     setcefc(wp, WaypointSprite_Customize);
1039     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1040     wp.reset2 = WaypointSprite_Reset;
1041     wp.cnt = icon.m_id;
1042     wp.colormod = spr.m_color;
1043     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1044     return wp;
1045 }
1046
1047 entity WaypointSprite_SpawnFixed(
1048     entity spr,
1049     vector ofs,
1050     entity own,
1051     .entity ownfield,
1052     entity icon // initial icon
1053 )
1054 {
1055     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, own, ownfield, true, icon);
1056 }
1057
1058 entity WaypointSprite_DeployFixed(
1059     entity spr,
1060     float limited_range,
1061     entity player,
1062     vector ofs,
1063     entity icon // initial icon
1064 )
1065 {
1066     float t;
1067     if (teamplay)
1068         t = player.team;
1069     else
1070         t = 0;
1071     float maxdistance;
1072     if (limited_range)
1073         maxdistance = waypointsprite_limitedrange;
1074     else
1075         maxdistance = 0;
1076     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, NULL, ofs, NULL, t, player, waypointsprite_deployed_fixed, false, icon);
1077 }
1078
1079 entity WaypointSprite_DeployPersonal(
1080     entity spr,
1081     entity player,
1082     vector ofs,
1083     entity icon // initial icon
1084 )
1085 {
1086     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, player, waypointsprite_deployed_personal, false, icon);
1087 }
1088
1089 entity WaypointSprite_Attach(
1090     entity spr,
1091     entity player,
1092     float limited_range,
1093     entity icon // initial icon
1094 )
1095 {
1096     float t;
1097     if (player.waypointsprite_attachedforcarrier)
1098         return NULL; // can't attach to FC
1099     if (teamplay)
1100         t = player.team;
1101     else
1102         t = 0;
1103     float maxdistance;
1104     if (limited_range)
1105         maxdistance = waypointsprite_limitedrange;
1106     else
1107         maxdistance = 0;
1108     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, player, '0 0 64', NULL, t, player, waypointsprite_attached, false, icon);
1109 }
1110
1111 entity WaypointSprite_AttachCarrier(
1112     entity spr,
1113     entity carrier,
1114     entity icon // initial icon and color
1115 )
1116 {
1117     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1118     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', NULL, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1119     if (carrier.health)
1120     {
1121         WaypointSprite_UpdateMaxHealth(e, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
1122         WaypointSprite_UpdateHealth(e, '1 0 0' * healtharmor_maxdamage(carrier.health, carrier.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
1123     }
1124     return e;
1125 }
1126
1127 void WaypointSprite_DetachCarrier(entity carrier)
1128 {
1129     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1130 }
1131
1132 void WaypointSprite_ClearPersonal(entity this)
1133 {
1134     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1135 }
1136
1137 void WaypointSprite_ClearOwned(entity this)
1138 {
1139     WaypointSprite_Kill(this.waypointsprite_deployed_fixed);
1140     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1141     WaypointSprite_Kill(this.waypointsprite_attached);
1142 }
1143
1144 void WaypointSprite_PlayerDead(entity this)
1145 {
1146     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1147     WaypointSprite_DetachCarrier(this);
1148 }
1149
1150 void WaypointSprite_PlayerGone(entity this)
1151 {
1152     WaypointSprite_Disown(this.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1153     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1154     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1155     WaypointSprite_DetachCarrier(this);
1156 }
1157 #endif
1158 #endif