Don't #include server files from CSQC
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / mutators / mutator / waypoints / waypointsprites.qc
1 #include "waypointsprites.qh"
2
3 REGISTER_MUTATOR(waypointsprites, true);
4
5 REGISTER_NET_LINKED(waypointsprites)
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     WriteHeader(MSG_ENTITY, waypointsprites);
12
13     sendflags = sendflags & 0x7F;
14
15     if (this.max_health || (this.pain_finished && (time < this.pain_finished + 0.25)))
16         sendflags |= 0x80;
17
18     int f = 0;
19     if(this.currentammo)
20         f |= 1; // hideable
21     if(this.exteriormodeltoclient == to)
22         f |= 2; // my own
23
24     MUTATOR_CALLHOOK(SendWaypoint, this, to, sendflags, f);
25     sendflags = M_ARGV(2, int);
26     f = M_ARGV(3, int);
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, bool isnew);
102 NET_HANDLE(waypointsprites, bool isnew) {
103     Ent_WaypointSprite(this, isnew);
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, bool isnew)
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     if (isnew) {
125                 IL_PUSH(g_drawables_2d, this);
126                 IL_PUSH(g_radaricons, this);
127     }
128
129     InterpolateOrigin_Undo(this);
130     this.iflags |= IFLAG_ORIGIN;
131
132     if (sendflags & 0x80)
133     {
134         int t = ReadByte();
135         if (t < 192)
136         {
137             this.health = t / 191.0;
138             this.build_finished = 0;
139         }
140         else
141         {
142             t = (t - 192) * 256 + ReadByte();
143             this.build_started = servertime;
144             if (this.build_finished)
145                 this.build_starthealth = bound(0, this.health, 1);
146             else
147                 this.build_starthealth = 0;
148             this.build_finished = servertime + t / 32;
149         }
150     }
151     else
152     {
153         this.health = -1;
154         this.build_finished = 0;
155     }
156
157     if (sendflags & 64)
158     {
159         // unfortunately, this needs to be exact (for the 3D display)
160         this.origin_x = ReadCoord();
161         this.origin_y = ReadCoord();
162         this.origin_z = ReadCoord();
163         setorigin(this, this.origin);
164     }
165
166     if (sendflags & 1)
167     {
168         this.team = ReadByte();
169         this.rule = ReadByte();
170     }
171
172     if (sendflags & 2)
173     {
174         if (this.netname)
175             strunzone(this.netname);
176         this.netname = strzone(ReadString());
177     }
178
179     if (sendflags & 4)
180     {
181         if (this.netname2)
182             strunzone(this.netname2);
183         this.netname2 = strzone(ReadString());
184     }
185
186     if (sendflags & 8)
187     {
188         if (this.netname3)
189             strunzone(this.netname3);
190         this.netname3 = strzone(ReadString());
191     }
192
193     if (sendflags & 16)
194     {
195         this.lifetime = ReadCoord();
196         this.fadetime = ReadCoord();
197         this.maxdistance = ReadShort();
198         this.hideflags = ReadByte();
199     }
200
201     if (sendflags & 32)
202     {
203         int f = ReadByte();
204         this.teamradar_icon = f & BITS(7);
205         if (f & BIT(7))
206         {
207             this.(teamradar_times[this.teamradar_time_index]) = time;
208             this.teamradar_time_index = (this.teamradar_time_index + 1) % MAX_TEAMRADAR_TIMES;
209         }
210         this.teamradar_color_x = ReadByte() / 255.0;
211         this.teamradar_color_y = ReadByte() / 255.0;
212         this.teamradar_color_z = ReadByte() / 255.0;
213         this.helpme = ReadByte() * 0.1;
214         if (this.helpme > 0)
215             this.helpme += servertime;
216     }
217
218     InterpolateOrigin_Note(this);
219
220     this.entremove = Ent_RemoveWaypointSprite;
221 }
222 #endif
223
224 #ifdef CSQC
225 float spritelookupblinkvalue(entity this, string s)
226 {
227     if (s == WP_Weapon.netname) {
228         if (Weapons_from(this.wp_extra).spawnflags & WEP_FLAG_SUPERWEAPON)
229             return 2;
230     }
231     if (s == WP_Item.netname) return Items_from(this.wp_extra).m_waypointblink;
232     if(s == WP_FlagReturn.netname) return 2;
233
234     return 1;
235 }
236
237 vector spritelookupcolor(entity this, string s, vector def)
238 {
239     if (s == WP_Weapon.netname  || s == RADARICON_Weapon.netname) return Weapons_from(this.wp_extra).wpcolor;
240     if (s == WP_Item.netname    || s == RADARICON_Item.netname) return Items_from(this.wp_extra).m_color;
241     if (MUTATOR_CALLHOOK(WP_Format, this, s))
242     {
243         return M_ARGV(2, vector);
244     }
245     return def;
246 }
247
248 string spritelookuptext(entity this, string s)
249 {
250     if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
251     if (s == WP_Weapon.netname) return Weapons_from(this.wp_extra).m_name;
252     if (s == WP_Item.netname) return Items_from(this.wp_extra).m_waypoint;
253     if (s == WP_Monster.netname) return get_monsterinfo(this.wp_extra).monster_name;
254     if (MUTATOR_CALLHOOK(WP_Format, this, s))
255     {
256         return M_ARGV(3, string);
257     }
258
259     // need to loop, as our netname could be one of three
260     FOREACH(Waypoints, it.netname == s, LAMBDA(
261         return it.m_name;
262     ));
263
264     return s;
265 }
266 #endif
267
268 #ifdef CSQC
269 void drawrotpic(vector org, float rot, string pic, vector sz, vector hotspot, vector rgb, float a, float f)
270 {
271     vector v1, v2, v3, v4;
272
273     hotspot = -1 * hotspot;
274
275     // hotspot-relative coordinates of the corners
276     v1 = hotspot;
277     v2 = hotspot + '1 0 0' * sz.x;
278     v3 = hotspot + '1 0 0' * sz.x + '0 1 0' * sz.y;
279     v4 = hotspot                  + '0 1 0' * sz.y;
280
281     // rotate them, and make them absolute
282     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
283     v1 = rotate(v1, rot) + org;
284     v2 = rotate(v2, rot) + org;
285     v3 = rotate(v3, rot) + org;
286     v4 = rotate(v4, rot) + org;
287
288     // draw them
289     R_BeginPolygon(pic, f);
290     R_PolygonVertex(v1, '0 0 0', rgb, a);
291     R_PolygonVertex(v2, '1 0 0', rgb, a);
292     R_PolygonVertex(v3, '1 1 0', rgb, a);
293     R_PolygonVertex(v4, '0 1 0', rgb, a);
294     R_EndPolygon();
295 }
296
297 void drawquad(vector o, vector ri, vector up, string pic, vector rgb, float a, float f)
298 {
299     R_BeginPolygon(pic, f);
300     R_PolygonVertex(o, '0 0 0', rgb, a);
301     R_PolygonVertex(o + ri, '1 0 0', rgb, a);
302     R_PolygonVertex(o + up + ri, '1 1 0', rgb, a);
303     R_PolygonVertex(o + up, '0 1 0', rgb, a);
304     R_EndPolygon();
305 }
306
307 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)
308 {
309     vector o, ri, up;
310     float owidth; // outer width
311
312     hotspot = -1 * hotspot;
313
314     // hotspot-relative coordinates of the healthbar corners
315     o = hotspot;
316     ri = '1 0 0';
317     up = '0 1 0';
318
319     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
320     o = rotate(o, rot) + org;
321     ri = rotate(ri, rot);
322     up = rotate(up, rot);
323
324     owidth = width + 2 * border;
325     o = o - up * (margin + border + theheight) + ri * (sz.x - owidth) * 0.5;
326
327     drawquad(o - up * border,                               ri * owidth,    up * border,    "", rgb,  a,  f);
328     drawquad(o + up * theheight,                            ri * owidth,    up * border,    "", rgb,  a,  f);
329     drawquad(o,                                             ri * border,    up * theheight, "", rgb,  a,  f);
330     drawquad(o + ri * (owidth - border),                    ri * border,    up * theheight, "", rgb,  a,  f);
331     drawquad(o + ri * (border + align * ((1 - h) * width)), ri * width * h, up * theheight, "", hrgb, ha, f);
332 }
333
334 // returns location of sprite text
335 vector drawspritearrow(vector o, float ang, vector rgb, float a, float t)
336 {
337     float size   = 9.0 * t;
338     float border = 1.5 * t;
339     float margin = 4.0 * t;
340
341     float borderDiag = border * 1.414;
342     vector arrowX  = eX * size;
343     vector arrowY  = eY * (size+borderDiag);
344     vector borderX = eX * (size+borderDiag);
345     vector borderY = eY * (size+borderDiag+border);
346
347     R_BeginPolygon("", DRAWFLAG_NORMAL);
348     R_PolygonVertex(o,                                  '0 0 0', '0 0 0', a);
349     R_PolygonVertex(o + rotate(arrowY  - borderX, ang), '0 0 0', '0 0 0', a);
350     R_PolygonVertex(o + rotate(borderY - borderX, ang), '0 0 0', '0 0 0', a);
351     R_PolygonVertex(o + rotate(borderY + borderX, ang), '0 0 0', '0 0 0', a);
352     R_PolygonVertex(o + rotate(arrowY  + borderX, ang), '0 0 0', '0 0 0', a);
353     R_EndPolygon();
354
355     R_BeginPolygon("", DRAWFLAG_ADDITIVE);
356     R_PolygonVertex(o + rotate(eY * borderDiag, ang), '0 0 0', rgb, a);
357     R_PolygonVertex(o + rotate(arrowY - arrowX, ang), '0 0 0', rgb, a);
358     R_PolygonVertex(o + rotate(arrowY + arrowX, ang), '0 0 0', rgb, a);
359     R_EndPolygon();
360
361     return o + rotate(eY * (borderDiag+size+margin), ang);
362 }
363
364 // returns location of sprite healthbar
365 vector drawspritetext(vector o, float ang, float minwidth, vector rgb, float a, vector fontsize, string s)
366 {
367     float algnx, algny;
368     float sw, w, h;
369     float aspect, sa, ca;
370
371     sw = stringwidth(s, false, fontsize);
372     if (sw > minwidth)
373         w = sw;
374     else
375         w = minwidth;
376     h = fontsize.y;
377
378     // how do corners work?
379     aspect = vid_conwidth / vid_conheight;
380     sa = sin(ang);
381     ca = cos(ang) * aspect;
382     if (fabs(sa) > fabs(ca))
383     {
384         algnx = (sa < 0);
385         float f = fabs(sa);
386         algny = 0.5 - 0.5 * (f ? (ca / f) : 0);
387     }
388     else
389     {
390         float f = fabs(ca);
391         algnx = 0.5 - 0.5 * (f ? (sa / f) : 0);
392         algny = (ca < 0);
393     }
394
395     // align
396     o.x -= w * algnx;
397     o.y -= h * algny;
398
399     // we want to be onscreen
400     if (o.x < 0)
401         o.x = 0;
402     if (o.y < 0)
403         o.y = 0;
404     if (o.x > vid_conwidth - w)
405         o.x = vid_conwidth - w;
406     if (o.y > vid_conheight - h)
407         o.x = vid_conheight - h;
408
409     o.x += 0.5 * (w - sw);
410
411     drawstring(o, s, fontsize, rgb, a, DRAWFLAG_NORMAL);
412
413     o.x += 0.5 * sw;
414     o.y += 0.5 * h;
415
416     return o;
417 }
418
419 vector fixrgbexcess_move(vector rgb, vector src, vector dst)
420 {
421     vector yvec = '0.299 0.587 0.114';
422     return rgb + dst * ((src * yvec) / (dst * yvec)) * ((rgb - '1 1 1') * src);
423 }
424
425 vector fixrgbexcess(vector rgb)
426 {
427     if (rgb.x > 1) {
428         rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 1');
429         if (rgb.y > 1) {
430             rgb = fixrgbexcess_move(rgb, '0 1 0', '0 0 1');
431             if (rgb.z > 1) rgb.z = 1;
432         } else if (rgb.z > 1) {
433             rgb = fixrgbexcess_move(rgb, '0 0 1', '0 1 0');
434             if (rgb.y > 1) rgb.y = 1;
435         }
436     } else if (rgb.y > 1) {
437         rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 1');
438         if (rgb.x > 1) {
439             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 0 1');
440             if (rgb.z > 1) rgb.z = 1;
441         } else if (rgb.z > 1) {
442             rgb = fixrgbexcess_move(rgb, '0 0 1', '1 0 0');
443             if (rgb.x > 1) rgb.x = 1;
444         }
445     } else if (rgb.z > 1) {
446         rgb = fixrgbexcess_move(rgb, '0 0 1', '1 1 0');
447         if (rgb.x > 1) {
448             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 0');
449             if (rgb.y > 1) rgb.y = 1;
450         } else if (rgb.y > 1) {
451             rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 0');
452             if (rgb.x > 1) rgb.x = 1;
453         }
454     }
455     return rgb;
456 }
457
458 void Draw_WaypointSprite(entity this)
459 {
460     if (this.lifetime > 0)
461         this.alpha = pow(bound(0, (this.fadetime - time) / this.lifetime, 1), waypointsprite_timealphaexponent);
462     else
463         this.alpha = 1;
464
465     if (this.hideflags & 2)
466         return; // radar only
467
468     if (autocvar_cl_hidewaypoints >= 2)
469         return;
470
471     if (this.hideflags & 1 && autocvar_cl_hidewaypoints)
472         return; // fixed waypoint
473
474     InterpolateOrigin_Do(this);
475
476     float t = entcs_GetTeam(player_localnum) + 1;
477     string spriteimage = "";
478
479     // choose the sprite
480     switch (this.rule)
481     {
482         case SPRITERULE_SPECTATOR:
483             if (!(
484                 (autocvar_g_waypointsprite_itemstime == 1 && t == NUM_SPECTATOR + 1)
485             ||  (autocvar_g_waypointsprite_itemstime == 2 && (t == NUM_SPECTATOR + 1 || warmup_stage || STAT(ITEMSTIME) == 2))
486                 ))
487                 return;
488             spriteimage = this.netname;
489             break;
490         case SPRITERULE_DEFAULT:
491             if (this.team)
492             {
493                 if (this.team == t)
494                     spriteimage = this.netname;
495                 else
496                     spriteimage = "";
497             }
498             else
499                 spriteimage = this.netname;
500             break;
501         case SPRITERULE_TEAMPLAY:
502             if (t == NUM_SPECTATOR + 1)
503                 spriteimage = this.netname3;
504             else if (this.team == t)
505                 spriteimage = this.netname2;
506             else
507                 spriteimage = this.netname;
508             break;
509         default:
510             error("Invalid waypointsprite rule!");
511             break;
512     }
513
514     if (spriteimage == "")
515         return;
516
517     ++waypointsprite_newcount;
518
519     float dist = vlen(this.origin - view_origin);
520     float 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 > 0) // 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 = e.teleport_time - time;
865         e.teleport_time = time + t;
866         if (e.fade_time < 0)
867                 e.fade_time = -e.fade_time;
868         e.fade_time = e.fade_time * t / current_fadetime;
869     }
870
871     e.SendFlags |= 16;
872 }
873
874 void WaypointSprite_Init()
875 {
876     waypointsprite_limitedrange = autocvar_sv_waypointsprite_limitedrange;
877     waypointsprite_deployed_lifetime = autocvar_sv_waypointsprite_deployed_lifetime;
878     waypointsprite_deadlifetime = autocvar_sv_waypointsprite_deadlifetime;
879 }
880
881 void WaypointSprite_Kill(entity wp)
882 {
883     if (!wp) return;
884     if (wp.owner) wp.owner.(wp.owned_by_field) = NULL;
885     delete(wp);
886 }
887
888 void WaypointSprite_Disown(entity wp, float fadetime)
889 {
890     if (!wp) return;
891     if (wp.classname != "sprite_waypoint")
892     {
893         backtrace("Trying to disown a non-waypointsprite");
894         return;
895     }
896     if (wp.owner)
897     {
898         if (wp.exteriormodeltoclient == wp.owner)
899             wp.exteriormodeltoclient = NULL;
900         wp.owner.(wp.owned_by_field) = NULL;
901         wp.owner = NULL;
902
903         WaypointSprite_FadeOutIn(wp, fadetime);
904     }
905 }
906
907 void WaypointSprite_Think(entity this)
908 {
909     bool doremove = false;
910
911     if (this.fade_time && time >= this.teleport_time)
912     {
913         doremove = true;
914     }
915
916     if (this.exteriormodeltoclient)
917         WaypointSprite_UpdateOrigin(this, this.exteriormodeltoclient.origin + this.view_ofs);
918
919     if (doremove)
920         WaypointSprite_Kill(this);
921     else
922         this.nextthink = time; // WHY?!?
923 }
924
925 bool WaypointSprite_visible_for_player(entity this, entity player, entity view)
926 {
927     // personal waypoints
928     if (this.enemy && this.enemy != view)
929         return false;
930
931     // team waypoints
932     if (this.rule == SPRITERULE_SPECTATOR)
933     {
934         if (!autocvar_sv_itemstime)
935             return false;
936         if (!warmup_stage && IS_PLAYER(view) && autocvar_sv_itemstime != 2)
937             return false;
938     }
939     else if (this.team && this.rule == SPRITERULE_DEFAULT)
940     {
941         if (this.team != view.team)
942             return false;
943         if (!IS_PLAYER(view))
944             return false;
945     }
946
947     return true;
948 }
949
950 entity WaypointSprite_getviewentity(entity e)
951 {
952     if (IS_SPEC(e)) e = e.enemy;
953     /* TODO idea (check this breaks nothing)
954     else if (e.classname == "observer")
955         e = NULL;
956     */
957     return e;
958 }
959
960 float WaypointSprite_isteammate(entity e, entity e2)
961 {
962     if (teamplay)
963         return e2.team == e.team;
964     return e2 == e;
965 }
966
967 bool WaypointSprite_Customize(entity this, entity client)
968 {
969     // this is not in SendEntity because it shall run every frame, not just every update
970
971     // make spectators see what the player would see
972     entity e = WaypointSprite_getviewentity(client);
973
974     if (MUTATOR_CALLHOOK(CustomizeWaypoint, this, client))
975         return false;
976
977     return this.waypointsprite_visible_for_player(this, client, e);
978 }
979
980 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
981
982 void WaypointSprite_Reset(entity this)
983 {
984     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
985
986     if (this.fade_time)
987         WaypointSprite_Kill(this);
988 }
989
990 entity WaypointSprite_Spawn(
991     entity spr, // sprite
992     float _lifetime, float maxdistance, // lifetime, max distance
993     entity ref, vector ofs, // position
994     entity showto, float t, // show to whom? Use a flag to indicate a team
995     entity own, .entity ownfield, // remove when own gets killed
996     float hideable, // true when it should be controlled by cl_hidewaypoints
997     entity icon // initial icon
998 )
999 {
1000     entity wp = new(sprite_waypoint);
1001     wp.fade_time = _lifetime; // if negative tells client not to fade it out
1002     if(_lifetime < 0)
1003         _lifetime = -_lifetime;
1004     wp.teleport_time = time + _lifetime;
1005     wp.exteriormodeltoclient = ref;
1006     if (ref)
1007     {
1008         wp.view_ofs = ofs;
1009         setorigin(wp, ref.origin + ofs);
1010     }
1011     else
1012         setorigin(wp, ofs);
1013     wp.enemy = showto;
1014     wp.team = t;
1015     wp.owner = own;
1016     wp.currentammo = hideable;
1017     if (own)
1018     {
1019         if (own.(ownfield))
1020             delete(own.(ownfield));
1021         own.(ownfield) = wp;
1022         wp.owned_by_field = ownfield;
1023     }
1024     wp.fade_rate = maxdistance;
1025     setthink(wp, WaypointSprite_Think);
1026     wp.nextthink = time;
1027     wp.model1 = spr.netname;
1028     setcefc(wp, WaypointSprite_Customize);
1029     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1030     wp.reset2 = WaypointSprite_Reset;
1031     wp.cnt = icon.m_id;
1032     wp.colormod = spr.m_color;
1033     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1034     return wp;
1035 }
1036
1037 entity WaypointSprite_SpawnFixed(
1038     entity spr,
1039     vector ofs,
1040     entity own,
1041     .entity ownfield,
1042     entity icon // initial icon
1043 )
1044 {
1045     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, own, ownfield, true, icon);
1046 }
1047
1048 entity WaypointSprite_DeployFixed(
1049     entity spr,
1050     float limited_range,
1051     entity player,
1052     vector ofs,
1053     entity icon // initial icon
1054 )
1055 {
1056     float t;
1057     if (teamplay)
1058         t = player.team;
1059     else
1060         t = 0;
1061     float maxdistance;
1062     if (limited_range)
1063         maxdistance = waypointsprite_limitedrange;
1064     else
1065         maxdistance = 0;
1066     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, NULL, ofs, NULL, t, player, waypointsprite_deployed_fixed, false, icon);
1067 }
1068
1069 entity WaypointSprite_DeployPersonal(
1070     entity spr,
1071     entity player,
1072     vector ofs,
1073     entity icon // initial icon
1074 )
1075 {
1076     return WaypointSprite_Spawn(spr, 0, 0, NULL, ofs, NULL, 0, player, waypointsprite_deployed_personal, false, icon);
1077 }
1078
1079 entity WaypointSprite_Attach(
1080     entity spr,
1081     entity player,
1082     float limited_range,
1083     entity icon // initial icon
1084 )
1085 {
1086     float t;
1087     if (player.waypointsprite_attachedforcarrier)
1088         return NULL; // can't attach to FC
1089     if (teamplay)
1090         t = player.team;
1091     else
1092         t = 0;
1093     float maxdistance;
1094     if (limited_range)
1095         maxdistance = waypointsprite_limitedrange;
1096     else
1097         maxdistance = 0;
1098     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, player, '0 0 64', NULL, t, player, waypointsprite_attached, false, icon);
1099 }
1100
1101 entity WaypointSprite_AttachCarrier(
1102     entity spr,
1103     entity carrier,
1104     entity icon // initial icon and color
1105 )
1106 {
1107     WaypointSprite_Kill(carrier.waypointsprite_attached); // FC overrides attached
1108     entity e = WaypointSprite_Spawn(spr, 0, 0, carrier, '0 0 64', NULL, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1109     if (carrier.health)
1110     {
1111         WaypointSprite_UpdateMaxHealth(e, '1 0 0' * healtharmor_maxdamage(start_health, start_armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id) * 2);
1112         WaypointSprite_UpdateHealth(e, '1 0 0' * healtharmor_maxdamage(carrier.health, carrier.armorvalue, autocvar_g_balance_armor_blockpercent, DEATH_WEAPON.m_id));
1113     }
1114     return e;
1115 }
1116
1117 void WaypointSprite_DetachCarrier(entity carrier)
1118 {
1119     WaypointSprite_Disown(carrier.waypointsprite_attachedforcarrier, waypointsprite_deadlifetime);
1120 }
1121
1122 void WaypointSprite_ClearPersonal(entity this)
1123 {
1124     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1125 }
1126
1127 void WaypointSprite_ClearOwned(entity this)
1128 {
1129     WaypointSprite_Kill(this.waypointsprite_deployed_fixed);
1130     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1131     WaypointSprite_Kill(this.waypointsprite_attached);
1132 }
1133
1134 void WaypointSprite_PlayerDead(entity this)
1135 {
1136     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1137     WaypointSprite_DetachCarrier(this);
1138 }
1139
1140 void WaypointSprite_PlayerGone(entity this)
1141 {
1142     WaypointSprite_Disown(this.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1143     WaypointSprite_Kill(this.waypointsprite_deployed_personal);
1144     WaypointSprite_Disown(this.waypointsprite_attached, waypointsprite_deadlifetime);
1145     WaypointSprite_DetachCarrier(this);
1146 }
1147 #endif