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