a11feab9eceac5b1947e22290cd6177cc8df5c17
[xonotic/xonotic-data.pk3dir.git] / qcsrc / common / mutators / mutator / waypoints / waypointsprites.qc
1 #include "waypointsprites.qh"
2
3 #ifdef IMPLEMENTATION
4
5 REGISTER_MUTATOR(waypointsprites, true);
6
7 REGISTER_NET_LINKED(waypointsprites)
8
9 #ifdef SVQC
10 /** flags origin [team displayrule] [spritename] [spritename2] [spritename3] [lifetime maxdistance hideable] */
11 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags)
12 {
13     WriteHeader(MSG_ENTITY, waypointsprites);
14
15     sendflags = sendflags & 0x7F;
16
17     if (self.max_health || (self.pain_finished && (time < self.pain_finished + 0.25)))
18         sendflags |= 0x80;
19
20     int f = 0;
21     if(self.currentammo)
22         f |= 1; // hideable
23     if(self.exteriormodeltoclient == to)
24         f |= 2; // my own
25
26     MUTATOR_CALLHOOK(SendWaypoint, this, to, sendflags, f);
27
28     WriteByte(MSG_ENTITY, sendflags);
29     WriteByte(MSG_ENTITY, self.wp_extra);
30
31     if (sendflags & 0x80)
32     {
33         if (self.max_health)
34         {
35             WriteByte(MSG_ENTITY, (self.health / self.max_health) * 191.0);
36         }
37         else
38         {
39             float dt = self.pain_finished - time;
40             dt = bound(0, dt * 32, 16383);
41             WriteByte(MSG_ENTITY, (dt & 0xFF00) / 256 + 192);
42             WriteByte(MSG_ENTITY, (dt & 0x00FF));
43         }
44     }
45
46     if (sendflags & 64)
47     {
48         WriteCoord(MSG_ENTITY, self.origin.x);
49         WriteCoord(MSG_ENTITY, self.origin.y);
50         WriteCoord(MSG_ENTITY, self.origin.z);
51     }
52
53     if (sendflags & 1)
54     {
55         WriteByte(MSG_ENTITY, self.team);
56         WriteByte(MSG_ENTITY, self.rule);
57     }
58
59     if (sendflags & 2)
60         WriteString(MSG_ENTITY, self.model1);
61
62     if (sendflags & 4)
63         WriteString(MSG_ENTITY, self.model2);
64
65     if (sendflags & 8)
66         WriteString(MSG_ENTITY, self.model3);
67
68     if (sendflags & 16)
69     {
70         WriteCoord(MSG_ENTITY, self.fade_time);
71         WriteCoord(MSG_ENTITY, self.teleport_time);
72         WriteShort(MSG_ENTITY, self.fade_rate); // maxdist
73         WriteByte(MSG_ENTITY, f);
74     }
75
76     if (sendflags & 32)
77     {
78         WriteByte(MSG_ENTITY, self.cnt); // icon on radar
79         WriteByte(MSG_ENTITY, self.colormod.x * 255.0);
80         WriteByte(MSG_ENTITY, self.colormod.y * 255.0);
81         WriteByte(MSG_ENTITY, self.colormod.z * 255.0);
82
83         if (WaypointSprite_isteammate(self.owner, WaypointSprite_getviewentity(to)))
84         {
85             float dt = (self.waypointsprite_helpmetime - time) / 0.1;
86             if (dt < 0)
87                 dt = 0;
88             if (dt > 255)
89                 dt = 255;
90             WriteByte(MSG_ENTITY, dt);
91         }
92         else
93             WriteByte(MSG_ENTITY, 0);
94     }
95
96     return true;
97 }
98 #endif
99
100 #ifdef CSQC
101 void Ent_WaypointSprite();
102 NET_HANDLE(waypointsprites, bool isnew) {
103     Ent_WaypointSprite();
104     return true;
105 }
106
107 void Ent_RemoveWaypointSprite()
108 {SELFPARAM();
109     if (self.netname) strunzone(self.netname);
110     if (self.netname2) strunzone(self.netname2);
111     if (self.netname3) strunzone(self.netname3);
112 }
113
114 /** flags origin [team displayrule] [spritename] [spritename2] [spritename3] [lifetime maxdistance hideable] */
115 void Ent_WaypointSprite()
116 {SELFPARAM();
117     int sendflags = ReadByte();
118     self.wp_extra = ReadByte();
119
120     if (!self.spawntime)
121         self.spawntime = time;
122
123     self.draw2d = Draw_WaypointSprite;
124
125     InterpolateOrigin_Undo();
126     self.iflags |= IFLAG_ORIGIN;
127
128     if (sendflags & 0x80)
129     {
130         int t = ReadByte();
131         if (t < 192)
132         {
133             self.health = t / 191.0;
134             self.build_finished = 0;
135         }
136         else
137         {
138             t = (t - 192) * 256 + ReadByte();
139             self.build_started = servertime;
140             if (self.build_finished)
141                 self.build_starthealth = bound(0, self.health, 1);
142             else
143                 self.build_starthealth = 0;
144             self.build_finished = servertime + t / 32;
145         }
146     }
147     else
148     {
149         self.health = -1;
150         self.build_finished = 0;
151     }
152
153     if (sendflags & 64)
154     {
155         // unfortunately, this needs to be exact (for the 3D display)
156         self.origin_x = ReadCoord();
157         self.origin_y = ReadCoord();
158         self.origin_z = ReadCoord();
159         setorigin(self, self.origin);
160     }
161
162     if (sendflags & 1)
163     {
164         self.team = ReadByte();
165         self.rule = ReadByte();
166     }
167
168     if (sendflags & 2)
169     {
170         if (self.netname)
171             strunzone(self.netname);
172         self.netname = strzone(ReadString());
173     }
174
175     if (sendflags & 4)
176     {
177         if (self.netname2)
178             strunzone(self.netname2);
179         self.netname2 = strzone(ReadString());
180     }
181
182     if (sendflags & 8)
183     {
184         if (self.netname3)
185             strunzone(self.netname3);
186         self.netname3 = strzone(ReadString());
187     }
188
189     if (sendflags & 16)
190     {
191         self.lifetime = ReadCoord();
192         self.fadetime = ReadCoord();
193         self.maxdistance = ReadShort();
194         self.hideflags = ReadByte();
195     }
196
197     if (sendflags & 32)
198     {
199         int f = ReadByte();
200         self.teamradar_icon = f & BITS(7);
201         if (f & BIT(7))
202         {
203             self.(teamradar_times[self.teamradar_time_index]) = time;
204             self.teamradar_time_index = (self.teamradar_time_index + 1) % MAX_TEAMRADAR_TIMES;
205         }
206         self.teamradar_color_x = ReadByte() / 255.0;
207         self.teamradar_color_y = ReadByte() / 255.0;
208         self.teamradar_color_z = ReadByte() / 255.0;
209         self.helpme = ReadByte() * 0.1;
210         if (self.helpme > 0)
211             self.helpme += servertime;
212     }
213
214     InterpolateOrigin_Note();
215
216     self.entremove = Ent_RemoveWaypointSprite;
217 }
218 #endif
219
220 #ifdef CSQC
221 float spritelookupblinkvalue(string s)
222 {SELFPARAM();
223     if (s == WP_Weapon.netname) {
224         if (get_weaponinfo(self.wp_extra).spawnflags & WEP_FLAG_SUPERWEAPON)
225             return 2;
226     }
227     if (s == WP_Item.netname) return Items_from(self.wp_extra).m_waypointblink;
228
229     return 1;
230 }
231
232 vector spritelookupcolor(entity this, string s, vector def)
233 {
234     if (s == WP_Weapon.netname  || s == RADARICON_Weapon.netname) return get_weaponinfo(this.wp_extra).wpcolor;
235     if (s == WP_Item.netname    || s == RADARICON_Item.netname) return Items_from(this.wp_extra).m_color;
236     if (MUTATOR_CALLHOOK(WP_Format, this, s))
237     {
238         return MUTATOR_ARGV(0, vector);
239     }
240     return def;
241 }
242
243 string spritelookuptext(string s)
244 {SELFPARAM();
245     if (s == WP_RaceStartFinish.netname) return (race_checkpointtime || race_mycheckpointtime) ? _("Finish") : _("Start");
246     if (s == WP_Weapon.netname) return get_weaponinfo(self.wp_extra).m_name;
247     if (s == WP_Item.netname) return Items_from(self.wp_extra).m_waypoint;
248     if (s == WP_Monster.netname) return get_monsterinfo(self.wp_extra).monster_name;
249     if (MUTATOR_CALLHOOK(WP_Format, this, s))
250     {
251         return MUTATOR_ARGV(0, string);
252     }
253
254     // need to loop, as our netname could be one of three
255     FOREACH(Waypoints, it.netname == s, LAMBDA(
256         return it.m_name;
257     ));
258
259     return s;
260 }
261 #endif
262
263 #ifdef CSQC
264 void drawrotpic(vector org, float rot, string pic, vector sz, vector hotspot, vector rgb, float a, float f)
265 {
266     vector v1, v2, v3, v4;
267
268     hotspot = -1 * hotspot;
269
270     // hotspot-relative coordinates of the corners
271     v1 = hotspot;
272     v2 = hotspot + '1 0 0' * sz.x;
273     v3 = hotspot + '1 0 0' * sz.x + '0 1 0' * sz.y;
274     v4 = hotspot                  + '0 1 0' * sz.y;
275
276     // rotate them, and make them absolute
277     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
278     v1 = rotate(v1, rot) + org;
279     v2 = rotate(v2, rot) + org;
280     v3 = rotate(v3, rot) + org;
281     v4 = rotate(v4, rot) + org;
282
283     // draw them
284     R_BeginPolygon(pic, f);
285     R_PolygonVertex(v1, '0 0 0', rgb, a);
286     R_PolygonVertex(v2, '1 0 0', rgb, a);
287     R_PolygonVertex(v3, '1 1 0', rgb, a);
288     R_PolygonVertex(v4, '0 1 0', rgb, a);
289     R_EndPolygon();
290 }
291
292 void drawquad(vector o, vector ri, vector up, string pic, vector rgb, float a, float f)
293 {
294     R_BeginPolygon(pic, f);
295     R_PolygonVertex(o, '0 0 0', rgb, a);
296     R_PolygonVertex(o + ri, '1 0 0', rgb, a);
297     R_PolygonVertex(o + up + ri, '1 1 0', rgb, a);
298     R_PolygonVertex(o + up, '0 1 0', rgb, a);
299     R_EndPolygon();
300 }
301
302 void drawhealthbar(vector org, float rot, float h, vector sz, vector hotspot, float width, float theheight, float margin, float border, float align, vector rgb, float a, vector hrgb, float ha, float f)
303 {
304     vector o, ri, up;
305     float owidth; // outer width
306
307     hotspot = -1 * hotspot;
308
309     // hotspot-relative coordinates of the healthbar corners
310     o = hotspot;
311     ri = '1 0 0';
312     up = '0 1 0';
313
314     rot = -rot; // rotate by the opposite angle, as our coordinate system is reversed
315     o = rotate(o, rot) + org;
316     ri = rotate(ri, rot);
317     up = rotate(up, rot);
318
319     owidth = width + 2 * border;
320     o = o - up * (margin + border + theheight) + ri * (sz.x - owidth) * 0.5;
321
322     drawquad(o - up * border,                               ri * owidth,    up * border,    "", rgb,  a,  f);
323     drawquad(o + up * theheight,                            ri * owidth,    up * border,    "", rgb,  a,  f);
324     drawquad(o,                                             ri * border,    up * theheight, "", rgb,  a,  f);
325     drawquad(o + ri * (owidth - border),                    ri * border,    up * theheight, "", rgb,  a,  f);
326     drawquad(o + ri * (border + align * ((1 - h) * width)), ri * width * h, up * theheight, "", hrgb, ha, f);
327 }
328
329 // returns location of sprite text
330 vector drawspritearrow(vector o, float ang, vector rgb, float a, float t)
331 {
332     float size   = 9.0 * t;
333     float border = 1.5 * t;
334     float margin = 4.0 * t;
335
336     float borderDiag = border * 1.414;
337     vector arrowX  = eX * size;
338     vector arrowY  = eY * (size+borderDiag);
339     vector borderX = eX * (size+borderDiag);
340     vector borderY = eY * (size+borderDiag+border);
341
342     R_BeginPolygon("", DRAWFLAG_NORMAL);
343     R_PolygonVertex(o,                                  '0 0 0', '0 0 0', a);
344     R_PolygonVertex(o + rotate(arrowY  - borderX, ang), '0 0 0', '0 0 0', a);
345     R_PolygonVertex(o + rotate(borderY - borderX, ang), '0 0 0', '0 0 0', a);
346     R_PolygonVertex(o + rotate(borderY + borderX, ang), '0 0 0', '0 0 0', a);
347     R_PolygonVertex(o + rotate(arrowY  + borderX, ang), '0 0 0', '0 0 0', a);
348     R_EndPolygon();
349
350     R_BeginPolygon("", DRAWFLAG_ADDITIVE);
351     R_PolygonVertex(o + rotate(eY * borderDiag, ang), '0 0 0', rgb, a);
352     R_PolygonVertex(o + rotate(arrowY - arrowX, ang), '0 0 0', rgb, a);
353     R_PolygonVertex(o + rotate(arrowY + arrowX, ang), '0 0 0', rgb, a);
354     R_EndPolygon();
355
356     return o + rotate(eY * (borderDiag+size+margin), ang);
357 }
358
359 // returns location of sprite healthbar
360 vector drawspritetext(vector o, float ang, float minwidth, vector rgb, float a, vector fontsize, string s)
361 {
362     float algnx, algny;
363     float sw, w, h;
364     float aspect, sa, ca;
365
366     sw = stringwidth(s, false, fontsize);
367     if (sw > minwidth)
368         w = sw;
369     else
370         w = minwidth;
371     h = fontsize.y;
372
373     // how do corners work?
374     aspect = vid_conwidth / vid_conheight;
375     sa = sin(ang);
376     ca = cos(ang) * aspect;
377     if (fabs(sa) > fabs(ca))
378     {
379         algnx = (sa < 0);
380         float f = fabs(sa);
381         algny = 0.5 - 0.5 * (f ? (ca / f) : 0);
382     }
383     else
384     {
385         float f = fabs(ca);
386         algnx = 0.5 - 0.5 * (f ? (sa / f) : 0);
387         algny = (ca < 0);
388     }
389
390     // align
391     o.x -= w * algnx;
392     o.y -= h * algny;
393
394     // we want to be onscreen
395     if (o.x < 0)
396         o.x = 0;
397     if (o.y < 0)
398         o.y = 0;
399     if (o.x > vid_conwidth - w)
400         o.x = vid_conwidth - w;
401     if (o.y > vid_conheight - h)
402         o.x = vid_conheight - h;
403
404     o.x += 0.5 * (w - sw);
405
406     drawstring(o, s, fontsize, rgb, a, DRAWFLAG_NORMAL);
407
408     o.x += 0.5 * sw;
409     o.y += 0.5 * h;
410
411     return o;
412 }
413
414 vector fixrgbexcess_move(vector rgb, vector src, vector dst)
415 {
416     vector yvec = '0.299 0.587 0.114';
417     return rgb + dst * ((src * yvec) / (dst * yvec)) * ((rgb - '1 1 1') * src);
418 }
419
420 vector fixrgbexcess(vector rgb)
421 {
422     if (rgb.x > 1) {
423         rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 1');
424         if (rgb.y > 1) {
425             rgb = fixrgbexcess_move(rgb, '0 1 0', '0 0 1');
426             if (rgb.z > 1) rgb.z = 1;
427         } else if (rgb.z > 1) {
428             rgb = fixrgbexcess_move(rgb, '0 0 1', '0 1 0');
429             if (rgb.y > 1) rgb.y = 1;
430         }
431     } else if (rgb.y > 1) {
432         rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 1');
433         if (rgb.x > 1) {
434             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 0 1');
435             if (rgb.z > 1) rgb.z = 1;
436         } else if (rgb.z > 1) {
437             rgb = fixrgbexcess_move(rgb, '0 0 1', '1 0 0');
438             if (rgb.x > 1) rgb.x = 1;
439         }
440     } else if (rgb.z > 1) {
441         rgb = fixrgbexcess_move(rgb, '0 0 1', '1 1 0');
442         if (rgb.x > 1) {
443             rgb = fixrgbexcess_move(rgb, '1 0 0', '0 1 0');
444             if (rgb.y > 1) rgb.y = 1;
445         } else if (rgb.y > 1) {
446             rgb = fixrgbexcess_move(rgb, '0 1 0', '1 0 0');
447             if (rgb.x > 1) rgb.x = 1;
448         }
449     }
450     return rgb;
451 }
452
453 void Draw_WaypointSprite(entity this)
454 {
455     if (self.lifetime)
456         self.alpha = pow(bound(0, (self.fadetime - time) / self.lifetime, 1), waypointsprite_timealphaexponent);
457     else
458         self.alpha = 1;
459
460     if (self.hideflags & 2)
461         return; // radar only
462
463     if (autocvar_cl_hidewaypoints >= 2)
464         return;
465
466     if (self.hideflags & 1)
467         if (autocvar_cl_hidewaypoints)
468             return; // fixed waypoint
469
470     InterpolateOrigin_Do();
471
472     float t = GetPlayerColor(player_localnum) + 1;
473
474     string spriteimage = "";
475
476     // choose the sprite
477     switch (self.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 = self.netname;
486             break;
487         case SPRITERULE_DEFAULT:
488             if (self.team)
489             {
490                 if (self.team == t)
491                     spriteimage = self.netname;
492                 else
493                     spriteimage = "";
494             }
495             else
496                 spriteimage = self.netname;
497             break;
498         case SPRITERULE_TEAMPLAY:
499             if (t == NUM_SPECTATOR + 1)
500                 spriteimage = self.netname3;
501             else if (self.team == t)
502                 spriteimage = self.netname2;
503             else
504                 spriteimage = self.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(self.origin - view_origin);
518
519     float a;
520     a = self.alpha * autocvar_hud_panel_fg_alpha;
521
522     if (self.maxdistance > waypointsprite_normdistance)
523         a *= pow(bound(0, (self.maxdistance - dist) / (self.maxdistance - waypointsprite_normdistance), 1), waypointsprite_distancealphaexponent);
524     else if (self.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(self, spriteimage, self.teamradar_color);
528     if (rgb == '0 0 0')
529     {
530         self.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 (self.helpme && time < self.helpme)
537             a *= SPRITE_HELPME_BLINK;
538         else if (!self.lifetime) // fading out waypoints don't blink
539             a *= spritelookupblinkvalue(spriteimage);
540     }
541
542     if (a > 1)
543     {
544         rgb *= a;
545         a = 1;
546     }
547
548     if (a <= 0.003)
549         return;
550
551     rgb = fixrgbexcess(rgb);
552
553     vector o;
554     float ang;
555
556     o = project_3d_to_2d(self.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 (self.build_finished)
633     {
634         if (time < self.build_finished + 0.25)
635         {
636             if (time < self.build_started)
637                 self.health = self.build_starthealth;
638             else if (time < self.build_finished)
639                 self.health = (time - self.build_started) / (self.build_finished - self.build_started) * (1 - self.build_starthealth) + self.build_starthealth;
640             else
641                 self.health = 1;
642         }
643         else
644             self.health = -1;
645     }
646
647     o = drawspritearrow(o, ang, rgb, a, SPRITE_ARROW_SCALE * t);
648
649     string txt;
650     if (autocvar_g_waypointsprite_spam && waypointsprite_count >= autocvar_g_waypointsprite_spam)
651         txt = _("Spam");
652     else
653         txt = spritelookuptext(spriteimage);
654     if (self.helpme && time < self.helpme)
655         txt = sprintf(_("%s needing help!"), txt);
656     if (autocvar_g_waypointsprite_uppercase)
657         txt = strtoupper(txt);
658
659     draw_beginBoldFont();
660     if (self.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 (self.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                 self.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_InitClient(entity e)
881 {
882 }
883
884 void WaypointSprite_Kill(entity wp)
885 {
886     if (!wp) return;
887     if (wp.owner) wp.owner.(wp.owned_by_field) = world;
888     remove(wp);
889 }
890
891 void WaypointSprite_Disown(entity wp, float fadetime)
892 {
893     if (!wp) return;
894     if (wp.classname != "sprite_waypoint")
895     {
896         backtrace("Trying to disown a non-waypointsprite");
897         return;
898     }
899     if (wp.owner)
900     {
901         if (wp.exteriormodeltoclient == wp.owner)
902             wp.exteriormodeltoclient = world;
903         wp.owner.(wp.owned_by_field) = world;
904         wp.owner = world;
905
906         WaypointSprite_FadeOutIn(wp, fadetime);
907     }
908 }
909
910 void WaypointSprite_Think()
911 {SELFPARAM();
912     bool doremove = false;
913
914     if (self.fade_time && time >= self.teleport_time)
915     {
916         doremove = true;
917     }
918
919     if (self.exteriormodeltoclient)
920         WaypointSprite_UpdateOrigin(self, self.exteriormodeltoclient.origin + self.view_ofs);
921
922     if (doremove)
923         WaypointSprite_Kill(self);
924     else
925         self.nextthink = time; // WHY?!?
926 }
927
928 float WaypointSprite_visible_for_player(entity e)
929 {SELFPARAM();
930     // personal waypoints
931     if (self.enemy && self.enemy != e)
932         return false;
933
934     // team waypoints
935     if (self.rule == SPRITERULE_SPECTATOR)
936     {
937         if (!autocvar_sv_itemstime)
938             return false;
939         if (!warmup_stage && IS_PLAYER(e))
940             return false;
941     }
942     else if (self.team && self.rule == SPRITERULE_DEFAULT)
943     {
944         if (self.team != e.team)
945             return false;
946         if (!IS_PLAYER(e))
947             return false;
948     }
949
950     return true;
951 }
952
953 entity WaypointSprite_getviewentity(entity e)
954 {
955     if (IS_SPEC(e)) e = e.enemy;
956     /* TODO idea (check this breaks nothing)
957     else if (e.classname == "observer")
958         e = world;
959     */
960     return e;
961 }
962
963 float WaypointSprite_isteammate(entity e, entity e2)
964 {
965     if (teamplay)
966         return e2.team == e.team;
967     return e2 == e;
968 }
969
970 float WaypointSprite_Customize()
971 {SELFPARAM();
972     // this is not in SendEntity because it shall run every frame, not just every update
973
974     // make spectators see what the player would see
975     entity e = WaypointSprite_getviewentity(other);
976
977     if (MUTATOR_CALLHOOK(CustomizeWaypoint, self, other))
978         return false;
979
980     return self.waypointsprite_visible_for_player(e);
981 }
982
983 bool WaypointSprite_SendEntity(entity this, entity to, float sendflags);
984
985 void WaypointSprite_Reset()
986 {SELFPARAM();
987     // if a WP wants to time out, let it time out immediately; other WPs ought to be reset/killed by their owners
988
989     if (self.fade_time)
990         WaypointSprite_Kill(self);
991 }
992
993 entity WaypointSprite_Spawn(
994     entity spr, // sprite
995     float _lifetime, float maxdistance, // lifetime, max distance
996     entity ref, vector ofs, // position
997     entity showto, float t, // show to whom? Use a flag to indicate a team
998     entity own, .entity ownfield, // remove when own gets killed
999     float hideable, // true when it should be controlled by cl_hidewaypoints
1000     entity icon // initial icon
1001 )
1002 {
1003     entity wp = new(sprite_waypoint);
1004     make_pure(wp);
1005     wp.teleport_time = time + _lifetime;
1006     wp.fade_time = _lifetime;
1007     wp.exteriormodeltoclient = ref;
1008     if (ref)
1009     {
1010         wp.view_ofs = ofs;
1011         setorigin(wp, ref.origin + ofs);
1012     }
1013     else
1014         setorigin(wp, ofs);
1015     wp.enemy = showto;
1016     wp.team = t;
1017     wp.owner = own;
1018     wp.currentammo = hideable;
1019     if (own)
1020     {
1021         if (own.(ownfield))
1022             remove(own.(ownfield));
1023         own.(ownfield) = wp;
1024         wp.owned_by_field = ownfield;
1025     }
1026     wp.fade_rate = maxdistance;
1027     wp.think = WaypointSprite_Think;
1028     wp.nextthink = time;
1029     wp.model1 = spr.netname;
1030     wp.customizeentityforclient = WaypointSprite_Customize;
1031     wp.waypointsprite_visible_for_player = WaypointSprite_visible_for_player;
1032     wp.reset2 = WaypointSprite_Reset;
1033     wp.cnt = icon.m_id;
1034     wp.colormod = spr.m_color;
1035     Net_LinkEntity(wp, false, 0, WaypointSprite_SendEntity);
1036     return wp;
1037 }
1038
1039 entity WaypointSprite_SpawnFixed(
1040     entity spr,
1041     vector ofs,
1042     entity own,
1043     .entity ownfield,
1044     entity icon // initial icon
1045 )
1046 {
1047     return WaypointSprite_Spawn(spr, 0, 0, world, ofs, world, 0, own, ownfield, true, icon);
1048 }
1049
1050 entity WaypointSprite_DeployFixed(
1051     entity spr,
1052     float limited_range,
1053     vector ofs,
1054     entity icon // initial icon
1055 )
1056 {SELFPARAM();
1057     float t;
1058     if (teamplay)
1059         t = self.team;
1060     else
1061         t = 0;
1062     float maxdistance;
1063     if (limited_range)
1064         maxdistance = waypointsprite_limitedrange;
1065     else
1066         maxdistance = 0;
1067     return WaypointSprite_Spawn(spr, waypointsprite_deployed_lifetime, maxdistance, world, ofs, world, t, self, waypointsprite_deployed_fixed, false, icon);
1068 }
1069
1070 entity WaypointSprite_DeployPersonal(
1071     entity spr,
1072     vector ofs,
1073     entity icon // initial icon
1074 )
1075 {SELFPARAM();
1076     return WaypointSprite_Spawn(spr, 0, 0, world, ofs, world, 0, self, waypointsprite_deployed_personal, false, icon);
1077 }
1078
1079 entity WaypointSprite_Attach(
1080     entity spr,
1081     float limited_range,
1082     entity icon // initial icon
1083 )
1084 {SELFPARAM();
1085     float t;
1086     if (self.waypointsprite_attachedforcarrier)
1087         return world; // can't attach to FC
1088     if (teamplay)
1089         t = self.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, self, '0 0 64', world, t, self, 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', world, carrier.team, carrier, waypointsprite_attachedforcarrier, false, icon);
1108     if (e)
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()
1122 {SELFPARAM();
1123     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1124 }
1125
1126 void WaypointSprite_ClearOwned()
1127 {SELFPARAM();
1128     WaypointSprite_Kill(self.waypointsprite_deployed_fixed);
1129     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1130     WaypointSprite_Kill(self.waypointsprite_attached);
1131 }
1132
1133 void WaypointSprite_PlayerDead()
1134 {SELFPARAM();
1135     WaypointSprite_Disown(self.waypointsprite_attached, waypointsprite_deadlifetime);
1136     WaypointSprite_DetachCarrier(self);
1137 }
1138
1139 void WaypointSprite_PlayerGone()
1140 {SELFPARAM();
1141     WaypointSprite_Disown(self.waypointsprite_deployed_fixed, waypointsprite_deadlifetime);
1142     WaypointSprite_Kill(self.waypointsprite_deployed_personal);
1143     WaypointSprite_Disown(self.waypointsprite_attached, waypointsprite_deadlifetime);
1144     WaypointSprite_DetachCarrier(self);
1145 }
1146 #endif
1147 #endif