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