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