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