]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/blob - qcsrc/common/mutators/mutator/waypoints/waypointsprites.qc
s/world/NULL/
[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
28     WriteByte(MSG_ENTITY, sendflags);
29     WriteByte(MSG_ENTITY, this.wp_extra);
30
31     if (sendflags & 0x80)
32     {
33         if (this.max_health)
34         {
35             WriteByte(MSG_ENTITY, (this.health / this.max_health) * 191.0);
36         }
37         else
38         {
39             float dt = this.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, this.origin.x);
49         WriteCoord(MSG_ENTITY, this.origin.y);
50         WriteCoord(MSG_ENTITY, this.origin.z);
51     }
52
53     if (sendflags & 1)
54     {
55         WriteByte(MSG_ENTITY, this.team);
56         WriteByte(MSG_ENTITY, this.rule);
57     }
58
59     if (sendflags & 2)
60         WriteString(MSG_ENTITY, this.model1);
61
62     if (sendflags & 4)
63         WriteString(MSG_ENTITY, this.model2);
64
65     if (sendflags & 8)
66         WriteString(MSG_ENTITY, this.model3);
67
68     if (sendflags & 16)
69     {
70         WriteCoord(MSG_ENTITY, this.fade_time);
71         WriteCoord(MSG_ENTITY, this.teleport_time);
72         WriteShort(MSG_ENTITY, this.fade_rate); // maxdist
73         WriteByte(MSG_ENTITY, f);
74     }
75
76     if (sendflags & 32)
77     {
78         WriteByte(MSG_ENTITY, this.cnt); // icon on radar
79         WriteByte(MSG_ENTITY, this.colormod.x * 255.0);
80         WriteByte(MSG_ENTITY, this.colormod.y * 255.0);
81         WriteByte(MSG_ENTITY, this.colormod.z * 255.0);
82
83         if (WaypointSprite_isteammate(this.owner, WaypointSprite_getviewentity(to)))
84         {
85             float dt = (this.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(entity this);
102 NET_HANDLE(waypointsprites, bool isnew) {
103     Ent_WaypointSprite(this);
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(entity this)
116 {
117     int sendflags = ReadByte();
118     this.wp_extra = ReadByte();
119
120     if (!this.spawntime)
121         this.spawntime = time;
122
123     this.draw2d = Draw_WaypointSprite;
124
125     InterpolateOrigin_Undo(this);
126     this.iflags |= IFLAG_ORIGIN;
127
128     if (sendflags & 0x80)
129     {
130         int t = ReadByte();
131         if (t < 192)
132         {
133             this.health = t / 191.0;
134             this.build_finished = 0;
135         }
136         else
137         {
138             t = (t - 192) * 256 + ReadByte();
139             this.build_started = servertime;
140             if (this.build_finished)
141                 this.build_starthealth = bound(0, this.health, 1);
142             else
143                 this.build_starthealth = 0;
144             this.build_finished = servertime + t / 32;
145         }
146     }
147     else
148     {
149         this.health = -1;
150         this.build_finished = 0;
151     }
152
153     if (sendflags & 64)
154     {
155         // unfortunately, this needs to be exact (for the 3D display)
156         this.origin_x = ReadCoord();
157         this.origin_y = ReadCoord();
158         this.origin_z = ReadCoord();
159         setorigin(this, this.origin);
160     }
161
162     if (sendflags & 1)
163     {
164         this.team = ReadByte();
165         this.rule = ReadByte();
166     }
167
168     if (sendflags & 2)
169     {
170         if (this.netname)
171             strunzone(this.netname);
172         this.netname = strzone(ReadString());
173     }
174
175     if (sendflags & 4)
176     {
177         if (this.netname2)
178             strunzone(this.netname2);
179         this.netname2 = strzone(ReadString());
180     }
181
182     if (sendflags & 8)
183     {
184         if (this.netname3)
185             strunzone(this.netname3);
186         this.netname3 = strzone(ReadString());
187     }
188
189     if (sendflags & 16)
190     {
191         this.lifetime = ReadCoord();
192         this.fadetime = ReadCoord();
193         this.maxdistance = ReadShort();
194         this.hideflags = ReadByte();
195     }
196
197     if (sendflags & 32)
198     {
199         int f = ReadByte();
200         this.teamradar_icon = f & BITS(7);
201         if (f & BIT(7))
202         {
203             this.(teamradar_times[this.teamradar_time_index]) = time;
204             this.teamradar_time_index = (this.teamradar_time_index + 1) % MAX_TEAMRADAR_TIMES;
205         }
206         this.teamradar_color_x = ReadByte() / 255.0;
207         this.teamradar_color_y = ReadByte() / 255.0;
208         this.teamradar_color_z = ReadByte() / 255.0;
209         this.helpme = ReadByte() * 0.1;
210         if (this.helpme > 0)
211             this.helpme += servertime;
212     }
213
214     InterpolateOrigin_Note(this);
215
216     this.entremove = Ent_RemoveWaypointSprite;
217 }
218 #endif
219
220 #ifdef CSQC
221 float spritelookupblinkvalue(entity this, string s)
222 {
223     if (s == WP_Weapon.netname) {
224         if (Weapons_from(this.wp_extra).spawnflags & WEP_FLAG_SUPERWEAPON)
225             return 2;
226     }
227     if (s == WP_Item.netname) return Items_from(this.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 M_ARGV(2, vector);
239     }
240     return def;
241 }
242
243 string spritelookuptext(entity this, string s)
244 {
245     if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
246     if (s == WP_Weapon.netname) return Weapons_from(this.wp_extra).m_name;
247     if (s == WP_Item.netname) return Items_from(this.wp_extra).m_waypoint;
248     if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).monster_name;
249     if (MUTATOR_CALLHOOK(WP_Format, this, s))
250     {
251         return M_ARGV(3, 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(this, 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(this, 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) = NULL;
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 = NULL;
899         wp.owner.(wp.owned_by_field) = NULL;
900         wp.owner = NULL;
901
902         WaypointSprite_FadeOutIn(wp, fadetime);
903     }
904 }
905
906 void WaypointSprite_Think(entity this)
907 {
908     bool doremove = false;
909
910     if (this.fade_time && time >= this.teleport_time)
911     {
912         doremove = true;
913     }
914
915     if (this.exteriormodeltoclient)
916         WaypointSprite_UpdateOrigin(this, this.exteriormodeltoclient.origin + this.view_ofs);
917
918     if (doremove)
919         WaypointSprite_Kill(this);
920     else
921         this.nextthink = time; // WHY?!?
922 }
923
924 bool WaypointSprite_visible_for_player(entity this, entity player, entity view)
925 {
926     // personal waypoints
927     if (this.enemy && this.enemy != view)
928         return false;
929
930     // team waypoints
931     if (this.rule == SPRITERULE_SPECTATOR)
932     {
933         if (!autocvar_sv_itemstime)
934             return false;
935         if (!warmup_stage && IS_PLAYER(view))
936             return false;
937     }
938     else if (this.team && this.rule == SPRITERULE_DEFAULT)
939     {
940         if (this.team != view.team)
941             return false;
942         if (!IS_PLAYER(view))
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 = NULL;
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(entity this)
967 {
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, this, other))
974         return false;
975
976     return this.waypointsprite_visible_for_player(this, other, e);
977 }
978
979 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
980
981 void WaypointSprite_Reset(entity this)
982 {
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 (this.fade_time)
986         WaypointSprite_Kill(this);
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     wp.teleport_time = time + _lifetime;
1001     wp.fade_time = _lifetime;
1002     wp.exteriormodeltoclient = ref;
1003     if (ref)
1004     {
1005         wp.view_ofs = ofs;
1006         setorigin(wp, ref.origin + ofs);
1007     }
1008     else
1009         setorigin(wp, ofs);
1010     wp.enemy = showto;
1011     wp.team = t;
1012     wp.owner = own;
1013     wp.currentammo = hideable;
1014     if (own)
1015     {
1016         if (own.(ownfield))
1017             remove(own.(ownfield));
1018         own.(ownfield) = wp;
1019         wp.owned_by_field = ownfield;
1020     }
1021     wp.fade_rate = maxdistance;
1022     setthink(wp, WaypointSprite_Think);
1023     wp.nextthink = time;
1024     wp.model1 = spr.netname;
1025     setcefc(wp, WaypointSprite_Customize);
1026     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1027     wp.reset2 = WaypointSprite_Reset;
1028     wp.cnt = icon.m_id;
1029     wp.colormod = spr.m_color;
1030     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1031     return wp;
1032 }
1033
1034 entity WaypointSprite_SpawnFixed(
1035     entity spr,
1036     vector ofs,
1037     entity own,
1038     .entity ownfield,
1039     entity icon // initial icon
1040 )
1041 {
1042     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, own, ownfield, true, icon);
1043 }
1044
1045 entity WaypointSprite_DeployFixed(
1046     entity spr,
1047     float limited_range,
1048     entity player,
1049     vector ofs,
1050     entity icon // initial icon
1051 )
1052 {
1053     float t;
1054     if (teamplay)
1055         t = player.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, NULL, ofs, NULL, t, player, waypointsprite_deployed_fixed, false, icon);
1064 }
1065
1066 entity WaypointSprite_DeployPersonal(
1067     entity spr,
1068     entity player,
1069     vector ofs,
1070     entity icon // initial icon
1071 )
1072 {
1073     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, player, waypointsprite_deployed_personal, false, icon);
1074 }
1075
1076 entity WaypointSprite_Attach(
1077     entity spr,
1078     entity player,
1079     float limited_range,
1080     entity icon // initial icon
1081 )
1082 {
1083     float t;
1084     if (player.waypointsprite_attachedforcarrier)
1085         return NULL; // can't attach to FC
1086     if (teamplay)
1087         t = player.team;
1088     else
1089         t = 0;
1090     float maxdistance;
1091     if (limited_range)
1092         maxdistance = waypointsprite_limitedrange;
1093     else
1094         maxdistance = 0;
1095     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, player, '0 0 64', NULL, t, player, waypointsprite_attached, false, icon);
1096 }
1097
1098 entity WaypointSprite_AttachCarrier(
1099     entity spr,
1100     entity carrier,
1101     entity icon // initial icon and color
1102 )
1103 {
1104     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1105     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', NULL, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1106     if (carrier.health)
1107     {
1108         WaypointSprite_UpdateMaxHealth(e, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
1109         WaypointSprite_UpdateHealth(e, '1 0 0' * healtharmor_maxdamage(carrier.health, carrier.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
1110     }
1111     return e;
1112 }
1113
1114 void WaypointSprite_DetachCarrier(entity carrier)
1115 {
1116     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1117 }
1118
1119 void WaypointSprite_ClearPersonal(entity this)
1120 {
1121     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1122 }
1123
1124 void WaypointSprite_ClearOwned(entity this)
1125 {
1126     WaypointSprite_Kill(this.waypointsprite_deployed_fixed);
1127     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1128     WaypointSprite_Kill(this.waypointsprite_attached);
1129 }
1130
1131 void WaypointSprite_PlayerDead(entity this)
1132 {
1133     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1134     WaypointSprite_DetachCarrier(this);
1135 }
1136
1137 void WaypointSprite_PlayerGone(entity this)
1138 {
1139     WaypointSprite_Disown(this.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1140     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1141     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1142     WaypointSprite_DetachCarrier(this);
1143 }
1144 #endif
1145 #endif